김경록의 앱 개발 여정

[Clean Architecture] DTO와 도메인 모델의 차이와 분리해서 사용해야하는 이유 본문

TIL

[Clean Architecture] DTO와 도메인 모델의 차이와 분리해서 사용해야하는 이유

Kim Roks 2025. 3. 13. 21:25

소프트웨어 아키텍처를 설계하다 보면
DTO(Data Transfer Object)도메인 모델(Domain Model)이라는 용어를 자주 접하게 됩니다.
이 두 가지 개념은 비슷해 보일 수 있지만, 각자의 역할과 책임이 명확하게 구분되어 있습니다.

어느 순간 이 둘의 개념 명확히 분리하지 않고 사용하던 제 모습을 발견해서

오늘은 DTO와 도메인 모델의 차이점, 그리고 왜 이들을 분리해서 사용해야 하는지에 대해 알아봤습니다.

 

 

1. DTO (Data Transfer Object)

역할 및 특징

  • 데이터 전송 전용: DTO는 주로 네트워크 요청/응답, API 통신, 데이터베이스 결과 등의 외부 시스템과의 데이터 교환에 사용됩니다.
  • 단순 데이터 컨테이너: 비즈니스 로직이나 행동이 포함되지 않고, 오직 데이터를 담아 전달하는 역할만 수행합니다.
  • 유연성: 외부 시스템의 변경(예: API 스펙 변경)에 민감하지만, 그 변경사항을 한 곳(DTO)에서만 관리하면 되므로 전체 시스템에 미치는 영향을 최소화할 수 있습니다.

사용 예시

struct UserDTO: Decodable {
    let id: Int
    let name: String
    let email: String
}
 

위 예시에서 UserDTO는 API로부터 전달받은 JSON 데이터를 디코딩하기 위한 모델로 사용됩니다. 단순히 데이터를 전달할 목적(Data Carrier)으로 만들어졌기 때문에, 비즈니스 로직은 전혀 포함되어 있지 않습니다.

 

2. 도메인 모델 (Domain Model)

역할 및 특징

  • 비즈니스 로직의 핵심: 도메인 모델은 애플리케이션의 핵심 비즈니스 로직과 규칙을 구현하는 객체입니다. 이는 단순히 데이터를 담는 역할을 넘어, 해당 데이터에 대한 행동과 연관된 규칙을 포함할 수 있습니다.
  • 순수성 유지: 도메인 모델은 외부 시스템(네트워크, 데이터베이스 등)의 구현 세부사항과 분리되어 있어야 합니다. 이를 통해 비즈니스 로직은 외부 변화에 영향을 받지 않고, 독립적으로 유지보수될 수 있습니다.
  • 구조화된 데이터: 여러 속성이나 복잡한 비즈니스 규칙이 결합되어 있을 때, 도메인 모델을 통해 이를 하나의 객체로 관리하면 코드의 가독성과 관리성이 향상됩니다.

사용 예시

struct User {
    let id: Int
    let name: String
    let email: String
    
    // 비즈니스 로직 예: 이메일 도메인 검사
    func isCorporateEmail() -> Bool {
        return email.hasSuffix("@company.com")
    }
}

 

위 예시에서 User는 도메인 모델로, 사용자의 정보를 담을 뿐만 아니라, 특정 이메일 주소가 기업용 이메일인지 검사하는 비즈니스 로직을 포함하고 있습니다.

 

 

클린 아키텍처에서 DTO와 도메인 모델 분리의 필요성

테스트 용이성 및 유지 보수성 등 많은 이유가 있지만 가장 큰 이유는 클린 아키텍처의 본질과 같은 관심사의 분리 때문입니다.

관심사의 분리 (Separation of Concerns)

  • DTO는 외부 시스템(API, 데이터베이스, 네트워크 등)과의 데이터 교환을 위해 존재합니다. 외부 응답 형식, JSON 스키마, 데이터베이스 레코드 구조 등을 그대로 반영하는 경우가 많습니다.
  • 도메인 모델은 애플리케이션의 핵심 비즈니스 로직과 규칙을 캡슐화하는 객체입니다. 도메인 모델은 외부 시스템의 구현 세부사항과 무관하게, 순수한 비즈니스 요구사항만 반영해야 합니다.
  • 이를 분리함으로써, 외부 시스템의 변경(예: API 스펙 변경)이 도메인 로직에 직접적인 영향을 주지 않도록 할 수 있습니다.

 

매핑 예시

struct UserDTO: Decodable {
    let id: Int
    let name: String
    let email: String
}

// 도메인 모델 (Domain Layer)
struct User {
    let id: Int
    let name: String
    let email: String
    
    func isCorporateEmail() -> Bool {
        return email.hasSuffix("@company.com")
    }
}
//DTO
struct UserDTO: Decodable {
    let id: Int
    let name: String
    let email: String
}
// DTO → 도메인 모델 변환
extension UserDTO {
    func toDomain() -> User {
        return User(id: id, name: name, email: email)
    }
}

// Repository 예시 -> Domain Model
protocol UserRepository {
    func fetchUser(with id: Int) -> Single<User>
}

final class UserRepositoryImpl: UserRepository {
    func fetchUser(with id: Int) -> Single<User> {
        return apiService.getUser(id: id)
            .map { (response: UserDTO) in
                return response.toDomain()
            }
    }
}

 

결론

  도메인 모델 DTO
역할 비즈니스 로직 포함 API에서 받은 데이터의 전달(Data Carrier
위치  Domain Layer Data Layer
데이터 구조 앱 내부에서 최적화 된 상태 API 응답 형식 그대로 유지
비즈니스 로직 포함 여부 있음 없음
API 응답 변경 시 영향 없음 있음

 

 

참조

https://medium.com/@tushar.sharma0214/mastering-clean-architecture-in-ios-creating-dto-models-and-integrating-api-calls-400df76cfcdf

https://github.com/kudoleh/iOS-Clean-Architecture-MVVM/blob/master/ExampleMVVM/Data/Network/DataMapping/MoviesResponseDTO%2BMapping.swift#L49