안드로이드 아키텍처 용어 사전
아키텍처매퍼

Mapper

계층 경계에서 모델을 변환하는 함수. 스키마 변경의 파급을 경계에서 끊는다.

계층 경계에서 모델을 변환하는 함수.

왜 모델이 계층마다 다른가

  • UserDto — 서버 응답 그대로. created_at 이 "2026-08-15T09:00:00Z" 문자열
  • UserEntity — Room 테이블. @PrimaryKey · 인덱스 · 컬럼명이 붙는다
  • User — 도메인. createdAt 이 Instant. 아무 프레임워크도 안 묻는다
  • UserUiModel — 화면용. createdAt 이 "3시간 전" 문자열
fun UserDto.toDomain() = User(
    id = id,
    name = name.trim(),
    createdAt = Instant.parse(createdAt)          // 파싱 실패 책임이 경계에 모인다
)

fun User.toEntity() = UserEntity(id, name, createdAt.toEpochMilli())
fun UserEntity.toDomain() = User(id, name, Instant.ofEpochMilli(createdAtMillis))

무엇을 얻나

서버가 name → full_name 으로 바꿨다
  매퍼가 있다  → toDomain() 한 줄만 고친다. 나머지 전부 그대로
  매퍼가 없다  → 도메인·UI·테스트 전부에서 name 을 찾아 고친다

서버가 필드를 추가했다

  • DTO 에만 추가하고 매퍼에서 무시하면 끝이다

어디에 두나

확장 함수로 data 모듈 안에 둔다 data/mapper/UserMapper.kt

도메인 모듈에 두면 안 된다

  • 도메인이 DTO 를 알게 되고 의존성 규칙이 깨진다
방향도 중요하다
  DTO.toDomain()  ✅ (data 가 domain 을 안다 — 허용된 방향)
  User.toDto()    ✅ (같은 이유로 data 모듈 안)

비용을 정직하게

필드가 똑같은 매퍼가 30개면 순수한 비용이다

  • 그럴 땐 계층을 나누지 않는 편이 낫다

매퍼가 값을 하는 순간

  • 타입이 바뀐다 (String → Instant, Int → enum)
  • 이름이 다르다
  • 검증·기본값·널 처리가 들어간다
  • 여러 DTO 를 하나의 도메인 모델로 합친다

널 처리를 어디서 하나

// DTO 는 서버를 믿지 않는다 — 전부 nullable
data class UserDto(val id: Long?, val name: String?)

// 도메인은 항상 유효하다 — 매퍼가 관문 역할을 한다
fun UserDto.toDomainOrNull(): User? {
    val id = id ?: return null
    val name = name?.takeIf { it.isNotBlank() } ?: return null
    return User(id, name)
}

이 한 겹 덕분에 도메인과 UI 전체에서 user.name?.let { } 이 사라진다.

면접 함정

  • "매퍼는 자동 생성 도구로 만들면 된다" → 필드가 같을 때만이다. 변환 규칙이 있으면 직접 쓴다.
  • "매퍼가 많으면 잘 설계한 것" → 값을 안 하는 매퍼는 그냥 비용이다.

컬렉션 변환을 어디서 하나

fun List<UserDto>.toDomain(): List<User> = mapNotNull { it.toDomainOrNull() }

mapNotNull 을 쓰면 잘못된 항목 하나가 전체를 무너뜨리지 않는다 다만 조용히 사라지므로 개수가 다르면 로그를 남기는 편이 좋다

  • val parsed = dtos.toDomain()
  • if (parsed.size != dtos.size) log.w("유효하지 않은 항목 ${dtos.size - parsed.size}개 제외")

열거형 변환 — 모르는 값이 오면

enum class Status { ACTIVE, SUSPENDED, UNKNOWN }

fun String?.toStatus(): Status = when (this) {
    "active" -> Status.ACTIVE
    "suspended" -> Status.SUSPENDED
    else -> Status.UNKNOWN            // 서버가 새 값을 추가해도 크래시하지 않는다
}
  • @Serializable enum 을 그대로 쓰면
    • 서버가 "deleted" 를 추가한 순간 구버전 앱이 파싱 예외로 죽는다
  • UNKNOWN 을 두는 것이 하위 호환의 기본기다

테스트

@Test fun `널 이름은 도메인으로 변환되지 않는다`() {
    assertNull(UserDto(id = 1, name = null).toDomainOrNull())
}

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Clean Architecture — domain·data·presentation