Chapter 3

레이어와 예외

  • 3.1 복잡성은 아래로
  • 3.2 합칠 때와 나눌 때
  • 3.3 예외 처리와 복잡성

모듈을 설계할 때 어려운 문제에 부딪히면 두 가지 선택이 있어. 모듈 내부에서 해결하거나, 사용자에게 떠넘기거나. 대부분의 경우 내부에서 해결하는 게 정답이야. 이유는 산수 문제지 — 모듈 개발자는 한 명이고 사용자는 많으니까. 개발자가 한 번 고생해서 복잡성을 숨기면 모든 사용자가 혜택을 보고, 반대로 떠넘기면 사용자 수만큼 복잡성이 증폭돼.

네트워크 프로토콜에서 패킷 유실을 생각해 보자. 프로토콜 레이어에서 내부적으로 재전송 메커니즘으로 해결하면 사용자는 신경 쓸 필요가 없어. 근데 "패킷이 유실될 수 있으니 직접 처리하세요"라고 떠넘기면 모든 애플리케이션 개발자가 재전송 로직을 짜야 하거든. 물론 예외도 있어 — 사용자가 이미 그 복잡성에 대한 지식을 갖고 있거나, 사용자만이 올바른 결정을 내릴 수 있는 경우에는 위로 올리는 게 맞지. 핵심은 **기본값(default)**으로 합리적인 동작을 제공하고, 정말 필요한 사용자만 세부 조정을 할 수 있게 하는 거야.

저자가 특별히 비판하는 패턴이 **설정 매개변수(Configuration Parameter)**의 남용이야. "이 값을 얼마로 해야 할지 모르겠으니 사용자가 설정하게 하자" — 이건 복잡성을 위로 올리는 전형적인 방법이지. 캐시 크기, 타임아웃 값, 스레드 풀 크기 같은 것들. 문제는 모듈 개발자보다 적절한 값을 아는 사용자가 거의 없다는 거야. 대부분의 사용자는 기본값을 그냥 쓰거든. 그렇다면 처음부터 모듈이 스스로 합리적인 기본값을 결정하거나, 런타임에 자동으로 적절한 값을 계산하는 게 낫지. 모듈 설계의 핵심 질문은 "내가 이 모듈의 사용자라면 편할까?"야. 좋은 설계자는 사용자의 편의를 위해 자기가 더 고생하는 것을 기꺼이 선택해.

다음은 소프트웨어 설계에서 매일 마주치는 질문 — 이 코드를 한 곳에 모을까, 분리할까? 합치는 게 나은 경우부터 보자. 두 모듈이 같은 지식을 공유한다면, 합치면 그 지식을 한 곳에서 관리할 수 있어. 정보 누출 문제의 해결책이기도 하지. 두 모듈이 항상 함께 사용된다면, 합치면 사용자가 알아야 할 인터페이스가 줄어들어. 비슷한 코드 중복이 있다면, 공통 부분을 추출해서 한 곳에 모으는 게 좋고. 저자는 특히 밀접하게 관련된 코드를 물리적으로 가까이 두라고 강조해. 관련된 코드가 여러 파일에 흩어져 있으면 왔다 갔다 해야 해서 인지적 부하가 높아지거든.

반대로 분리하는 게 나은 경우도 있어. 범용 코드와 특수 목적 코드가 섞여있으면 범용 부분을 분리하는 게 좋지. 두 기능이 서로 독립적이라면 합칠 이유가 없고. 메서드 분리의 경우, 저자는 무작정 메서드를 작게 쪼개는 것에 반대해. Robert Martin의 Clean Code에서 "메서드는 작아야 한다"고 했는데, 저자는 이에 동의하지 않아. 메서드를 쪼개면 각 메서드는 작아지지만 메서드 수가 늘어나고 호출 관계가 복잡해지고 인터페이스가 늘어나잖아. 메서드를 분리하는 기준은 크기가 아니라 추상화 수준이어야 해.

궁극적인 판단 기준은 하나야 — 전체 시스템의 복잡성이 줄어드는가? 합치든 분리하든, 인터페이스가 더 단순해지는지, 중복이 줄어드는지, 각 모듈의 목적이 더 명확해지는지, 개발자가 알아야 할 것이 줄어드는지 따져 보면 돼. 합쳐야 할 신호는 항상 함께 사용된다거나 같은 정보를 공유한다거나 한쪽 없이는 이해가 안 되는 경우고, 분리해야 할 신호는 독립적으로 이해 가능하거나 서로 다른 이유로 변경되는 경우야. 이 판단은 경험과 직관이 필요한 영역이지. 규칙을 기계적으로 적용하는 게 아니라 상황에 맞는 판단을 내려야 해.

마지막으로 예외(exception) 이야기야. 저자는 예외가 소프트웨어 복잡성의 주범이라고 봐. 예외 처리 코드가 정상 흐름보다 더 많은 코드를 차지하는 경우도 흔하거든. 예외는 정상 흐름과 별도의 코드 경로를 만들고, 전파되면서 여러 레이어에 걸쳐 복잡성을 퍼뜨리고, 거의 실행되지 않아서 테스트하기 어렵고 버그가 숨어있기 쉬워. **"예외를 던지는 건 쉽지만, 처리하는 건 어렵다"**는 말이 핵심이야. 많은 개발자가 "문제가 생기면 예외를 던지면 된다"고 생각하는데, 그 예외를 누군가는 처리해야 하잖아.

저자가 제시하는 핵심 기법이 **"에러를 정의 밖으로 밀어내기(Define Errors Out of Existence)"**야. 에러 상황을 정상 동작으로 재정의해서 예외 자체가 발생하지 않게 만드는 거지. 대표적인 예시가 파일 삭제 연산이야. "존재하지 않는 파일을 삭제하려고 하면?" 많은 시스템에서 에러를 던지지. 근데 삭제의 목적은 "파일이 없는 상태를 만드는 것"이잖아. 파일이 이미 없다면 목적은 달성된 거야 — 에러를 던질 필요가 없어. Python의 슬라이싱도 좋은 예시야. "hello"[2:100]이 에러가 아니라 "llo"를 반환하거든. API의 의미를 넓게 정의해서 특정 입력을 "에러"로 보지 않고 "자연스러운 케이스"로 포함시키면 예외가 사라져.

에러를 정의 밖으로 밀어낼 수 없는 경우에도 방법이 있어. **예외 마스킹(Exception Masking)**은 낮은 레이어에서 예외를 감지하고 상위 레이어 모르게 내부적으로 처리하는 거야. TCP가 패킷 유실을 자동으로 재전송하는 게 좋은 예시지. **예외 집계(Exception Aggregation)**는 여러 곳에서 발생하는 비슷한 예외를 한 곳에서 모아서 처리하는 거야. 웹 서버에서 각 요청 핸들러마다 모든 예외를 처리하는 대신 최상위 핸들러에서 "처리되지 않은 예외가 발생하면 500 에러를 반환한다"라고 일괄 처리하는 식이지. 그리고 정말 복구할 방법이 없는 심각한 에러 — 메모리 부족, 내부 데이터 불일치 같은 건 그냥 크래시하는 게 더 안전해. 잘못된 상태에서 계속 실행하는 것보다 깨끗하게 재시작하는 게 낫거든.


정리

3장 읽고 기억할 거 세 가지:

  1. 복잡성은 아래로 끌어내려야 해. 모듈 개발자가 고생해서 사용자를 편하게 만드는 게 전체 시스템 복잡성을 줄이는 방법이야. 설정 매개변수 남용은 복잡성을 떠넘기는 거지
  2. 합칠지 분리할지의 판단 기준은 "전체 시스템 복잡성이 줄어드는가?" 하나야. 메서드를 크기 기준으로 무작정 쪼개지 말고, 추상화 수준을 기준으로 판단하자
  3. 예외는 복잡성의 주범이야. API의 의미를 넓게 정의해서 에러를 없애고, 마스킹이나 집계로 예외 처리를 최소화하자. 정말 복구 불가능한 건 크래시가 답이야