우선 DTO, Entity, Model(Domain, UI) 다 데이터를 표현하는 구조적 클래스 아니야?
-> 맞다!
그런데 데이터를 담는다고 다 똑같은 게 아니다. 다르다. 다르기 때문에 어떤 데이터 구조에 어떤 이름을 쓰는지 명확히 알아야 한다.
일단 이렇게 구분해서 쓰는 이유는 책임을 분리하기 위해서다.(SRP 원칙)
| 변경 발생 | 영향을 받는 구조 |
| 서버 Response/Request 변경 | DTO |
| DB 스키마 변경 | Entity |
| 비즈니스 규칙 변경 | Domain Model |
| UI 요구 변경 | UI Model |
하나 바뀌었을 때 다른 레이어에 영향이 없어진다.
때문에, 클린아키텍처 공식 가이드에서 권장하고 있다.
두번째 이유로는 의존성 방향 규칙을 지키기 위해서다
Domain → Data는 의존 가능
Data → Domain은 의존 불가 (역참조 금지)
DTO (Remote/Data Layer)
↓
Entity (Local/Data Layer)
↓
Domain Model (Domain Layer)
↓
UI Model (Presentation Layer)
상위 계층이 하위 계층에 의존하지 않도록 함 → 유지보수성 상승.
이렇게 나눠 두면 테스트 또한 쉬워진다.
DTO(Data Transfer Object)
DTO의 영어를 해석하면 데이터 전송 객체라는 뜻이다.
- Data = 데이터
- Transfer = 전송하다, 옮기다
- Object = 객체
시스템 간 데이터 전달 용도로 만들어진 객체다.
- 서버 ↔ 앱 사이에서 Request/Response를 담는다.
- Retrofit, Ktor 등 네트워크 계층에서 사용.
예시
data class UserDto(
val id: String,
val name: String,
val age: Int
)
필드는 API 응답 필드와 거의 동일하다.(가공 금지)
DTO 필드는 API 응답과 동일해야 하는데 왜 거의 동일이라고 했냐면 원칙적으로는 DTO는 서버 응답과 1:1 매핑되는 것이 맞지만 실무에서 다음과 같은 예외가 존재할 수 있기 때문이다.
1) nullable → non-null 변환
서버는 null 보낼 수 있지만
앱은 null을 원하지 않아 default 값을 넣을 수 있음.
2) nested JSON 구조 단순화 (Flattening)
이건 실무에서 흔함.
예:
{
"user": {
"id": "123",
"profile": {
"name": "Tom",
"age": 20
}
}
}
원래 DTO:
data class UserDto(
val id: String,
val profile: ProfileDto
)
하지만 너무 깊으면 → 아래처럼 flatten:
data class UserDto(
val id: String,
val name: String,
val age: Int
)
3) snake_case → camelCase 변환
서버는 snake_case
앱은 보통 camelCase
@Json(name = "user_name")
val userName: String
동일한 데이터지만 “필드 이름”은 달라짐.
Entity
- 로컬 DB(Table)와 연결된 데이터 구조
- Room, SQLite 스키마에 직접 매핑됨
- DB가 바뀌면 Entity도 반드시 바뀜
예시
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: String,
val name: String,
val age: Int
)
Model (Domain Model)
- 앱의 비즈니스 로직을 표현하는 핵심 데이터 구조
- 앱이 “의미 있게” 사용하는 형태로 가공
- DTO/Entity와 구조가 달라도 됨
- Domain Layer 내부에서만 사용되는 순수 구조체
- Clean Architecture 공식에서 Domain Model을 가장 중요한 레이어로 봄
- ViewModel/UseCase 등 비즈니스 계층에서 사용
예시
data class User(
val id: String,
val displayName: String,
val isAdult: Boolean
)
UI Model (ViewState)
- 화면에 필요한 데이터만 모아둔 구조
- Domain Model과 다를 수 있음
- UI 상태를 표현하기 위해 사용
data class UserUiModel(
val name: String,
val ageText: String,
val showAdultBadge: Boolean
)
👉 결론적으로 Entity·DTO는 있는 그대로, Model은 앱이 실제로 쓰기 좋게 가공된 형태이다.
개념 목적 특징
| 개념 | 목적 | 특징 |
| DTO | 서버 ↔ 앱 데이터 전달 | API 응답/요청 그대로, 가공 없음 |
| Entity | DB 저장 | Room 등 DB 스키마 기반 |
| Model (Domain) | 앱 내부 핵심 로직 | 앱에서 쓰기 좋은 형태, 가공·로직 가능 |
| Model(UI) | UI가 바로 사용할 수 있는 형태로 데이터 표현 | 화면 전용 구조, 가공된 값 가능, View의 상태를 안정적으로 관리 |
📌 실제 흐름 예시
서버 응답(JSON) → DTO → Model → UI 표시
로컬 저장(DB) ←→ Entity ←→ Model
fun UserDto.toModel() = User(
id = id,
displayName = name,
isAdult = age >= 20
)
fun UserEntity.toModel() = User(
id = id,
displayName = name,
isAdult = age >= 20
)
- DTO → Model 변환 = Domain Layer
- Entity → Model 변환 = Domain Layer
model로 변환하기 위해서는 Mapper는 Domain Layer에 둬야 한다.
data
├── dto
├── entity
└── repositoryImpl
domain
├── model ← 순수 데이터 구조
├── mapper ← 변환 책임
└── usecase ← 비즈니스 규칙
presentation
└── viewmodel
네이밍 컨벤션
DTO
- 반드시 Dto suffix 사용
UserDto, LoginResponseDto, OrderRequestDto
Entity
- Entity suffix
UserEntity, OrderEntity
Domain Model
- 아무 suffix 안 붙임
User, Order, Store
UI Model(ViewState)
- UserUiModel or UserViewState
- UiModel, UiState, ViewState
UserUiModel, OrderViewState
이 네이밍 방식은 Android·Spring·Clean Architecture에서 공통적으로 채택되는 방식이다.
'android' 카테고리의 다른 글
| Module 생성하기 (0) | 2026.01.11 |
|---|---|
| [프로그래머스] A로 B 만들기 (0) | 2025.12.22 |
| [Compose] Recomposition (1) | 2025.07.13 |
| 딥링크란? (5) | 2025.06.02 |
| CoroutineDispatcher (0) | 2025.03.23 |