모듈 설계
- 2.1 모듈: 인터페이스와 구현
- 2.2 깊은 모듈 vs 얕은 모듈
- 2.3 정보 은닉
- 2.4 범용 vs 특수 목적
- 2.5 패스스루 메서드
좋은 소프트웨어 설계의 핵심 단위는 모듈이야. 클래스든 함수든 서비스든 패키지든, 인터페이스와 구현을 가진 모든 코드 단위가 모듈이지. 이 모듈을 어떻게 설계하느냐가 시스템 전체의 복잡성을 결정해.
모든 모듈은 두 부분으로 나뉘어. **인터페이스(Interface)**는 모듈을 사용하기 위해 알아야 하는 것이고, **구현(Implementation)**은 인터페이스가 약속한 것을 실제로 수행하는 코드야. 인터페이스에는 공식적(formal) 부분과 비공식적(informal) 부분이 있는데, 공식적 부분은 컴파일러가 체크할 수 있는 것 — 메서드 이름, 매개변수 타입 같은 거고, 비공식적 부분은 코드만 봐서는 알 수 없는 것이야. "이 메서드 호출 전에 반드시 저 메서드를 먼저 호출해야 한다" 같은 제약이지. 비공식적 부분이 많을수록 모듈은 사용하기 어려워져.
모듈을 직사각형으로 시각화해 보자. 윗변의 너비가 인터페이스의 복잡성이고, 아래로의 깊이가 구현의 기능성이야. **깊은 모듈(Deep Module)**은 인터페이스가 단순하면서 내부에 강력한 기능을 숨기고 있어. 좁은 입구로 들어가면 넓은 공간이 펼쳐지는 느낌이지. 대표적인 예시가 Unix의 파일 I/O 인터페이스야:
open(path, flags) → fd
read(fd, buffer, count) → bytes_read
write(fd, buffer, count) → bytes_written
close(fd)
이 네 개 함수 뒤에는 디스크 관리, 캐싱, 파일 시스템 구조, 권한 관리 등 엄청난 복잡성이 숨어있어. 근데 사용자는 이 단순한 인터페이스만 알면 되거든. 이게 깊은 모듈의 위력이야. 반대로 **얕은 모듈(Shallow Module)**은 인터페이스가 복잡한데 내부 기능은 별거 없는 거야. Java의 LinkedList를 감싸면서 거의 같은 메서드를 그대로 노출하는 래퍼 클래스 같은 거지. 모듈의 가치 = 제공하는 기능 / 인터페이스의 복잡성 — 이 비율이 클수록 좋은 모듈이야.
저자가 만든 용어인 **클래시티스(Classitis)**도 알아두자. "클래스는 작아야 한다"는 원칙을 극단적으로 적용한 병폐야. Java 생태계에서 특히 많이 보이는 현상인데, 클래스를 너무 잘게 쪼개서 오히려 시스템 전체가 더 복잡해지는 거지. 클래스 수가 늘어나면 클래스 간의 인터페이스도 늘어나잖아. 한 가지 작업을 하려면 여러 클래스를 오가면서 코드를 따라가야 하고, 클래스 간의 관계를 이해해야 해. 클래스가 작은 것보다 인터페이스가 단순한 게 더 중요해. Java의 InputStream/BufferedInputStream 분리가 좋은 예시야 — 대부분 버퍼링이 필요한데 왜 별도 클래스로 분리해서 사용자가 직접 조합하게 만들었을까? InputStream 안에 버퍼링을 기본으로 넣는 게 더 깊은 모듈이 되었을 거잖아.
깊은 모듈을 만드는 핵심 기법이 **정보 은닉(Information Hiding)**이야. David Parnas가 1971년에 제안한 개념인데, 50년이 지난 지금도 가장 중요한 설계 원칙 중 하나지. 정보 은닉은 모듈의 설계 결정을 다른 모듈에게 숨기는 거야. 데이터 구조, 알고리즘, 파일 형식 같은 내부 지식을 인터페이스에 노출하지 않는 것이지. 어떤 모듈이 내부적으로 B-트리를 써서 데이터를 저장한다고 하자. 정보 은닉이 잘 되어 있으면 외부에서는 "데이터를 넣고 빼는" 인터페이스만 보이고, B-트리라는 사실은 몰라. 나중에 해시 테이블로 바꿔도 외부 코드는 영향을 받지 않지.
정보 은닉이 좋은 이유는 인터페이스가 단순해지고 시스템 진화가 쉬워지기 때문이야. 근데 주의할 게, 정보 은닉은 접근 제어(private/public)만으로 달성되는 게 아니야. private 변수로 만들어 놓고 getter/setter를 다 열어두면 사실상 숨긴 게 없는 거거든. 진짜 정보 은닉은 설계 수준에서 이루어져야 해.
정보 은닉의 반대가 **정보 누출(Information Leakage)**이야. 모듈 내부의 설계 결정이 외부로 새어나가는 건데, 저자는 이걸 소프트웨어 설계에서 가장 중요한 red flag라고 봐. 가장 흔한 유형은 인터페이스를 통한 누출 — 메서드의 매개변수나 반환값에 내부 구현 세부사항이 드러나는 경우야. 더 미묘한 유형은 배경 지식을 통한 누출이야. 파일을 쓰는 모듈과 읽는 모듈이 파일 형식에 대한 지식을 각각 독립적으로 갖고 있으면, 형식이 바뀔 때 두 모듈을 다 고쳐야 하잖아. 해결책은 그 공유 지식을 하나의 모듈로 모으는 거야.
저자가 특별히 경고하는 패턴이 **시간적 분해(Temporal Decomposition)**야. 코드를 실행 순서에 따라 모듈로 나누는 방식인데, 예를 들어 파일을 읽는 모듈, 수정하는 모듈, 쓰는 모듈을 따로 만들면 "읽기 → 수정 → 쓰기" 순서는 잘 반영되지만 파일 형식에 대한 지식이 양쪽에 퍼져. 모듈 분해의 기준은 실행 순서가 아니라 정보(지식)여야 해.
모듈의 범용성 이야기도 해야지. 모듈을 설계할 때 항상 마주치는 딜레마가 있어 — 지금 필요한 기능만 딱 맞게 만들 것인가(특수 목적), 아니면 더 넓은 상황에서 쓸 수 있게 만들 것인가(범용)? 결론부터 말하면 범용적인 모듈이 더 깊고 더 좋아. 핵심은 인터페이스의 단순성에 있어. 텍스트 에디터를 예로 들면, "백스페이스 키 처리", "딜리트 키 처리", "선택 영역 삭제" 메서드를 따로 만드는 건 특수 목적이지. 반면 "주어진 범위의 텍스트를 삭제한다"라는 하나의 범용 메서드면 세 가지 상황을 다 처리할 수 있어. 인터페이스는 더 단순한데 처리할 수 있는 상황은 더 많아 — 이게 깊은 모듈이야.
저자의 "somewhat general-purpose" 접근은 이래 — 인터페이스는 범용적으로, 구현은 현재 필요한 것만. API를 설계할 때는 다양한 용도를 지원할 수 있게 충분히 범용적으로 만들되, 실제 구현은 지금 당장 필요한 부분만 하는 거지. 범용성을 판단할 때 유용한 질문 세 가지가 있어: "이 API를 사용할 수 있는 가장 간단한 인터페이스는 뭔가?", "이 메서드가 몇 가지 상황에서 사용될 수 있는가?", "현재 필요에 맞게 이 API를 사용하기 쉬운가?"
마지막으로 패스스루 메서드(Pass-through Method) 이야기야. 받은 인자를 거의 그대로 다른 메서드에 전달하기만 하는 메서드인데, 아무런 가치도 추가하지 않으면서 인터페이스만 하나 더 늘리거든. 전형적인 얕은 모듈이야.
class A:
def do_something(self, x, y):
return self.b.do_something(x, y) # 그냥 전달만
패스스루 메서드가 나타나면 보통 모듈 간의 책임 분배가 잘못된 거야. 두 모듈의 기능을 하나로 합치거나, 기능을 재분배하거나, 호출자가 직접 하위 모듈을 사용하게 해야 해. 각 레이어가 고유한 가치를 제공해야 한다는 게 핵심이지.
데코레이터(Decorator) 패턴도 같은 맥락에서 조심해야 해. 기존 객체를 감싸서 기능을 추가하는 패턴인데, 본질적으로 원래 객체와 거의 같은 인터페이스를 가지면서 약간의 기능만 추가하기 때문에 대부분의 메서드가 패스스루가 돼. 데코레이터를 만들기 전에 "이 기능을 원래 클래스에 직접 추가할 수 있는가?"를 먼저 고민해 보자.
여러 레이어를 거쳐서 전달되지만 중간 레이어에서는 사용하지 않는 **패스스루 변수(Pass-through Variable)**도 비슷한 문제야. A → B → C → D 호출 체인에서 A에서 만든 변수가 D에서만 사용되는데 B, C는 그냥 전달만 해야 하는 경우. 해결 방법 중 하나가 **컨텍스트 객체(Context Object)**야. 시스템 전반에 걸쳐 필요한 정보를 하나의 객체에 모아두고 전달하면 중간 레이어가 개별 변수를 일일이 전달할 필요가 없어지지. 다만 너무 많은 것을 담으면 God Object가 될 수 있으니 적절한 크기를 유지하는 게 중요해.
정리
2장 읽고 기억할 거 세 가지:
- 깊은 모듈이 좋은 모듈이야. 인터페이스는 단순하면서 내부에 강력한 기능을 숨기는 게 이상적이지. 작은 것보다 깊은 게 중요하고, 클래시티스를 경계해야 해
- 정보 은닉이 깊은 모듈을 만드는 핵심 기법이야. 정보 누출은 가장 중요한 설계 red flag고, 모듈을 실행 순서가 아니라 정보(지식) 기준으로 나눠야 해
- 인터페이스는 범용적으로, 구현은 현재 필요한 것만. 그리고 각 레이어가 고유한 가치를 제공하는지 확인하자 — 패스스루 메서드는 설계가 잘못되었다는 신호야