3장, 모듈성

2022.05.24

모듈성은 일종의 구성 원리(organizing principle)이다.
물리학에 비유하자면, 소프트웨어 시스템은 엔트로피(무질서)가 증가하는 방향으로 움직이는 복잡한 시스템을 모델링한다. 아키텍트는 끊임 없이 에너지를 소비해서 시스템을 구조적으로 탄탄하게 유지해야 한다.

모듈성을 잘 유지하는 건 우리가 암묵적(implicit) 아키텍처 특성이라고 정의한 것의 좋은 예가 된다.

3.1 정의

사전에서 모듈을 찾아보면, '복잡한 구조를 만드는 데 쓰이는 각각의 표준화한 부품이나 독립적인 단위'라고 나온다. 우리는 모듈성을 이용해 객체 지향 언어의 클래스나 함수형 언어의 함수가 될 만한 서로 연관된 코드를 논리적으로 묶는다.

프로그래밍 언어에 내장된 패키징 메커니즘은 이제 워낙 다양해져서 개발자가 그 중 하나를 선택하는 것조차 힘들어졌다. 예를 들어, 요즘 언어는 대부분 각각 가시성과 스코핑 규칙이 다른 함수/메서드, 클래스, 패키지/네임스페이스에 개발자가 로직을 정의할 수 있다.

아키텍트는 개발자가 코드를 어떻게[ 패키징하는지 반드시 알아야 한다. 아키텍처에 중요한 영향을 미치기 때문이다.

우리는 아키텍처를 논할 때 클래스, 함수처럼 코드를 묶어 놓은 덩어리를 모듈성이라는 일반 용어로 나타낸다. 이것은 논리적인 구분이지 물리적인 구분은 아닌데, 이 차이점이 굉장히 중요한 경우가 있다. 예를 들어, 모놀리식 애플리케이션은 편의상 꽤 많은 클래스를 한 덩이로 묶어도 크게 상관없지만, 아키텍처를 재구축할 때에는 이렇게 커플링된 구조가 모놀리스를 나누는 데 걸림돌이 된다. 따라서 모듈성은 특정 플랫폼에 함축되어 있거나 불가피한 물리적인 분리와 다른 개념으로 바라보는 게 좋다.

3.2 모듈성 측정

3.2.1 응집

응집(cohesion)은 한 모듈의 파트(구성 요소)가 동일한 모듈 안에 얼마나 포함되어 있는지를 나타낸다.
다시 말해, 모듈을 구성하는 파트가 서로 얼마나 연관되어 있는가, 하는 것이다.

응집된 모듈을 나누려고 해봐야 더 커플링되고 가독성을 떨어진다.

래리 콘스탄틴(Larry Constantine)

컴퓨터 과학자들이 정의한 응집도의 측정 범위를 가장 좋은 것부터 나쁜 것 순으로 나열해 보자.

  • 기능적 응집(functional cohesion)
    • 모듈의 각 파트는 다른 파트와 연관되어 있고 기능상 꼭 필요한 것들이 모듈에 들어있다.
  • 순차적 응집(sequential cohesion)
    • 두 모듈이, 한쪽이 데이터를 출력하면 다른 한쪽이 그것을 입력 받는 형태로 상호작용한다.
  • 소통적 응집(communication cohesion)
    • 두 모듈이, 각자 정보에 따라 작동하고 어떤 출력을 내는 형태로 통신 체인을 형성한다.
    • 예를 들면, 데이터베이스에 레코드를 추가하면 그 정보에 따라 이메일이 만들어지는 식.
  • 절차적 응집(procedural cohesion)
    • 두 모듈은 정해진 순서대로 실행되어야 한다.
  • 일시적 응집(temporal cohesion)
    • 모듈은 시점 의존성(timing dependency)에 따라 연관된다.
    • 예를 들어, 많은 시스템들이 시동할 때 그다지 관련이 없어 보이는 것들을 초기화 하는경우가 많은데, 이런 작업들이 일시적으로 응집됐다고 할 수 있다.
  • 논리적 응집(logical cohesion)
    • 모듈의 내부 데이터는 기능적이 아니라, 논리적으로 연관되어 있다.
    • 이를테면, 텍스트, 직렬화 객체, 스트림 형태로 받은 데이터를 변환하는 모듈. ex) 자바의 StringUtils 패키지
    • 서로 연관된 작업들이지만 하는 일은 전혀 다르다.
  • 동시적 응집(coincidental cohesion)
    • 같은 소스 파일에 모듈 구성 요소가 들어 있지만 그 외에는 아무 연관성도 없습니다.
    • 이는 가장 좋지 않은 형태의 응집이다.

응집은 커플링보다는 덜 정확한 메트릭이므로 아키텍트 재량에 따라 특정된 모듈의 응집도는 다르다.
예를 들어, 모듈을 다음과 같이 정의했다고 가정하자.

  • 고객 관리
    • 고객 추가
    • 고객 수정
    • 고객 조회
    • 고객 알림
    • 고객 주문 조회
    • 고객 주문 취소

마지막 두 항목은 고객 관리 모듈에 그냥 두거나, 다음과 같이 2개의 모듈로 나눌 수 있다.

  • 고객 관리
    • 고객 추가
    • 고객 수정
    • 고객 조회
    • 고객 알림
  • 주문 관리
    • 고객 주문 조회
    • 고객 주문 취소

하지만 이것도 경우에 따라 어떠한 것도 정확한 구성이 될 수 있다.

  • 주문 관리에 작업이 2개뿐인가? 만약 그렇다면 두 작업을 고객 관리로 다시 돌려놓는 게 더 합리적일 것 같다.
  • 고객 관리 모듈이 앞으로도 계속 확장될 에정이고 개발자가 작업을 추출할 일이 많을까?
  • 두 모듈이 작동하려면 반드시 주문 관리 모듈이 고객 정보를 많이 알고 있어야 하는가? 이는 앞서 래리 콘스탄틴이 한 말과 연관된다.

위와 같은 질문이 소프트웨어 아키텍트 업무의 핵심이라고 할 수 있는 트레이드오프 분석이다.

컴퓨터 과학자들은 응집의 주관성을 전제로, 응집도(응집의 결여도)를 가늠할 수 있는 정말 우수한 구조적 메트릭을 개발했다.
메서드의 응집 결여도(Lack of Conhesion in Methods, LCOM)는 모듈(보통 컴포넌트)의 구조적 응집도를 나타낸다.

LCOM : 공유 필드를 통해 공유되지 않는 메서드의 총 갯수


클래스 X는 구조적 응집이 우수한 반면, 클래스 Y는 응집이 결여되어 있다. 클래스 Y의 필드/메서드 세 쌍은 각자 자기 클래스에 두어도 별로 상관이 없을 듯 하다. 클래스 Z는 응집이 조합된 모양새로, 세 번째 필드/메서드 쌍은 개발자가 자체 클래스로 빼도 된다.

3.2.2 커플링

다행이도 코드베이스의 커플리은 그래프 이론에 기반한 좋은 분석 도구들이 많이 있다. 메서드의 호출과 반환은 호출 그래프를 형성하므로 수학적인 분석이 가능하다. 워드 요던(Edward Yourdon)과 래리 콘스탄틴(Larry Constantine)이 지은 Structured Design에는 구심 커플링, 원심 커플링을 비롯한 중요한 개념들이 대거 등장한다.
구심 커플링은 (컴포넌트, 클래스, 함수 등의) 코드 아티팩트로 유입되는 접속 수를, 원심 커플링은 다른 코드 아티팩트로 유출되는 접속 수를 나타낸다.

3.2.3 추상도, 불안정도, 메인 시퀀스로부터의 거리

컴포넌트 커플링이 아키텍트에게 유의미한, 있는 그대로의 가치가 있다면, 여기서 파생된 다른 메트릭들도 잘 살펴볼 필요가 있다.

추상도는 추상 아티팩트와 구상 아티팩트(구현체)의 비율, 즉 구현 대비 추상화 정도를 나타낸다. 가령, 추상화를 전혀 하지 않고 (main() 메서드 하나에 코드를 전부 다 몰아넣은 경우처럼) 하나의 엄청나게 큰 함수 코드가 있는 코드베이스도 있고, 반대로 너무 지나치게 추상화해서 코드가 서로 어떻게 연결되어 있는지 개발자가 파악하기 어려운 코드베이스도 있다.

추상도 = 추상 요소 / 구상 요소

아키텍트는 추상 아티팩트의 총 개수와 구상 아티팩트의 총 개수로 추상도를 계산한다.
여기서 파생된 불안정도는 원심 커플링과의 비율이다.

불안정도 = 원심 커플링 / (원심 커플링 + 구심 커플링)

불안정도는 코드베이스의 변동성을 의미하므로 불안정도가 높은 코드베이스는 변경 시 커플링이 높아 더 깨지기 쉽다. 예를 들어, 여러 다른 클래스를 호출해서 작업을 위임하는 클래스는 호출되느,ㄴ 메서드 중 하나라도 변경되면 호출하는 이 클래스 역시 잘못될 공산이 매우 크다.

3.2.4 메인 시퀀스로부터의 거리

메인 시퀀스로부터의 거리는 아키텍처 구조를 평가하는 몇 가지 전체적인 매트릭 중 하나로, 불안정도와 추상도를 이용하여 계산한다.

메인 시퀀스로부터의 거리 = | 추상도 + 불안정도 - 1 |


개발자는 후보 클래스를 그래프로 그려보고 이상적인 선에서 얼마나 떨어져 있는지 거리를 잰다. 이 선에 가까울수록 클래스 균형이 잘 맞다는 방증이다.
오른쪽 위로 치우친 부분을 쓸모없는 구역(즉, 너무 추상화를 많이해서 사용하기 어려운 코드), 반대로 왼쪽 아래로 치우친 부분을 고통스런 구역(즉, 추상화를 거의 안 하고 구현 코드만 잔뜩 넣어 취약하고 관리하기 힘든 코드)이라고 한다.

3.2.5 커네이션스

밀러 페이지-존스는 1996년에 출간된 What Every Programmer Should Know About Object-Oriented Design에서 구심/원심 커플링 메트릭을 더욱 발전시킨 커네이선스(connascence) 개념을 객체 지향 언어의 화두로 던졌다.

두 컴포넌트 중 한쪽이 변경될 경우 다른 쪽도 변경해야 전체 시스템의 정합성이 맞는다면 이들은 커네이선스를 갖고 있는 것이다.

밀러 페이지-존스

정적 커네이선스

정적 커네이선스는 소스 코드 레벨의 커플링으로, 구심/원심 커플링을 발전시킨 개념이다. 다시 말해, 아키텍트는 구심적이든, 원심적이든 다음 종류의 정적 커네이선스를 뭔가에 커플링된 정도라고 보는 것이다.

  • 명칭 커네이선스
    • 여러 컴포넌트의 엔티티명이 일치해야 한다.
    • 메서드명은 코드베이스가 커플링되는 가장 일반적이면서 바람직한 방법이다.
  • 타입 커네이선스
    • 여러 컴포넌트의 엔티티 타입이 일치해야 한다.
    • 대부분의 정적 타입 언어에서 변수와 매개변수를 특정 타입으로 제한하는 일반적인 기능이다.
  • 의미 커네이선스 또는 관례 커네이선스
    • 여러 컴포넌트에 걸쳐 어떤 값의 의미가 일치해야 한다.
    • 이런 종류의 커네이선스는 상수 대신 숫자를 하드코딩한 코드베이스에서 흔히 발견된다.
    • 가령, 코드 어딘가에 int TRUE = 1; int FALSE = 0처럼 박아 놓고 쓰는 식이다.
      • (누군가 이 값을 바꾸면 결과는 끔찍할 것이다.)
  • 위치 커네이선스
    • 여러 컴포넌트는 값의 순서가 일치해야 한다.
    • 정적 타이핑이 가능한 언어에서도 메서드와 함수 호출 시 전달하는 매개변수 값은 순서가 맞아야 한다.
    • 예를 들어, void updateSeat(String name, String seatLocation)라는 메서드를 만들고 update("14D", "Ford,N")로 호출하면 타입은 맞지만 의미는 맞지 않는다.
  • 알고리즘 커네이선스
    • 여러 컴포넌트는 특정 알고리즘이 일치해야 한다.
    • 흔한 예로, 서버/클라이언트 둘 다 실행되어야 하고 유저 인증 시 반드시 동일한 결과를 내야하는 보안 해시 알고리즘이 그렇습니다.
      • 이것은 아주 커플링이 심하다는 증거이다.

동적 커네이선스

동적 커네이선스는 런타임 호출을 분석하는, 페이지-존스가 정의한 또 다른 유형의 커네이선스다.

  • 실행 커네이선스
    • 여러 컴포넌트의 실행 순서가 중요하다.
    • 실행 순서가 보장되지 않으면 제대로 작동하지 않는다.
  • 시점 커네이선스
    • 여러 컴포넌트의 실행 시점이 중요하다.
    • 일반적인 사례는 동시에 실행 중인 두 스레드 때문에 경합 조건이 발생하여 결과에 영향을 끼치는 것이다.
  • 값 커네이선스
    • 상호 연관된 다수의 값들을 함께 변경할 때 발생한다.
    • 개발자가 꼭지점 4개로 사각형을 정의했다고 해보자.
      • 자료 구조의 무결성을 유지하려면 다른 꼭지점에 미치는 영향을 고려하여 함부로 어느 한 꼭지점을 변경해선 안 된다.
  • 식별 커네이선스
    • 여러 컴포넌트가 동일한 엔티티를 참조할 때 발생한다.
    • 가장 대표적인 사례는, 독립적인 두 컴포넌트가 분산 큐 같은 자료 구조를 공유해서 업데이트하는 경우다.

런타임 호출은 호출 그래프에 비해 효과적인 분석 도구가 많지 않아 아키텍트는 동적 커네이선스를 파악하기가 쉽지 않다.

커네이선스 속성

  • 강도
    아키텍트는 개발자가 어떤 유형의 커네이선스를 얼마나 쉽게 리팩터링할 수 있는지에 따라 커네이선스 강도를 결정한다.
    정적 커네이선스는 개발자가 간단히 소스 코드를 분석하거나 최신 도구를 활용하면 어렵잖게 개선할 수 있기 때문에 아키텍트는 동적 커네이선스보다 정적 커네이선스를 선호한다. 예를 들어, 의미 커네이선스를 생각해보면 매직 밸류(magic value) 대신 기명 상수(named constant)를 만드는 명칭 커네이선스로 리팩터링하면 의미 커네이선스가 개선될 것이다.


    커네이선스 강도는 리팩터링을 안내하는 충실한 표지판이다.

  • 지역성
    커네이선스의 지역성은 코드베이스의 모듈들이 서로 얼마나 가까이 있는가, 이다.
    근접한 코드는 보통 더 분리된 코드보다 높은 형태의 커네이선스를 가진다. 즉, 모듈을 서로 떨어트렸을 때 커플링이 형편없는 형태의 커네이선스는 모듈을 서로 가까이 붙여 놓는 식으로 개선할 수 있다.
    개발자는 강도와 지역성을 함께 고민해야 한다. 동일한 모듈에서 더 강한 형태의 커네이선스가 발견된다면 그와 동일한 커네이선스가 널리 흩어져 있는 것보다는 코드 스멜이 덜하다는 증거이다.

  • 정도
    커네이선스 정도는 커네이선스가 미치는 영향의 규모에 관한 것이다.
    이 값이 작을수록 코드베이스 입장에서는 바람직하다. 모듈이 몇 개 안 된다면 동적 커네이선스가 높아도 별로 해롭지 않지만, 코드베이스는 점점 커지기 마련이니 사소한 문제도 점점 더 악화될 것이다.

페이지-존스가 제시한 커네이선스를 이용해 시스템의 모듈성을 개선하는 세 가지 방법은 아래와 같다.

  1. 시스템을 캡슐화한 요소들로 잘게 나누어 전체 커네이선스를 최소화한다.
  2. 캡슐화 경계를 벗어나는 나머지 커네이선스를 모조리 최소화한다.
  3. 캡슐화 경계 내부에서 커네이선스를 최대화한다.

전설적인 소프트웨어 아키텍처 혁신가, 짐 웨이리치(Jim Weirich)는 커네이선스 개념을 다시 대중화하면서 두 가지 위대한 조언을 남겼다.

  • 정도의 규칙(Rule of Degree)
    강한 형태의 커네이선스를 보다 약한 형태의 커네이선스로 전환하라.
  • 지역성의 규칙(Rule of Locally)
    소프트웨어 엘리먼트 간의 거리가 멀어질수록 보다 약한 형태의 커네이선스를 사용하라.

3.2.6 커플링과 커네이선스 메트릭을 통합

지금까지 우리는 시기와 목표가 상이한 커플링과 커네이선스를 이야기했다. 아키텍트 관점에서는 이 두 가지 뷰가 서로 중첩된다. 페이지-존스가 정적 커네이선스라고 밝힌 것들은 유출/유입 커플링 정도를 나타낸다. 구조적 프로그래밍은 들어오고 나가는 것에만 관심이 있는 반면, 커네이선스는 여러 가지 요소가 서로 어떻게 커플링되는지에 주목한다.


구조적 프로그래밍의 커플링 개념은 왼쪽, 커네이선스 특성은 오른쪽에 표시되어 있다. 구조적 프로그래밍에서 데이터 커플링(메서드 호출)이라고 부르는 커네이선스는 커플링이 어떻게 나타나야하는지 알려준다.

1990년대 커네이선스의 문제점

아키텍트가 이런 유용한 메트릭을 적용해서 시스템을 분석/설계할 때에는 몇 가지 문제점이 있다.

첫째, 이들 메트릭은 아키텍처 구조보다는 저수준 코드의 세부분을, 코드 품질 및 정리 상태 위주로 관찰한다.
둘째, 사실 커네이선스 자체는 요즘 아키텍트가 내려야 할 근본적인 결정에 관한 문제는 다루지 않는다.

3.3 모듈에서 컴포넌트로

이 책에서는 연관된 코드의 묶음을 모듈이라는 일반 용어로 표현하지만, 대부분의 플랫폼은 소프트웨어 아키텍트에게 핵심 구성 요소 중 하나인 컴포넌트 형태로 지원한다. 논리적, 물리적 분리에 관한 개념과 그에 따른 분석은 컴퓨터 과학 초창기부터 존재했지만, 아직도 컴포넌트 분리에 관한 수많은 글과 의견이 쏟아져 나오고 있는 가운데 개발자와 아키텍트는 좋은 결과를 내기 위해 애쓰고 있다.