모듈성과 결합
시스템을 어디서 나누느냐가 바꾸기 쉬운 정도를 정한다. 응집과 결합, 커넥서스, 아키텍처 퀀텀으로 나눔을 잰다.
목차 (6)
응집과 결합
모듈은 서로 관련된 코드를 묶은 단위다. 패키지, 네임스페이스, 라이브러리, 서비스가 모두 모듈이 될 수 있다. 나눔이 좋은지 보는 가장 오래된 두 잣대가 응집과 결합이다. 응집(cohesion)은 한 모듈 안의 것들이 얼마나 서로 관련 있는가, 결합(coupling)은 모듈들이 서로에게 얼마나 기대는가다. 두 말은 1970년대 구조적 설계 운동에서 나왔고, 목표는 지금도 같다. 모듈 안은 단단히 묶고 모듈 사이는 느슨하게.
응집이 낮은 모듈은 "공용", "유틸", "매니저" 같은 이름을 달기 쉽다. 이름으로 한 가지 일을 말할 수 없으니 이것저것 들어간다. 시험 삼아 모듈을 둘로 쪼갰을 때 두 조각이 서로를 끊임없이 불러야 한다면 원래 응집이 높았던 것이고, 둘 다 혼자서 잘 돈다면 처음부터 따로였어야 했다.
결합은 없앨 수 없다. 모듈들이 협력해야 시스템이 일하기 때문이다. 파울러가 IEEE Software에 쓴 Reducing Coupling의 요지처럼, 목표는 결합을 없애는 것이 아니라 어디에 둘지 고르는 것이다. 자주 함께 바뀌는 것은 가까이 두고, 바뀌는 이유가 다른 것 사이에는 좁고 안정된 인터페이스를 둔다.
결합을 숫자로 재기
로버트 마틴은 1994년 글 OO Design Quality Metrics에서 모듈(패키지) 사이의 의존을 재는 몇 가지 수를 제안했다.
- 구심 결합(afferent coupling, Ca): 이 모듈에 기대는 바깥 모듈 수. 들어오는 화살표다.
- 원심 결합(efferent coupling, Ce): 이 모듈이 기대는 바깥 모듈 수. 나가는 화살표다.
- 불안정성 I = Ce ÷ (Ca + Ce). 0이면 남들이 기대기만 하는 안정된 모듈, 1이면 남에게 기대기만 하는 쉽게 바뀌는 모듈이다.
- 추상성 A = 추상 타입(인터페이스, 추상 클래스) 수 ÷ 전체 타입 수.
- 주계열과의 거리 D = |A + I − 1|. 안정된 모듈은 추상적이어야 하고, 불안정한 모듈은 구체적이어도 된다는 생각을 선 A + I = 1(주계열)로 그은 것이다. 0이면 선 위, 1이면 가장 먼 모서리다.
예에서 공용 모듈은 두 모듈이 기대므로 I = 0(안정)인데 추상 타입이 하나도 없어 A = 0이다. 거리 D = 1로, 이 구역을 고통의 영역이라 한다. 많은 곳이 기대고 있어 고치기 무섭고, 추상화가 없어 확장으로 피해 갈 수도 없다. 데이터베이스 스키마나 날짜 유틸처럼 거의 바뀌지 않는 것이라면 견딜 만하지만, 자주 바뀌는 업무 코드가 여기 있으면 고칠 때마다 줄줄이 깨진다. 반대편 모서리인 쓸모없음의 영역은 아무도 쓰지 않는 추상 타입만 가득한 모듈이다.
결제 쪽은 의존 역전 원칙을 보여 준다. 주문은 결제 구현이 아니라 결제 계약(인터페이스 묶음)에 기대고, 구현을 고르는 일은 맨 바깥의 웹 모듈이 한다. 그래서 결제 대행사를 바꿔도 결제 구현만 바뀐다. 이 수들은 절대 기준이 아니라 주의할 곳을 찾는 손전등이다.
커넥서스: 결합의 종류와 세기
결합이 "있다, 없다"만으로는 부족하다. 메일러 페이지존스가 1990년대에 제안한 커넥서스(connascence, "함께 태어남")는 결합의 종류와 세기를 이름 붙인다. 두 코드 조각이 커넥서스로 엮여 있다는 것은, 한쪽을 바꾸면 시스템이 계속 맞게 돌도록 다른 쪽도 바꿔야 한다는 뜻이다. connascence.io가 종류를 예제와 함께 정리해 두었다. 이후 포드와 리처즈의 아키텍처 책들도 이 용어로 서비스 사이 결합을 설명한다.
- 정적: 이름(같은 함수 이름을 불러야 한다), 타입(같은 타입을 주고받아야 한다), 의미(값 1이 "관리자"라는 뜻에 양쪽이 합의), 위치(인자 순서에 합의), 알고리즘(양쪽이 같은 해시나 같은 검증식을 써야 한다).
- 동적: 실행 순서(초기화한 다음에 써야 한다), 타이밍(시간 안에 일어나야 한다), 값(여러 값이 함께 맞아야 한다, 예: 사본 여러 개가 같은 값), 정체(같은 객체 하나를 함께 가리켜야 한다).
판단에는 세 성질을 쓴다. 강도는 알아채고 고치기 어려운 정도로, 위 목록 뒤쪽으로 갈수록 강하다. 지역성은 엮인 코드가 얼마나 가까운가다. 한 함수 안의 위치 커넥서스는 괜찮지만 서비스 두 개에 걸친 위치 커넥서스(필드 순서로 해석하는 메시지)는 위험하다. 정도는 엮인 곳의 수다. 그래서 규칙이 나온다. 강한 커넥서스는 한 모듈 안에 가둔다. 모듈 경계를 넘는 커넥서스는 약한 종류로 바꾼다. 예컨대 매직 넘버(의미)는 이름 붙은 상수(이름)로, 인자 다섯 개의 순서(위치)는 이름 있는 필드를 가진 객체(이름)로 바꾼다.
정보 은닉: 바뀔 결정을 숨긴다
무엇을 기준으로 모듈을 나눌까. 데이비드 파르나스는 1972년 논문 On the Criteria To Be Used in Decomposing Systems into Modules에서 같은 프로그램(KWIC 색인)을 두 가지로 나눠 비교했다. 하나는 처리 흐름도의 단계마다 모듈을 두는 흔한 방식, 다른 하나는 바뀔 만한 설계 결정 하나하나를 숨기는 방식이다.
처음 보기에는 두 나눔 모두 잘 돈다. 차이는 바꿀 때 난다. 데이터를 메모리에 어떻게 담을지, 순환 이동을 미리 다 만들지 필요할 때 계산할지 같은 결정을 바꾸면, 단계별 나눔은 모든 모듈을 고쳐야 하지만 결정마다 나눈 쪽은 그 결정을 숨긴 모듈 하나만 고친다. 이것이 정보 은닉(information hiding)이다. 모듈의 인터페이스는 잘 안 바뀔 것만 드러내고, 바뀔 것은 안에 숨긴다.
정보 은닉은 위의 다른 잣대들과 이어진다. 숨긴 결정은 바깥과 커넥서스를 만들지 않고, 바뀌는 이유가 같은 것끼리 모이니 응집도 높아진다. 서비스 수준에서 보면 "다른 서비스의 데이터베이스를 직접 읽지 않는다"는 규칙이 바로 정보 은닉이다. 테이블 구조라는 결정을 서비스 안에 숨기는 것이다(분해와 분산 데이터 노트).
아키텍처 퀀텀: 함께 서고 함께 쓰러지는 단위
서비스를 열 개로 나눴다고 독립된 단위가 열 개인 것은 아니다. 주문 서비스가 결제 서비스를 동기로 부르고 응답을 기다린다면, 결제가 느려지면 주문도 느려지고 결제가 죽으면 주문도 실패한다. 두 서비스는 운영 특성(가용성, 확장성, 성능)을 사실상 공유한다. 포드·파슨스·쿠아의 Building Evolutionary Architectures가 붙인 이름이 아키텍처 퀀텀(architecture quantum)이고, 이후 리처즈·포드의 책들이 다듬었다. 이 노트식으로 줄이면, 퀀텀은 다음을 모두 갖춘 가장 작은 단위다.
- 따로 배포할 수 있다: 돌아가는 데 필요한 것(데이터베이스 포함)을 모두 품고, 다른 부분과 상관없이 배포된다. 여러 서비스가 데이터베이스 하나를 함께 쓰면 그 데이터베이스까지 한 퀀텀이다.
- 기능 응집이 높다: 한 가지 업무 흐름을 처음부터 끝까지 맡는다.
- 동기 결합으로 묶인다: 실행 중에 동기로 부르고 기다리는 관계는 퀀텀 경계를 넘지 못한다. 그런 관계로 이어진 서비스는 한 퀀텀으로 센다.
퀀텀이 쓸모 있는 까닭은 아키텍처 특성의 범위를 정해 주기 때문이다. 퀀텀마다 다른 특성을 가질 수 있다. 위 예에서 알림은 몇 분 늦어도 되니 작은 서버 하나로 충분하고, 주문·결제 퀀텀은 명절 트래픽에 맞춰 함께 늘려야 한다. 반대로 퀀텀이 하나뿐이면 시스템 전체가 같은 특성을 가져야 한다. 서비스를 잘게 나눴는데 모두 동기로 서로를 부르는 시스템은 배포 단위만 여럿이고 실제로는 퀀텀 하나, 이른바 분산 모놀리스다.
컴포넌트 찾기: 기술 분할과 도메인 분할
아키텍처에서 말하는 컴포넌트는 패키지나 네임스페이스로 묶인 큰 모듈이다. 맨 위 수준에서 컴포넌트를 나누는 방법은 크게 둘이다. 기술 분할은 화면, 업무 규칙, 데이터 접근처럼 기술적 역할로 나눈다. 도메인 분할은 주문, 회원, 배송처럼 업무 영역으로 나눈다.
기술 분할은 익숙하고, 화면 전문가와 데이터베이스 전문가가 각자 계층을 맡기 좋다. 하지만 "주문에 쿠폰 할인 추가" 같은 변경은 세 계층을 모두 건드린다. 도메인 분할은 변경이 한 컴포넌트 안에서 끝나고, 나중에 그 컴포넌트를 서비스로 떼어 내기도 쉽다. 대신 공통 코드가 도메인마다 중복되기 쉽다. 파울러의 PresentationDomainDataLayering도 화면·도메인·데이터 계층은 좋은 나눔이지만, 큰 애플리케이션의 맨 위 수준은 도메인으로 나누고 계층은 그 안에 두라고 권한다.
컴포넌트를 찾는 과정은 한 번에 끝나지 않는다. 요구사항에서 행위자와 그들이 하는 일을 뽑아 첫 후보를 만들고, 그 후보에 역할을 배정해 보고, 고른 아키텍처 특성(예: 이 부분만 따로 늘려야 하는가)에 비춰 다시 쪼개거나 합친다. 팀 구조도 영향을 준다. 계층별 팀은 계층형 구조를, 기능별 팀은 도메인 구조를 낳는 경향이 있는데, 이 이야기는 진화적 아키텍처 노트의 콘웨이 법칙 절에 있다.