
멀티 모듈을 직접 구현해보고 개발을 하면서 구현하기 시작한 그 즉시부터 지금까지 생겼던 의문을 해결하고 글로 정리한다.
Core-API, Core-Domain으로 모듈이 분리되어있다고 가정하자.
Core-API에서는 Presentation Layer를 Core-Domain에서는 Business, Implement, Data-access Layer가 모두 들어있다고 가정하자.
Core-API 모듈에서는 컨트롤러와 DTO를 관리하고 있다. 이곳은 클라이언트로부터의 요청을 직접 받는, 즉 외부 스펙을 정의하는 계층이다.
그렇다면 비즈니스 로직을 담당하는 Service 단에서는 클라이언트의 요청 데이터를 어떻게 받아야 할까?
기존처럼 Controller → Service → Repository 흐름을 그대로 따른다면, 컨트롤러에서 받은 Request 객체를 그대로 서비스 레이어에 전달하게 된다.
하지만 이 방식은 멀티모듈 구조에서 큰 문제를 일으킨다.
Core-Domain 모듈이 Core-API 모듈에 의존하게 되는 구조가 되고, 이는 곧 양방향 참조를 유발하게 된다. 순환 참조로 인해 컴파일 에러가 발생하거나, 의존성이 꼬이게 되는 상황이 벌어질 수 있다.
이럴 때는 각 계층의 책임과 역할을 다시 떠올려야 한다.
외부 클라이언트는 항상, 그리고 자주 변한다. 따라서 Core-API 모듈의 DTO는 외부의 변화에 유연하게 대응할 수 있어야 하며, 그 자체로 Presentation Layer를 구성하는 역할을 맡는다.
실제로 중요한 비즈니스 로직은 Core-Domain에서 수행된다. 이 도메인 레이어는 외부 스펙에 종속되면 안 된다. 대신 외부에서 들어온 요청 DTO는 도메인 모델로 변환되어 서비스 레이어로 전달된다.
data class NewTodoRequest(
val title: String,
val category: String,
val scheduledDate: LocalDateTime,
val notificationTime: LocalDateTime? = null,
) {
fun toNewTodo(): NewTodo {
return NewTodo(
title = this.title,
category = this.category,
scheduledDate = this.scheduledDate,
notificationTime = this.notificationTime
)
}
}
이렇게 클라이언트에게서 새로운 Todo를 만들 때 위와 같은 스펙을 가져왔다고 하자.
이 Request DTO 객체는 Presentation Layer에서 관리한다.
이제 저기서 입력받은 title, category, scheduledDate, notificationTime을 비즈니스 로직에서 사용한다.
위에서 입력받은 외부 스펙을 비즈니스 로직에서 사용하기 위해 toNewTodo로 변환하는 것을 볼 수 있다.
data class NewTodo(
val title: String,
val category: String,
val scheduledDate: LocalDateTime,
val notificationTime: LocalDateTime? = null,
)
이 NewTodo 객체는 Core-Domain에 위치한 도메인 모델이며, 서비스 로직은 오직 이 도메인 객체를 기준으로 동작한다.
결과적으로 Core-API → Core-Domain으로만 참조가 발생하는 순방향 참조 구조가 완성된다.
나 역시 이 부분에서 많은 고민을 했다. 코드 필드가 거의 동일한데, 굳이 두 클래스를 만들어야 하는가?
NewTodoRequest: 클라이언트 요청을 표현 (외부와의 계약)NewTodo: 도메인 로직을 위한 내부 모델비슷해 보이지만, 역할이 완전히 다르다.
예를 들어, 클라이언트 요구로 필드명이 category에서 categoryName으로 바뀐다면 NewTodoRequest만 수정되면 되고, NewTodo는 그대로 유지될 수 있다.
NewTodoRequest는 외부 요청 포맷에 의존하므로 Mocking 대상이 된다.NewTodo는 도메인 로직의 단위 테스트 대상이 된다.두 개를 하나로 합치면, 테스트 목적이 다른 코드가 결합되어 유연하지 못한 구조가 된다.
겉으로 보기엔 중복처럼 보여도, 외부 변화와 내부 도메인을 분리할 수 있는 방화벽 역할을 한다.
멀티모듈을 선택한 이상, 각 계층의 책임을 명확히 나누고 의존 방향은 항상 도메인 중심으로 흘러가야 한다.
이것은 단순한 클래스 분리 이상의 의미를 가지며, 변화에 강한 아키텍처를 만들기 위한 필수적인 설계다.
모든 것은 트레이드 오프인 것 같다.
하지만 멀티 모듈을 사용하기로 했을 때의 이점을 생각해보고 그에 따라서 모듈 간의 의존 관계를 잘 분리해보자.