Software Design

[모듈화 #2] 모듈 의존성 설계 총정리, 순환 의존은 왜 생기고 어떻게 끊을까

지난 글에서 모듈화란 "같이 바뀌는 것끼리 묶어 경계를 세우는 일"이라고 정리했습니다.

이석우iOS Developer4분 읽기
[모듈화 #2] 모듈 의존성 설계 총정리, 순환 의존은 왜 생기고 어떻게 끊을까 대표 이미지

지난 글에서 모듈화란 “같이 바뀌는 것끼리 묶어 경계를 세우는 일”이라고 정리했습니다.

그런데 모듈을 나누고 나면 진짜 문제가 시작됩니다. 나눈 조각들이 서로를 참조하기 시작하거든요.

A가 B를 쓰고, B가 C를 쓰고, 어느 날 C가 다시 A를 쓰는 순간, 모듈화의 이점은 전부 사라집니다. 셋이 사실상 한 덩어리가 된 거니까요.

핵심 답부터 드리면, 모듈 의존성 설계의 원칙은 두 가지입니다. 의존 방향을 한쪽으로만 흐르게 하고, 자주 바뀌는 쪽이 안정된 쪽에 의존하게 하는 것이죠.

이 글에서는 의존 방향을 정하는 기준, 순환 의존이 생기는 전형적인 경로, 끊는 방법 세 가지를 정리합니다.

모듈 의존성 화살표가 돌기 시작하면 모듈화는 무너집니다
모듈 의존성 화살표가 돌기 시작하면 모듈화는 무너집니다

의존 방향은 어느 쪽으로 흘러야 할까?

의존성에는 방향이 있습니다. A 모듈이 B 모듈을 import하면, A는 B에 의존하는 거예요.

이때 화살표가 어디를 향해야 하는가에는 고전적인 답이 있습니다.

불안정한 것이 안정된 것에 의존해야 한다. 반대가 되는 순간, 사소한 변경이 시스템 전체로 번진다.

안정된 모듈이란 자주 안 바뀌는 모듈입니다. 도메인 모델, 공용 프로토콜 같은 것들이죠.

불안정한 모듈은 자주 바뀌는 모듈이에요. 화면(UI), 이벤트 대응 로직처럼 요구사항 따라 계속 손대는 부분입니다.

그래서 건강한 의존 그래프는 대체로 이런 방향이 됩니다.

  • 화면·기능 모듈 → 도메인 모듈 → 공용 인터페이스 모듈
  • 화살표는 아래(안정된 쪽)로만 흐르고, 거슬러 올라가지 않는다

로버트 마틴(Robert C. Martin)은 이걸 안정 의존 원칙(SDP, Stable Dependencies Principle)이라고 불렀습니다.


순환 의존은 왜 생길까?

순환 의존은 대부분 악의 없이, 아주 자연스럽게 생깁니다. 전형적인 경로는 이래요.

주문(Order) 모듈이 회원(User) 모듈을 참조합니다. 주문에 주문자 정보가 필요하니까요.

시간이 지나 회원 화면에 “최근 주문 목록”을 보여달라는 요구가 옵니다. 회원 모듈이 주문 모듈을 참조하는 순간, 순환 완성입니다.

주문과 회원이 서로를 부르면 이렇게 고리가 완성돼요
주문과 회원이 서로를 부르면 이렇게 고리가 완성돼요

순환이 생기면 세 가지가 무너집니다.

  1. 빌드 단위 분리 실패: 컴파일러가 둘을 따로 처리할 수 없어 사실상 한 모듈이 됩니다 (Swift는 모듈 간 순환 import를 아예 컴파일 에러로 막습니다)
  2. 독립 테스트 불가: A를 테스트하려면 B가 필요하고, B를 테스트하려면 A가 필요한 교착
  3. 영향 범위 예측 불가: 어느 쪽을 고쳐도 반대쪽까지 재검토해야 합니다

순환 의존 어떻게 끊나요?

실무에서 쓰는 방법은 크게 세 가지입니다.

첫째, 공통 조각을 아래로 내리기.

양쪽이 서로 원하는 게 사실 “상대의 일부”라면, 그 일부를 더 안정된 하위 모듈로 추출합니다. 주문·회원이 서로 필요로 하는 타입만 모아 도메인 모델 모듈로 내리는 식이죠.

둘째, 인터페이스로 방향 뒤집기(DIP).

한쪽 의존을 프로토콜로 대체합니다. 회원 모듈은 “주문 목록을 주는 무언가”라는 프로토콜만 정의하고, 구현은 주문 모듈이 하는 거예요.

// User 모듈: 필요한 능력만 프로토콜로 선언
public protocol OrderHistoryProviding {
    func recentOrders(of userID: String) -> [OrderSummary]
}

// Order 모듈: User 모듈의 프로토콜을 채택해 구현
public struct OrderHistoryProvider: OrderHistoryProviding {
    public func recentOrders(of userID: String) -> [OrderSummary] {
        // 주문 저장소 조회
    }
}

이러면 화살표는 Order → User 한 방향만 남습니다. 앞서 발행한 DIP 글에서 다룬 의존성 역전이 모듈 단위에서 그대로 쓰이는 겁니다.

셋째, 조립을 위로 올리기.

두 모듈을 직접 연결하지 않고, 둘 다 아는 상위 모듈(앱 타깃, 조립 계층)이 연결합니다. 화면 전환처럼 기능 모듈끼리 서로를 불러야 하는 상황에서 특히 유용해요.

상황 끊는 방법
서로 상대의 타입 일부만 필요 공통 타입을 하위 모듈로 추출
한쪽이 다른 쪽의 기능을 호출 프로토콜 정의 후 의존성 역전
기능 모듈끼리 화면 전환 상위 조립 계층에서 연결

언제 적용하고 언제 과할까?

의존 방향 관리도 비용이 듭니다. 프로토콜이 늘어나고, 조립 코드가 생기니까요.

  • 모듈이 3~4개 이상이고 팀이 나뉘어 있다면: 방향 규칙을 문서화하고 순환을 도구로 감시할 가치가 충분합니다
  • 모듈 2개짜리 소규모 프로젝트라면: 순환만 안 만들면 됩니다. 모든 참조를 프로토콜로 감싸는 건 과합니다
  • 이미 순환이 있는 레거시라면: 한 번에 다 끊으려 하지 말고, 가장 자주 바뀌는 모듈부터 화살표를 정리하세요

면접에서는 이렇게 물어봅니다

Q. 모듈 간 순환 의존이 왜 문제이고, 어떻게 해결하시겠습니까?

순환이 생기면 두 모듈이 빌드·테스트·배포 관점에서 한 덩어리가 되어 모듈화의 이점이 사라집니다. 해결은 공통 타입을 하위 모듈로 추출하거나, 프로토콜을 정의해 의존 방향을 역전(DIP)하거나, 상위 조립 계층에서 두 모듈을 연결하는 방식으로 화살표를 한 방향으로 정리합니다.

Q. 안정 의존 원칙(SDP)을 설명해 보세요.

모듈은 자신보다 안정적인(변경이 적은) 모듈에만 의존해야 한다는 원칙입니다. 자주 바뀌는 모듈이 의존의 하류에 있으면 그 변경이 상류 전체로 전파되므로, 변경 빈도가 낮은 도메인·인터페이스 모듈을 그래프의 아래쪽에 두는 구조가 건강합니다.

화살표 하나 끊는 데도 회의가 필요할 때가 있죠
화살표 하나 끊는 데도 회의가 필요할 때가 있죠

다음 글에서는 많은 팀이 모듈화 도중에 빠지는 함정, 이름부터 위험한 Common 모듈 이야기를 합니다.

“일단 공용이니까 Common에 넣자”가 어떻게 모듈 전체를 다시 한 덩어리로 되돌리는지, 레이어 구조로 어떻게 예방하는지 다룰게요.