테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

모듈성과 결합

시스템을 어디서 나누느냐가 바꾸기 쉬운 정도를 정한다. 응집과 결합, 커넥서스, 아키텍처 퀀텀으로 나눔을 잰다.

목차 (6)
  1. 응집과 결합
  2. 결합을 숫자로 재기
  3. 커넥서스: 결합의 종류와 세기
  4. 정보 은닉: 바뀔 결정을 숨긴다
  5. 아키텍처 퀀텀: 함께 서고 함께 쓰러지는 단위
  6. 컴포넌트 찾기: 기술 분할과 도메인 분할

응집과 결합

모듈은 서로 관련된 코드를 묶은 단위다. 패키지, 네임스페이스, 라이브러리, 서비스가 모두 모듈이 될 수 있다. 나눔이 좋은지 보는 가장 오래된 두 잣대가 응집과 결합이다. 응집(cohesion)은 한 모듈 안의 것들이 얼마나 서로 관련 있는가, 결합(coupling)은 모듈들이 서로에게 얼마나 기대는가다. 두 말은 1970년대 구조적 설계 운동에서 나왔고, 목표는 지금도 같다. 모듈 안은 단단히 묶고 모듈 사이는 느슨하게.

같은 여섯 요소를 두 모듈로 나누는 두 방법. 주문 요소와 회원 요소를 섞어 나누면 모듈 경계를 넘는 선이 네 개라 응집이 낮고 결합이 높다. 같은 업무끼리 모으면 경계를 넘는 선이 하나뿐이다나쁜 나눔모듈 A모듈 B응집 낮음 · 경계를 넘는 선 4개좋은 나눔모듈 A모듈 B응집 높음 · 경계를 넘는 선 1개주문 관련회원 관련경계를 넘는 의존
같은 여섯 요소, 두 가지 나눔. 관련된 것끼리 모으면 선이 모듈 안으로 들어가고, 모듈 하나를 고칠 때 건드릴 바깥이 줄어든다.

응집이 낮은 모듈은 "공용", "유틸", "매니저" 같은 이름을 달기 쉽다. 이름으로 한 가지 일을 말할 수 없으니 이것저것 들어간다. 시험 삼아 모듈을 둘로 쪼갰을 때 두 조각이 서로를 끊임없이 불러야 한다면 원래 응집이 높았던 것이고, 둘 다 혼자서 잘 돈다면 처음부터 따로였어야 했다.

결합은 없앨 수 없다. 모듈들이 협력해야 시스템이 일하기 때문이다. 파울러가 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이면 가장 먼 모서리다.
다섯 모듈의 의존 그래프와 주계열 도표. 웹은 주문과 결제 구현에, 주문과 결제 구현은 결제 계약과 공용에 기댄다. 불안정성과 추상성 평면에서 웹, 주문, 결제 계약, 결제 구현은 주계열 선 근처에 있고, 많은 모듈이 기대는데 구체적인 공용은 고통의 영역에 있다의존 그래프 (화살표 = 기댄다)웹주문결제 계약결제 구현공용추상성 A ↑주계열 A+I=1고통의 영역쓸모없음의 영역01불안정성 I →웹주문결제 계약결제 구현공용모듈CaCeIAD판정웹021.000.000.00선 근처주문120.670.200.13선 근처결제 계약200.000.800.20선 근처결제 구현120.670.000.33선 근처공용200.000.001.00고통의 영역I = Ce ÷ (Ca + Ce), A = 추상 타입 ÷ 전체 타입, D = |A + I − 1|
작은 쇼핑몰 코드의 다섯 모듈을 잰 예. 결제 계약은 많이 기대받지만 인터페이스가 대부분이라 괜찮고, 공용은 많이 기대받으면서 구체 클래스뿐이라 고통의 영역에 있다. 값은 직접 계산했다.

예에서 공용 모듈은 두 모듈이 기대므로 I = 0(안정)인데 추상 타입이 하나도 없어 A = 0이다. 거리 D = 1로, 이 구역을 고통의 영역이라 한다. 많은 곳이 기대고 있어 고치기 무섭고, 추상화가 없어 확장으로 피해 갈 수도 없다. 데이터베이스 스키마나 날짜 유틸처럼 거의 바뀌지 않는 것이라면 견딜 만하지만, 자주 바뀌는 업무 코드가 여기 있으면 고칠 때마다 줄줄이 깨진다. 반대편 모서리인 쓸모없음의 영역은 아무도 쓰지 않는 추상 타입만 가득한 모듈이다.

결제 쪽은 의존 역전 원칙을 보여 준다. 주문은 결제 구현이 아니라 결제 계약(인터페이스 묶음)에 기대고, 구현을 고르는 일은 맨 바깥의 웹 모듈이 한다. 그래서 결제 대행사를 바꿔도 결제 구현만 바뀐다. 이 수들은 절대 기준이 아니라 주의할 곳을 찾는 손전등이다.

커넥서스: 결합의 종류와 세기

결합이 "있다, 없다"만으로는 부족하다. 메일러 페이지존스가 1990년대에 제안한 커넥서스(connascence, "함께 태어남")는 결합의 종류와 세기를 이름 붙인다. 두 코드 조각이 커넥서스로 엮여 있다는 것은, 한쪽을 바꾸면 시스템이 계속 맞게 돌도록 다른 쪽도 바꿔야 한다는 뜻이다. connascence.io가 종류를 예제와 함께 정리해 두었다. 이후 포드와 리처즈의 아키텍처 책들도 이 용어로 서비스 사이 결합을 설명한다.

커넥서스의 종류. 정적 커넥서스는 이름, 타입, 의미, 위치, 알고리즘이고, 동적 커넥서스는 실행 순서, 타이밍, 값, 정체다. 위에서 아래로, 정적에서 동적으로 갈수록 강하다. 강도, 지역성, 정도의 세 성질로 판단한다정적코드만 읽어도 보인다이름 (name)타입 (type)의미 (meaning)위치 (position)알고리즘 (algorithm)동적실행해야 드러난다실행 순서 (execution)타이밍 (timing)값 (values)정체 (identity)약함강함각 열도 위에서 아래로 갈수록 강하다세 성질강도알아채고 고치기어려운 정도지역성엮인 코드끼리얼마나 가까운가정도엮인 곳이몇 군데인가강할수록 가까이 두고멀수록 약한 것으로 바꾼다
커넥서스의 아홉 종류. 정적인 것은 컴파일러나 정적 분석이 잡아 주지만, 동적인 것은 실행해 봐야 드러나서 더 위험하다.
  • 정적: 이름(같은 함수 이름을 불러야 한다), 타입(같은 타입을 주고받아야 한다), 의미(값 1이 "관리자"라는 뜻에 양쪽이 합의), 위치(인자 순서에 합의), 알고리즘(양쪽이 같은 해시나 같은 검증식을 써야 한다).
  • 동적: 실행 순서(초기화한 다음에 써야 한다), 타이밍(시간 안에 일어나야 한다), 값(여러 값이 함께 맞아야 한다, 예: 사본 여러 개가 같은 값), 정체(같은 객체 하나를 함께 가리켜야 한다).

판단에는 세 성질을 쓴다. 강도는 알아채고 고치기 어려운 정도로, 위 목록 뒤쪽으로 갈수록 강하다. 지역성은 엮인 코드가 얼마나 가까운가다. 한 함수 안의 위치 커넥서스는 괜찮지만 서비스 두 개에 걸친 위치 커넥서스(필드 순서로 해석하는 메시지)는 위험하다. 정도는 엮인 곳의 수다. 그래서 규칙이 나온다. 강한 커넥서스는 한 모듈 안에 가둔다. 모듈 경계를 넘는 커넥서스는 약한 종류로 바꾼다. 예컨대 매직 넘버(의미)는 이름 붙은 상수(이름)로, 인자 다섯 개의 순서(위치)는 이름 있는 필드를 가진 객체(이름)로 바꾼다.

정보 은닉: 바뀔 결정을 숨긴다

무엇을 기준으로 모듈을 나눌까. 데이비드 파르나스는 1972년 논문 On the Criteria To Be Used in Decomposing Systems into Modules에서 같은 프로그램(KWIC 색인)을 두 가지로 나눠 비교했다. 하나는 처리 흐름도의 단계마다 모듈을 두는 흔한 방식, 다른 하나는 바뀔 만한 설계 결정 하나하나를 숨기는 방식이다.

파르나스의 두 가지 모듈 나눔. 처리 단계마다 나누면 입력, 순환 이동, 정렬, 출력 모듈이 모두 데이터 배치를 직접 알아서 배치를 바꾸면 네 곳을 고친다. 숨길 결정마다 나누면 줄 저장소 모듈만 배치를 알고 나머지는 그 인터페이스만 쓰므로 한 곳만 고친다나눔 1: 처리 단계마다배치를 바꾸면 4곳 수정입력순환 이동정렬출력메모리 속 데이터 배치 (모든 모듈이 직접 읽고 쓴다)나눔 2: 숨길 결정마다배치를 바꾸면 1곳 수정입력순환 이동정렬출력줄 저장소배치는 숨기고 함수만 내보낸다D. L. Parnas (1972)의 KWIC 예를 줄여 다시 그렸다
파르나스 논문의 비교를 줄여 다시 그린 것. 위쪽은 모든 단계가 데이터 배치를 직접 다루고, 아래쪽은 줄 저장소만 배치를 알고 나머지는 함수로 묻는다.

처음 보기에는 두 나눔 모두 잘 돈다. 차이는 바꿀 때 난다. 데이터를 메모리에 어떻게 담을지, 순환 이동을 미리 다 만들지 필요할 때 계산할지 같은 결정을 바꾸면, 단계별 나눔은 모든 모듈을 고쳐야 하지만 결정마다 나눈 쪽은 그 결정을 숨긴 모듈 하나만 고친다. 이것이 정보 은닉(information hiding)이다. 모듈의 인터페이스는 잘 안 바뀔 것만 드러내고, 바뀔 것은 안에 숨긴다.

정보 은닉은 위의 다른 잣대들과 이어진다. 숨긴 결정은 바깥과 커넥서스를 만들지 않고, 바뀌는 이유가 같은 것끼리 모이니 응집도 높아진다. 서비스 수준에서 보면 "다른 서비스의 데이터베이스를 직접 읽지 않는다"는 규칙이 바로 정보 은닉이다. 테이블 구조라는 결정을 서비스 안에 숨기는 것이다(분해와 분산 데이터 노트).

아키텍처 퀀텀: 함께 서고 함께 쓰러지는 단위

서비스를 열 개로 나눴다고 독립된 단위가 열 개인 것은 아니다. 주문 서비스가 결제 서비스를 동기로 부르고 응답을 기다린다면, 결제가 느려지면 주문도 느려지고 결제가 죽으면 주문도 실패한다. 두 서비스는 운영 특성(가용성, 확장성, 성능)을 사실상 공유한다. 포드·파슨스·쿠아의 Building Evolutionary Architectures가 붙인 이름이 아키텍처 퀀텀(architecture quantum)이고, 이후 리처즈·포드의 책들이 다듬었다. 이 노트식으로 줄이면, 퀀텀은 다음을 모두 갖춘 가장 작은 단위다.

  • 따로 배포할 수 있다: 돌아가는 데 필요한 것(데이터베이스 포함)을 모두 품고, 다른 부분과 상관없이 배포된다. 여러 서비스가 데이터베이스 하나를 함께 쓰면 그 데이터베이스까지 한 퀀텀이다.
  • 기능 응집이 높다: 한 가지 업무 흐름을 처음부터 끝까지 맡는다.
  • 동기 결합으로 묶인다: 실행 중에 동기로 부르고 기다리는 관계는 퀀텀 경계를 넘지 못한다. 그런 관계로 이어진 서비스는 한 퀀텀으로 센다.
아키텍처 퀀텀. 주문이 결제를 동기로 부르므로 주문과 결제, 두 데이터베이스가 퀀텀 하나를 이룬다. 결제 완료 이벤트는 메시지 큐를 거쳐 배송과 알림에 비동기로 가므로 배송과 알림은 각각 따로 선 퀀텀이다퀀텀 1주문주문 DB결제결제 DB큐퀀텀 2배송배송 DB퀀텀 3알림알림 DB동기 호출: 상대가 느리거나 죽으면 같이 멈춘다 → 한 퀀텀비동기 이벤트: 큐가 시간 차를 받아 준다 → 따로 선다
퀀텀 세기. 주문과 결제는 동기 호출로 묶여 하나다. 큐 너머의 배송과 알림은 결제가 잠깐 멈춰도 쌓인 이벤트를 나중에 처리하면 되므로 따로 선다.

퀀텀이 쓸모 있는 까닭은 아키텍처 특성의 범위를 정해 주기 때문이다. 퀀텀마다 다른 특성을 가질 수 있다. 위 예에서 알림은 몇 분 늦어도 되니 작은 서버 하나로 충분하고, 주문·결제 퀀텀은 명절 트래픽에 맞춰 함께 늘려야 한다. 반대로 퀀텀이 하나뿐이면 시스템 전체가 같은 특성을 가져야 한다. 서비스를 잘게 나눴는데 모두 동기로 서로를 부르는 시스템은 배포 단위만 여럿이고 실제로는 퀀텀 하나, 이른바 분산 모놀리스다.

컴포넌트 찾기: 기술 분할과 도메인 분할

아키텍처에서 말하는 컴포넌트는 패키지나 네임스페이스로 묶인 큰 모듈이다. 맨 위 수준에서 컴포넌트를 나누는 방법은 크게 둘이다. 기술 분할은 화면, 업무 규칙, 데이터 접근처럼 기술적 역할로 나눈다. 도메인 분할은 주문, 회원, 배송처럼 업무 영역으로 나눈다.

기술 분할과 도메인 분할. 기술 분할은 화면, 업무 규칙, 데이터 접근 계층으로 나누어 주문 기능이 세 계층에 흩어진다. 도메인 분할은 주문, 회원, 배송으로 나누어 각 모듈 안에 화면, 규칙, 데이터가 함께 있다기술 분할화면 계층주문회원배송업무 규칙 계층주문회원배송데이터 접근 계층주문회원배송할인 규칙 하나에 세 계층을 고친다도메인 분할주문화면규칙데이터회원화면규칙데이터배송화면규칙데이터한 모듈 안에서 끝난다
같은 쇼핑몰을 두 방식으로 나눈 모습. 업무 변경은 대개 도메인 단위로 오므로, 기술 분할에서는 작은 변경도 여러 계층을 가로지른다.

기술 분할은 익숙하고, 화면 전문가와 데이터베이스 전문가가 각자 계층을 맡기 좋다. 하지만 "주문에 쿠폰 할인 추가" 같은 변경은 세 계층을 모두 건드린다. 도메인 분할은 변경이 한 컴포넌트 안에서 끝나고, 나중에 그 컴포넌트를 서비스로 떼어 내기도 쉽다. 대신 공통 코드가 도메인마다 중복되기 쉽다. 파울러의 PresentationDomainDataLayering도 화면·도메인·데이터 계층은 좋은 나눔이지만, 큰 애플리케이션의 맨 위 수준은 도메인으로 나누고 계층은 그 안에 두라고 권한다.

컴포넌트를 찾는 과정은 한 번에 끝나지 않는다. 요구사항에서 행위자와 그들이 하는 일을 뽑아 첫 후보를 만들고, 그 후보에 역할을 배정해 보고, 고른 아키텍처 특성(예: 이 부분만 따로 늘려야 하는가)에 비춰 다시 쪼개거나 합친다. 팀 구조도 영향을 준다. 계층별 팀은 계층형 구조를, 기능별 팀은 도메인 구조를 낳는 경향이 있는데, 이 이야기는 진화적 아키텍처 노트의 콘웨이 법칙 절에 있다.