Why This Work Exists
모듈 수가 늘어나면 빌드 시간, 테스트 비용, 변경 파급이 급격히 커진다. 특히 "공통 모듈이 기능 모듈을 참조"하거나 "구현체 직접 의존"이 누적되면 작은 수정도 다수 모듈 재빌드와 회귀 검증으로 이어진다.
레포를 특정하지 않고도 모듈 아키텍처를 평가할 수 있도록, 의존 방향, 테스트 경계, 변경 전파, 빌드 영향도를 정량·정성 기준으로 정리한다. 이 문서는 개념 설명을 넘어서 실제 리팩터링 순서와 검증 체크리스트까지 포함한다.
2026-04-29
모듈 수가 늘어나면 빌드 시간, 테스트 비용, 변경 파급이 급격히 커진다. 특히 "공통 모듈이 기능 모듈을 참조"하거나 "구현체 직접 의존"이 누적되면 작은 수정도 다수 모듈 재빌드와 회귀 검증으로 이어진다.
모듈 전체를 한 번에 뜯지 말고, "파급이 큰 경계"부터 순차적으로 분리한다.
| 계약 타입 | 노출 규칙 | 예시 |
|---|---|---|
| UseCase Interface | Feature 외부에 동작 계약만 노출 | LoginUseCaseInterface |
| Gateway Interface | Core 구현체 대신 protocol 노출 | TokenStoreInterface |
| DTO/Entity | 경계 간 전달 모델을 불변에 가깝게 유지 | AuthTokenDTO |
classDiagram
class AppComposer
class FeatureInteractor
class FeaturePresenter
class WorkerInterface {
<>
+execute()
}
class WorkerImpl
class CoreGatewayInterface {
<>
+request()
}
class CoreGatewayImpl
AppComposer --> FeatureInteractor : injects
FeatureInteractor --> WorkerInterface : depends on
WorkerImpl ..|> WorkerInterface
WorkerImpl --> CoreGatewayInterface
CoreGatewayImpl ..|> CoreGatewayInterface
FeatureInteractor --> FeaturePresenter
sequenceDiagram
participant VC as ViewController
participant I as Interactor
participant W as WorkerInterface
participant C as CoreGatewayInterface
participant P as Presenter
VC->>I: doAction(request)
I->>W: execute(request)
W->>C: request(dto)
C-->>W: response
W-->>I: result
I->>P: present(result)
P-->>VC: viewModel
flowchart LR
A[Start: Analyze module graph] --> B{Reverse dependency exists?}
B -->|Yes| C[Split contract module]
B -->|No| D{Direct concrete dependency exists?}
C --> D
D -->|Yes| E[Introduce interface + DI]
D -->|No| F{Test doubles can replace I/O?}
E --> F
F -->|No| G[Refactor injection boundary]
F -->|Yes| H[Measure build impact]
G --> H
H --> I[Finalize checklist + rollout]
final class LoginInteractor {
private let worker = RealLoginWorker()
func login(id: String, pw: String) async throws -> Token {
try await worker.login(id: id, pw: pw)
}
}protocol LoginWorkerInterface {
func login(id: String, pw: String) async throws -> Token
}
final class LoginInteractor {
private let worker: LoginWorkerInterface
init(worker: LoginWorkerInterface) {
self.worker = worker
}
func login(id: String, pw: String) async throws -> Token {
try await worker.login(id: id, pw: pw)
}
}final class LoginWorkerStub: LoginWorkerInterface {
var result: Result!
func login(id: String, pw: String) async throws -> Token {
switch result {
case .success(let token): return token
case .failure(let error): throw error
case .none: fatalError("result not set")
}
}
}
func test_login_success() async throws {
let stub = LoginWorkerStub()
stub.result = .success(Token(value: "t1"))
let sut = LoginInteractor(worker: stub)
let token = try await sut.login(id: "a", pw: "b")
assert(token.value == "t1")
} 핵심: 실제 네트워크 없이 use case 테스트가 독립적으로 수행된다.
| 지표 | 의미 | 권장 방향 |
|---|---|---|
| 역방향 의존 수 | 하위 레이어가 상위 레이어를 참조하는 건수 | 0 |
| 순환 컴포넌트 수 | 강결합 SCC(Strongly Connected Component) 개수 | 0 |
| 직접 구현체 의존 비율 | 구현체 타입 import / 전체 의존 | 감소 추세 |
| 테스트 더블 대체율 | 테스트에서 외부 I/O를 더블로 대체 가능한 비율 | 상승 추세 |
| 변경 전파 크기 | 모듈 1개 변경 시 재빌드/재테스트 대상 수 | 감소 추세 |