13.3 모듈 소프트웨어 설계 — 변경할 결정을 경계 안에 숨긴다
모듈이 많다고 모듈식 설계가 되는 것은 아니다. 디렉터리와 서비스가 나뉘어도 하나의 정책 변경이 모든 경계를 관통하면 실질적으로 한 덩어리다. 이 문서는 모듈을 독립 배포 단위나 클래스가 아니라 변경 가능한 설계 결정을 숨기고, 외부에는 안정된 계약만 드러내는 경계로 설명한다.
학습 목표
- 처리 순서 분해와 정보 은닉 기반 분해가 변경 전파에 미치는 차이를 설명한다.
- 예상 변경·불변식·데이터 소유권을 근거로 모듈 경계를 제안한다.
- 응집도·결합도와 의존 방향을 정성적 증거로 평가한다.
- 설계 원칙을 절대 규칙이 아니라 경계 문제를 탐색하는 휴리스틱으로 사용한다.
배경: 한 번 읽기 쉬운 구조와 여러 번 바꾸기 쉬운 구조
주문 취소 프로그램을 처리 순서대로 나누면 자연스럽다.
입력 읽기 → 정책 검사 → 환불 계산 → 결제 호출 → 상태 저장 → 알림각 단계를 함수나 서비스로 만들면 흐름은 읽기 쉽다. 그러나 새 결제사를 추가할 때 정책 검사, 환불 계산, 결제 호출과 상태 저장이 모두 결제사별 오류 의미를 알아야 한다면 변경은 여러 모듈에 퍼진다. 반대로 처리 순서가 다른 책임이라도 같은 비즈니스 규칙 때문에 함께 바뀐다면 떨어뜨린 경계가 조정을 늘린다.
D. L. Parnas는 1972년 논문에서 처리 단계에 따른 분해와 변경 가능한 설계 결정을 숨기는 분해를 비교했다. 핵심은 모듈이 수행 단계 하나를 대표하는 것이 아니라 다른 모듈이 알 필요 없는 지식을 감추는 것이다. 정보 은닉(information hiding)은 데이터를 private으로 선언하는 문법 기능보다 넓다. 알고리즘, 데이터 표현, 외부 시스템의 특성, 정책과 실패 처리 같은 변동 가능한 결정을 경계 뒤에 둔다.
핵심 개념
모듈의 세 요소: 책임, 비밀, 계약
모듈을 설계할 때 다음 세 문장에 답한다.
- 책임: 이 모듈은 어떤 결과와 불변식을 소유하는가?
- 비밀: 어떤 변경 가능한 지식이 외부로 새지 않아야 하는가?
- 계약: 소비자는 내부를 몰라도 무엇을 가정할 수 있는가?
예를 들어 PaymentGateway 모듈을 다음처럼 정의할 수 있다.
책임: 결제 승인 취소 의도를 외부 결제사 계약으로 변환하고 결과를 정규화한다.
비밀: 결제사별 API, 인증, 오류 코드, retry-after와 멱등 키 형식.
계약: 같은 취소 ID의 반복 호출은 중복 환불을 만들지 않으며,
ACCEPTED / REJECTED / UNKNOWN 중 하나를 반환한다.외부에 httpStatus, vendorErrorCode, 결제사 SDK 타입을 그대로 노출하면 비밀이 계약으로 새어 나온다. 호출자마다 벤더 분기문을 쓰고 결제사 교체가 전체 시스템 변경이 된다. 반대로 모든 오류를 단순 false로 숨기면 호출자가 거절과 미확정을 구분하지 못해 주문 불변식을 지킬 수 없다. 정보 은닉은 정보를 최대한 감추는 것이 아니라 소비자가 올바른 결정을 내리는 데 필요한 최소 의미만 안정적으로 공개하는 것이다.
예상 변경으로 경계를 시험한다
좋은 분해는 미래를 맞히는 예언이 아니다. 현재 증거로 예상되는 변경을 서로 다른 경계 안에 가두는 가설이다. 다음 변경 카드를 설계에 적용한다.
- 결제사 B를 추가한다.
- 국가마다 취소 가능 시간이 달라진다.
- 부분 취소와 쿠폰 환급 규칙이 추가된다.
- 고객 알림 채널이 이메일에서 앱 푸시로 바뀐다.
- 감사 규정이 취소 사유의 원본 보존을 요구한다.
각 변경에서 수정해야 할 모듈과 계약 수, 동시 배포가 필요한 소비자, 데이터 마이그레이션과 재검증 범위를 기록한다. 변경 하나가 여러 모듈을 건드린다는 사실만으로 나쁘지는 않다. 본질적으로 여러 책임을 바꾸는 요구도 있다. 문제는 한 결정의 세부가 불필요하게 여러 곳에 복제되는가이다.
응집도 — 함께 있는 이유가 하나인가
응집도(cohesion)는 모듈 내부 요소들이 하나의 책임·지식·변경 이유를 중심으로 얼마나 강하게 관련되는지를 나타낸다. 높은 응집도는 “함수들이 모두 주문을 다룬다”처럼 넓은 명사가 아니라 같은 불변식과 변경 이유를 공유한다는 뜻이다.
OrderService가 주문 생성, 가격 계산, 결제 SDK, 이메일 템플릿, CSV 보고서를 모두 소유한다면 이름은 하나지만 변경 이유는 다섯 개다. 반대로 취소 가능성 판단에 필요한 주문 상태, 출고 상태와 정책 규칙은 서로 다른 데이터처럼 보여도 하나의 결정에서 함께 바뀔 수 있다.
응집도를 물을 때 다음 신호를 본다.
- 모듈 설명에 “그리고”가 반복되는가?
- 서로 다른 이해관계자 요청 때문에 같은 모듈을 자주 수정하는가?
- 일부 소비자는 인터페이스의 극히 일부만 사용하고 나머지를 피하는가?
- 모듈 내부 데이터가 하나의 불변식 아래 함께 변하는가?
결합도 — 무엇을 얼마나 구체적으로 아는가
결합도(coupling)는 단순한 호출 횟수가 아니다. 한 모듈이 다른 모듈의 결정에 얼마나 의존하는지다. 결합의 위험은 여러 차원에 있다.
| 결합 차원 | 강한 결합의 신호 | 완화 방향 |
|---|---|---|
| 의미 | 호출자가 내부 상태·오류 코드를 해석 | 도메인 의미를 가진 계약 |
| 시간 | 상대가 동시에 살아 있고 즉시 응답해야 함 | 비동기화·timeout·상태 명시 |
| 데이터 | 공유 스키마의 내부 열을 여러 모듈이 직접 수정 | 소유권과 변환 경계 |
| 순서 | 숨은 호출 순서를 지켜야 정상 동작 | 단일 연산 계약·상태 머신 |
| 배포 | 한쪽 변경마다 모든 소비자를 동시에 배포 | 호환 가능한 계약 진화 |
| 운영 | 실패·부하가 경계를 넘어 같이 전파 | 격리·예산·역압력 |
비동기 메시지로 바꾸면 시간 결합은 줄지만 스키마와 의미 결합, 중복·순서 처리 책임이 생긴다. 네트워크 호출 수가 적다고 결합도가 낮은 것도 아니다. 하나의 공유 데이터베이스 테이블을 여러 서비스가 직접 수정하면 호출 없이도 강하게 결합된다.
계약은 정상 결과보다 경계 조건을 포함한다
모듈 계약은 함수 시그니처만이 아니다.
- 입력의 전제 조건과 출력의 사후 조건
- 모듈이 항상 지킬 불변식
- 오류 분류와 재시도 가능성
- 멱등성, 순서와 동시성 의미
- 시간·자원 한도
- 버전과 호환성 규칙
취소 모듈의 핵심 불변식이 “성공한 환불 총액은 결제된 순액을 넘지 않는다”라면 이 불변식을 여러 호출자에게 분산하지 않는다. 모듈이 상태 전이와 금액 변경을 원자적 계약으로 소유해야 경쟁 요청에서도 지킬 수 있다.
// 계약을 설명하기 위한 TypeScript 형태의 예시이며 완전한 구현은 아니다.
type CancellationResult =
| { status: 'accepted'; cancellationId: string }
| { status: 'rejected'; reason: 'already_shipped' | 'not_refundable' }
| { status: 'pending'; cancellationId: string };
interface CancellationService {
request(command: {
orderId: string;
actorId: string;
idempotencyKey: string;
lines: ReadonlyArray<{ lineId: string; quantity: number }>;
}): Promise<CancellationResult>;
}이 계약도 완벽하지 않다. pending의 만료와 조회 방법, 권한 실패, 동시 변경 의미가 필요할 수 있다. 중요한 것은 결제사 SDK와 데이터베이스 행이 아니라 소비자가 취소 업무를 올바르게 진행할 의미를 공개한다는 점이다.
데이터 소유권과 불변식은 경계를 고정한다
“각 서비스가 자기 데이터베이스를 가져야 한다”는 문구보다 먼저 어느 경계가 어떤 불변식을 판정할 권한을 갖는지 정한다. 취소 가능 수량과 환불 누계의 원천을 주문 모듈과 결제 모듈이 각각 보유하면 어느 쪽도 전체 불변식을 원자적으로 판정하지 못할 수 있다.
데이터를 복제할 수는 있지만 복제본의 용도를 명시한다.
- 명령을 거절하거나 승인하는 권위 있는 원천인가?
- 조회를 빠르게 하기 위한 지연 허용 사본인가?
- 장애 복구를 위한 이벤트 기록인가?
- 캐시가 오래됐을 때 허용할 행동은 무엇인가?
소유권이 불명확하면 팀은 직접 테이블 접근, 공유 DTO와 임의 이벤트를 통해 빈틈을 메운다. 이것이 장기적으로 가장 강한 결합이 된다.
의존 방향 — 변동성이 안정된 정책을 침범하지 않게 한다
가격·취소 정책은 특정 웹 프레임워크나 결제사 SDK보다 업무 의미에 가깝다. 핵심 정책이 외부 도구의 구체 타입을 직접 알면 도구 변경과 정책 검증이 함께 묶인다. 의존성 역전(dependency inversion)은 모든 것에 인터페이스를 만들라는 규칙이 아니라 중요하고 안정돼야 할 정책이 변동성 큰 세부에 종속되지 않게 의존 방향을 조정하는 방법이다.
HTTP handler ──▶ Cancellation use case ◀── Payment adapter
│
▼
Cancellation policy화살표는 소스 코드 의존을 뜻한다. 실행 중에는 use case가 adapter를 호출하지만, 계약은 정책 쪽에서 정의하고 adapter가 구현한다. 프레임워크와 결제사 교체가 핵심 정책의 타입과 테스트를 바꾸지 않게 한다.
추상화에도 비용이 있다. 대안이 하나이고 변경 가능성이 낮은 단순 코드까지 인터페이스·팩토리로 감싸면 탐색 비용과 간접성만 늘어난다. 외부 시스템, 시간·랜덤성, 중요한 정책 경계처럼 변동성과 검증 가치가 분명한 곳에 우선한다.
순환 의존은 경계가 숨기지 못한 지식의 신호다
Order가 Payment를 알고 Payment가 다시 OrderStatus를 바꾸며 Notification이 둘을 호출하는 순환은 독립 이해·테스트·배포를 어렵게 한다. 무조건 이벤트를 넣기 전에 순환의 이유를 분류한다.
- 하나의 불변식을 공동 소유한다면 잘못 나뉜 모듈을 합칠 수 있다.
- 한쪽이 조회 정보만 필요하면 읽기 모델이나 좁은 계약을 제공할 수 있다.
- 완료 사실을 여러 후속 작업이 관찰한다면 이벤트가 적합할 수 있다.
- 상위 정책이 두 하위 기능을 조정한다면 orchestration 책임을 별도 경계에 둘 수 있다.
순환을 dependency injection으로 감추거나 메시지 브로커로 바꿔도 의미 순환은 남을 수 있다. 누가 최종 결정을 소유하는지 먼저 정한다.
두 분해안 비교
안 A: 처리 단계별 분해
CancellationValidator
RefundCalculator
PaymentCaller
OrderUpdater
NotificationSender각 처리 단계는 작고 흐름이 명확하다. 고정된 파이프라인과 단계별 재사용이 중요하면 합리적이다. 그러나 국가별 정책과 부분 취소가 추가될 때 validator, calculator, updater가 같은 규칙을 각자 알아야 하고 결제사 결과 의미가 caller와 updater 양쪽에 샐 수 있다.
안 B: 숨길 결정별 분해
CancellationPolicy — 취소 가능성·수량·금액 규칙
Cancellation — 요청 상태와 환불 누계 불변식
PaymentGateway — 결제사별 계약과 결과 정규화
FulfillmentGateway — 출고 중지 계약
CustomerNotification — 채널·템플릿·전송 실패안 B는 처리 흐름을 한눈에 읽으려면 상위 use case를 따라가야 하므로 간접성이 늘어난다. 대신 결제사 추가는 PaymentGateway, 정책 변경은 CancellationPolicy, 채널 변경은 CustomerNotification 안에 머물 가능성이 크다.
| 변경 시나리오 | 안 A의 영향 | 안 B의 영향 |
|---|---|---|
| 결제사 B 추가 | caller와 updater의 오류 해석 변경 | gateway 구현과 계약 검증 |
| 부분 취소 | validator·calculator·updater 동시 변경 | policy와 cancellation 불변식 변경 |
| 푸시 알림 추가 | notification sender 내부 | notification 내부 |
| 파이프라인 단계별 독립 재실행 | 단계 구조가 직접 지원 | orchestration 계약 추가 필요 |
안 B가 항상 우월하지 않다. 실제 변화가 처리 단계의 독립 실행·스케줄링에 집중된다면 안 A의 경계가 더 안정적일 수 있다. 설계는 예상 변경과 품질 목표에 대한 가설이며 운영 변화로 다시 평가한다.
설계 원칙을 사용하는 법
SOLID 같은 원칙은 모듈 경계를 자동 생성하지 않는다. 이 문서의 모델로 다시 읽으면 질문 목록이 된다.
- 단일 책임: 이 단위가 숨기는 변경 이유가 하나인가?
- 개방–폐쇄: 예상 변형을 소비자 수정 없이 경계 안에서 수용해야 하는가?
- 리스코프 치환: 구현들이 소비자가 믿는 의미·오류·불변식을 보존하는가?
- 인터페이스 분리: 소비자가 필요 없는 계약과 변경에 종속되는가?
- 의존성 역전: 중요한 정책이 변동성 큰 세부 타입에 의존하는가?
모든 클래스를 하나의 이유로만 바꾸거나 모든 미래 확장을 열어 두는 것은 불가능하다. 원칙 사이도 충돌한다. 작은 인터페이스를 지나치게 쪼개면 호출 조정과 탐색 비용이 늘고, 확장을 위해 추상화하면 현재의 이해 가능성을 잃는다. 어떤 변경과 소비자를 보호하는지 설명할 수 없으면 원칙 이름은 근거가 아니다.
실무 관점
모듈과 배포 단위를 혼동하지 않는다
프로세스 하나 안에서도 강한 모듈 경계를 만들 수 있고, 여러 서비스로 나눠도 공유 데이터베이스·동시 배포·공통 SDK로 강하게 결합될 수 있다. 네트워크 경계는 지연·부분 실패·운영 비용을 추가한다. 먼저 논리적 소유권과 계약을 세운 뒤 독립 배포의 가치가 비용보다 큰지 판단한다.
DRY보다 지식의 중복을 본다
모양이 같은 코드 두 줄은 서로 다른 이유로 변할 수 있다. 성급히 공통 모듈로 합치면 무관한 소비자를 묶는다. 반대로 같은 정책 규칙이 여러 곳에 다른 문법으로 복제되면 코드 모양이 달라도 위험한 지식 중복이다.
지표는 신호이지 판결이 아니다
fan-in, fan-out, 순환 수와 변경 동시성은 조사 대상을 찾는 데 도움을 준다. 그러나 높은 fan-in은 안정된 핵심 계약의 자연스러운 결과일 수 있고, 자주 함께 바뀌는 파일은 잘못된 결합이 아니라 하나의 응집된 기능일 수 있다. 변경 이유와 불변식을 읽지 않고 임계값만 적용하지 않는다.
정리
- 모듈은 처리 단계나 파일 묶음이 아니라 변경 가능한 설계 결정을 숨기는 경계다.
- 책임·비밀·계약을 명시하고 예상 변경으로 변경 전파 범위를 시험한다.
- 응집도는 함께 있는 이유, 결합도는 다른 경계의 구체적 결정을 얼마나 아는지에 관한 판단이다.
- 데이터 소유권과 불변식은 모듈의 권한을 정하고, 의존 방향은 안정돼야 할 정책을 변동성에서 보호한다.
- 설계 원칙과 지표는 맥락 없는 정답이 아니라 경계의 문제를 찾는 휴리스틱이다.
확인 문제
- 두 서비스가 서로 API를 호출하지 않지만 같은 주문 테이블을 직접 수정한다. 결합도를 어떻게 평가해야 하는가?
- 결제사 SDK를
PaymentService뒤에 감쌌지만 반환 타입으로 SDK의 오류 객체를 그대로 노출한다. 무엇이 숨겨지지 않았는가? - 국가별 취소 정책 변경마다 세 모듈이 함께 수정된다. 모듈을 합치는 것과 계약을 개선하는 것 중 무엇을 선택할지 어떤 증거를 볼 것인가?
정답과 해설
- 호출 결합은 없지만 데이터 표현, 불변식, 배포와 변경에 강하게 결합됐다. 어느 서비스가 상태 전이 권한을 소유하는지 정하고 다른 서비스는 계약이나 읽기 사본을 사용하게 해야 한다.
- 결제사별 오류 분류와 타입이라는 변동 가능한 결정이 새고 있다. 소비자가 필요한 재시도 가능성, 거절, 미확정 같은 도메인 의미로 정규화해야 한다.
- 같은 불변식과 같은 이해관계자 결정 때문에 항상 함께 바뀐다면 하나의 응집된 경계일 수 있다. 서로 다른 이유로 바뀌지만 공유된 결제사 코드나 DTO 때문에 동시 변경된다면 비밀 누출을 줄이는 계약이 적합하다. 변경 기록과 실패 사례로 구분한다.
참고 자료
- D. L. Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules”: 처리 단계가 아니라 변경 가능한 설계 결정을 숨기는 모듈 분해를 비교한 원 논문이다.
- D. L. Parnas, “Designing Software for Ease of Extension and Contraction”: 확장·축소 가능한 소프트웨어의 사용 관계와 인터페이스 설계를 논한다.
- D. L. Parnas and P. C. Clements, “A Rational Design Process: How and Why to Fake It”: 실제 설계의 반복성을 인정하면서도 검토 가능한 합리적 기록을 만드는 접근을 설명한다.