API DTO vs 도메인 모델: 멀티모듈 설계에서 책임의 경계

임동혁 Ldhbenecia·2025년 7월 10일

SpringBoot

목록 보기
20/28
post-thumbnail

개요

멀티 모듈을 직접 구현해보고 개발을 하면서 구현하기 시작한 그 즉시부터 지금까지 생겼던 의문을 해결하고 글로 정리한다.

의문점

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으로만 참조가 발생하는 순방향 참조 구조가 완성된다.

거의 유사한 코드인데 불필요하게 클래스 파일을 생성하게 되는게 아닐까?

나 역시 이 부분에서 많은 고민을 했다. 코드 필드가 거의 동일한데, 굳이 두 클래스를 만들어야 하는가?

1. 계층 간 역할 분리 (Separation of Concerns)

  • NewTodoRequest: 클라이언트 요청을 표현 (외부와의 계약)
  • NewTodo: 도메인 로직을 위한 내부 모델

비슷해 보이지만, 역할이 완전히 다르다.

예를 들어, 클라이언트 요구로 필드명이 category에서 categoryName으로 바뀐다면 NewTodoRequest만 수정되면 되고, NewTodo는 그대로 유지될 수 있다.

2. 유지보수성과 테스트 용이성

  • NewTodoRequest는 외부 요청 포맷에 의존하므로 Mocking 대상이 된다.
  • NewTodo는 도메인 로직의 단위 테스트 대상이 된다.

두 개를 하나로 합치면, 테스트 목적이 다른 코드가 결합되어 유연하지 못한 구조가 된다.

3. 변화에 유연한 구조

겉으로 보기엔 중복처럼 보여도, 외부 변화와 내부 도메인을 분리할 수 있는 방화벽 역할을 한다.

멀티모듈을 선택한 이상, 각 계층의 책임을 명확히 나누고 의존 방향은 항상 도메인 중심으로 흘러가야 한다.

이것은 단순한 클래스 분리 이상의 의미를 가지며, 변화에 강한 아키텍처를 만들기 위한 필수적인 설계다.

결론

모든 것은 트레이드 오프인 것 같다.
하지만 멀티 모듈을 사용하기로 했을 때의 이점을 생각해보고 그에 따라서 모듈 간의 의존 관계를 잘 분리해보자.

0개의 댓글