Martinfowler Quotes

마틴 파울러는 “컴퓨터가 이해하는 코드는 누구나 작성할 수 있다. 좋은 프로그래머는 사람이 이해하는 코드를 작성한다.“라고 『Refactoring: Improving the Design of Existing Code』에서 말했다.

SICP Quotes

“프로그램은 사람이 읽기 위해 작성되어야 하며, 기계가 실행하는 것은 부차적인 일이다.“라는 명언은 『Structure and Interpretation of Computer Programs』에서 1984년에 소개되었다.

약 40년 전의 문장이 지금도 유효한 이유는, 코드를 실행하는 주체는 계속 빨라졌지만 코드를 읽는 주체는 여전히 사람이기 때문이다. 현재 Claude Code, Codex, Gemini CLI, Cursor 같은 도구를 활용하여 코드를 작성하는 시대에서도 여전히 사람이 이해하는 코드를 작성하는 것은 중요하며 AI를 활용하여 본인과 동료가 코드를 잘 이해하도록 작성 해야 한다.

Go는 Clear is better than clever라는 철학을 가지고 있다. 마틴 파울러가 말한 내용과 크게 다르지 않다.

본질은 동료가 코드를 이해하는데 드는 비용을 최소화하라는 원칙이다.

명확성과 가독성

명확성(Clarity)와 가독성(Readability)은 다르다. 명확성은 코드가 무엇을 하는지를 나타내는 성질이고 감추는게 적을 수록 명확성이 높다고 할 수 있다. 가독성은 코드를 얼마나 쉽고 편하게 읽고 이해할 수 있는 지를 의미한다.

Go는 명확성을 중시하는 언어이다. 아래 Go로 작성된 코드와 Spring 기반의 Kotlin 코드와 비교해보자.

for {
    select {
    case <-ctx.Done():
        return
    case msg := consumer.Messages():
        handle(msg)
    }
}

먼저 Go로 작성된 코드의 경우 언제 종료되고 누가 종료 시키고, 어떤 이벤트를 기다리는지 명확하다. 명확성을 중시하기 때문에 함수를 작성하더라도 무엇을 하는지가 다 드러난다.

@Transactional
fun createOrder(...) {
    // Busines logic
}

반면 Spring 기반의 Kotlin으로 작성된 코드의 경우 가독성은 높지만 명확성은 낮다. 위 함수는 Transaction의 시작과 종료가 어떻게 이뤄지고, 언제 Commit/Rollback 되고, Proxy 생성 여부 등을 모두 알아야 함수를 올바르게 이해했다고 할 수 있다.

즉 Spring은 비지니스 로직에 대한 가독성을 중시하며, 비지니스 로직을 이해하는데 방해가 되는 영역들을 Proxy, AOP 등을 통해 동작 방식을 감추는 특징을 가지고 있다.

그렇다고 Spring이 항상 Go보다 가독성이 좋다는 말은 아니며, Go가 가독성을 중시하지 않는다는 말도 아니다. 위에서 설명하는 가독성은 비지니스 로직에 대한 가독성이다.

가독성의 표면적 성질은 들여쓰기, 네이밍 컨벤션, 포매팅, 줄 길이와 같이 코드가 어떻게 쓰였는가 에 대한 것이다. Go에서는 이 영역의 상당 부분을 gofmt가 기계적으로 해결한다.

Go가 명확한 코드 작성을 위해 포기한 것들

  • 삼항 연산자가 없다 — x = cond ? a : b는 중첩되는 순간 해석 비용이 폭발한다. Go는 if 문을 쓰게 한다.
  • 예외(exception)가 없다 — 에러는 값이고, 제어 흐름은 눈에 보여야 한다.
  • while / do-while이 없다 — 반복은 for 하나로 표현한다. 반복문을 읽는 방법도 하나면 충분하다.
  • 암묵적 형변환이 없다 — int와 int64 조차 명시적으로 변환해야 한다. 타입이 조용히 바뀌는 일은 없다.
  • 구현 상속이 없다 — 깊은 상속 계층을 따라 올라가며 동작을 재구성할 필요가 없다. 조합(composition)으로 표현한다.
  • 연산자 오버로딩이 없다 — +는 언제나 + 다. 연산자의 의미가 타입마다 달라지지 않는다.

각 항목은 표현력의 손실이다. 같은 로직을 더 길게 써야 한다. Go 팀이 이 트레이드오프에서 일관되게 선택한 것은 **“작성자의 편의"가 아니라 “독자의 확신”**이다. 기능이 하나 빠질 때마다, 코드를 읽는 사람이 고려해야 할 경우의 수가 하나 줄어든다.

장황함과 명확함의 트레이드오프

Java/Kotlin의 경우에는 exception과 try-catch를 통해서 Happy Path를 깔끔하게 작성할 수 있다.

try {
    User user = repository.find(id);
    Profile profile = profileService.load(user);
    notifier.send(profile);
} catch (IOException e) {
    ...
}

하지만 실제 실행 경로(제어 흐름) 이 코드에 명확하게 드러나지 않는다. 특히 throw 된 예외가 어느 스택 프레임에서 잡히는지는 그 코드만 읽어서는 알 수 없다. 호출자, 호출자의 호출자, 어쩌면 프레임워크의 최상단까지 올라가야 한다.

반면 Go에서는 아래와 같이 err를 확인하는 패턴이 코드베이스 전체에 반복된다.

user, err := repository.Find(id)
if err != nil {
   return err
}

이러한 장황함을 통해서 얻는 것은 명확성이다. Go는 에러를 값으로 다루는 특징이 있다. 이 덕분에 모든 실패 경로가 함수에 다 드러난다.

좋은 코드는 해석(decode)이 필요 없다

Clever 한 코드는 어려운 문제를 해결하는 것처럼 보인다. 하지만 대부분의 경우 복잡성을 제거한 것이 아니라 코드 내부에 압축해 놓은 것이다. 반면 Clear 한 코드는 어려운 문제를 쉬워 보이게 만든다.

Clever Code

func processJobs(jobs []Job) []Result {
	results := make([]Result, len(jobs))

	var wg sync.WaitGroup

	for i, job := range jobs {
		wg.Add(1)

		go func() {
			defer wg.Done()

			results[i] = process(job)
		}()
	}

	wg.Wait()

	return results
}

위 코드는 짧고 빠르게 보이지만 여러 goroutine이 동시에 write 하는 복잡성이 숨겨져 있고 data race는 없는지, 실패하면 worker는 어떻게 처리하는지, worker 종료 시점은 언제인지 등 사고해야하는 내용이 증가했다.

Clear Code

func processJobs(jobs []Job) []Result {
	jobCh := make(chan Job)
	resultCh := make(chan Result)

	var wg sync.WaitGroup

	// Worker Pool
	// 제한된 개수의 worker가 job을 받아 병렬 처리한다.
	for i := 0; i < 4; i++ {
		wg.Add(1)

		go func() {
			defer wg.Done()

			// Worker
			// Producer가 전달한 Job을 처리하고 Result를 Consumer에게 전달한다.
			for job := range jobCh {
				resultCh <- process(job)
			}
		}()
	}

	// Producer
	// 처리해야 할 Job을 생성하고 Worker에게 전달한다.
	go func() {
		for _, job := range jobs {
			jobCh <- job
		}

		// 더 이상 전달할 Job이 없음을 알림
		close(jobCh)
	}()

	// Worker 종료 감시자
	// 모든 Worker가 작업을 완료하면 Result Channel을 닫는다.
	go func() {
		wg.Wait()
		close(resultCh)
	}()

	var results []Result

	// Consumer
	// Worker가 처리한 Result를 소비한다.
	for result := range resultCh {
		results = append(results, result)
	}

	return results
}

코드는 길어졌지만 공유 메모리가 없으며 누가 데이터를 소유하는지 명확하다.

위 두 코드의 차이는 문제의 복잡성을 없애는 것이 아니라, 복잡성을 사람이 이해할 수 있는 위치에 배치하는 데 있다.

첫 번째는 코드 내부에 복잡성을 압축하여 독자가 코드를 해석(decode) 해야 하는 양이 증가했다.

후자는 “여러 goroutine이 결과 배열을 직접 수정한다"라는 저수준 복잡성을 “Producer → Worker → Consumer"라는 사람이 이해하기 쉬운 모델로 변환하였다. 각 컴포넌트는 책임 경계가 명확하고 자신의 책임에만 집중한다. 코드는 길어졌지만, 동료는 구현 세부사항을 해석하는 대신 데이터의 흐름과 책임의 경계만 이해하면 된다.

마무리

AI가 코드를 대신 작성해주는 시대에 엔지니어의 실력은 무엇으로 증명될까? 타이핑 속도, 문법 지식, 언어의 숨겨진 기능을 아는 것, clever 한 트릭도 아니다. 이런 것들은 AI가 가장 잘 대체하는 영역이다.

AI 시대에 인간은 문제를 정의하는 능력과 복잡한 문제를 단순하게 표현하는 능력을 갖춰야 한다. 무엇을 만들어야 하는지 정의하고, 복잡함을 구조로 정리하고, 그 구조를 사람과 AI 모두가 오해 없이 이해할 수 있는 형태로 코드에 새기는 능력이 중요하다.

복잡성을 잘 다루기 위해서는 어디까지 책임을 나눌 것인지, 어떤 도메인이 필요한지, 무엇을 숨기고 노출할 것인지 등을 고려해야 한다.

이러한 복잡성을 어디에서 다룰지 결정하는 것을 **추상화(Abstraction)**라고 한다. Hides unnecessary details to Reduce complexity는 추상화를 설명하는 가장 좋은 문장 중 하나라고 생각한다.

AI 시대에서 추상화를 소프트웨어 엔지니어링에 잘 적용하는 능력을 갖추는 것이 중요하다.

References