안드로이드 아키텍처 용어 사전
아키텍처Clean Architecture · 의존성 역전 · 도메인 모델

클린 아키텍처

의존성 화살표가 항상 안쪽(도메인)을 향하게 하는 구조. 격리를 얻고 보일러플레이트를 낸다.

의존성이 항상 안쪽을 향하게 만드는 구조.

        ┌──────────────────────────┐
        │  Presentation (UI)       │
        │  ┌───────────────────┐  │
        │  │  Domain           │   │  ← 아무것도 의존하지 않는다
        │  │  Entity · UseCase │   │
        │  └───────────────────┘  │
        │  Data (Repository 구현)  │
        └──────────────────────────┘
            화살표는 전부 안쪽으로

어떻게 화살표를 뒤집나

// domain 모듈 — 인터페이스를 '도메인이' 소유한다
package com.example.domain
interface UserRepository { suspend fun getUser(id: Long): User }

// data 모듈 — 도메인의 인터페이스를 구현한다
package com.example.data
class UserRepositoryImpl @Inject constructor(
    private val api: UserApi,
    private val dao: UserDao
) : UserRepository { ... }

인터페이스를 어느 쪽이 소유하느냐가 전부다. 도메인이 가지면 화살표가 뒤집힌다.

모델이 계층마다 다른 이유

  • UserDto — 네트워크 응답 그대로 (서버 스키마에 묶인다)
  • UserEntity — Room 테이블 (@Entity 애노테이션 · DB 스키마에 묶인다)
  • User — 도메인 모델 (순수 코틀린. 아무것도 안 묻는다)
  • UserUiModel — 화면 표시용 (포맷된 날짜 · 표시 문자열)
// Mapper 가 경계에서 변환한다
fun UserDto.toDomain() = User(id, name, email)
fun User.toEntity() = UserEntity(id, name, email)

왜 나누나 서버가 필드를 바꿔도 도메인이 안 바뀐다 DB 스키마를 바꿔도 도메인이 안 바뀐다

  • 변경의 파급을 경계에서 끊는다

트레이드오프를 정직하게

얻는 것

  • 도메인 테스트에 안드로이드도 DB 도 필요 없다 (순수 JUnit)
  • 프레임워크·라이브러리를 바꿔도 도메인이 산다
  • 모듈 경계가 명확해 팀이 나눠 일하기 좋다

내는 것

  • 클래스와 매퍼 코드가 2~3배로 는다
  • 단순 CRUD 화면 하나에 파일이 6개 생긴다
  • 팀이 규칙을 이해하지 못하면 형식만 남는다

작은 앱에서는 과설계다 — 도메인 규칙이 얇으면 3계층으로 충분하다. "공식 가이드가 클린 아키텍처를 강제하지 않는다"는 점을 기억한다.

면접 함정

  • "클린 아키텍처가 안드로이드 표준" → 공식 권장은 3계층이다. 클린은 그보다 엄격한 선택지다.
  • "모델을 나누면 무조건 좋다" → 필드가 같은 매퍼가 수십 개면 비용만 남는다.

어디까지 나눌지 — 실무의 절충

  • 흔한 절충 1 — DTO 와 Entity 만 나누고 도메인 모델은 Entity 를 재사용

    • 매퍼가 절반으로 준다. Room 애노테이션이 도메인에 남는 것을 감수
  • 흔한 절충 2 — 도메인 모델은 두되 UiModel 은 안 만든다

    • 포맷팅은 Composable 안에서 한다
  • 흔한 절충 3 — UseCase 는 조합이 필요한 곳에만

    • 단순 위임은 ViewModel 이 Repository 를 직접 부른다

어느 쪽이든 '왜 그렇게 했는지' 를 말할 수 있으면 된다 면접에서 나쁜 답은 절충 자체가 아니라 "원래 그렇게 배웠어요" 다

테스트가 실제로 어떻게 쉬워지나

// 도메인 테스트 — 안드로이드도 DB 도 네트워크도 없다. 순수 JUnit
class GetUserFeedUseCaseTest {
    private val userRepo = FakeUserRepository()
    private val postRepo = FakePostRepository()
    private val useCase = GetUserFeedUseCase(userRepo, postRepo)

    @Test fun `차단된 글은 피드에서 제외된다`() = runTest {
        postRepo.setPosts(listOf(post(id = 1, blocked = true), post(id = 2)))
        val feed = useCase(userId = 1)
        assertEquals(listOf(2L), feed.posts.map { it.id })
    }
}

이 테스트는 밀리초 단위로 돈다 Robolectric 도 에뮬레이터도 필요 없다 — 이게 격리의 실제 이득이다

함께 보면 좋은 용어

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