본문 바로가기

Android

클린 아키텍처와 데이터 모델들

클린 아키텍처에 관한 글을 읽어보면, 크게 3가지의 레이어층으로 구분하여 코드를 작성하는 것을 권장하고 있다.

  • Presentation Layer
    • 사용자에게 정보를 표현해주는 일을 담당한다.
    • 사용자의 입력을 받아들이는 일을 담당한다.
  • Domain Layer
    • 데이터 요청의 흐름을 관리한다.
    • 핵심 비즈니스 로직을 수행한다.
  • Data Layer
    • 데이터를 Persistence하게 저장하는 것을 담당한다.

각 레이어 별로 Separation of concerns 규칙을 적용하여, 책임 범위를 확실히 나눈 뒤 경계를 지켜가면서 구현을 해 나가는 것이다.

 

Presentation 계층은 사용자와 가장 가까운 계층으로 사용자에게 데이터를 보여주고, 사용자 입력을 받는 역할 이외에는 관여하지 않는다.

Domain 계층은 애플리케이션의 주요한 로직을 담당하는 곳으로, 데이터 계층에게 정보를 요청하여 프리젠테이션에게 전달하거나, 애플리케이션의 고유 기능을 위한 로직처리가 필요한 경우 이 레이어에서 진행되어야한다.

Data 계층은 데이터를 장기적으로 보관하는 것을 담당하는 곳으로 도메인 계층에서 처리한 결과물을 저장하거나 요청 받은 정보를 꺼내어 전달하는 것을 담당한다.

 

여기서 자주 간과되는 점은 각 레이어마다 사용하는 데이터 모델이 서로 다르다는 점이다.

데이터 모델을 서로 다르게 사용해야하는 이유

각 레이어마다 서로 다른 데이터 모델을 사용 해야하는 이유를 알아보자.

서버에서 "사용자 정보"를 가져와서 보여주는 기능을 구현한다고 가정해본다.

// User.kt
data class User(
    val name: String,
    val address: String,
    val age: Int
)

데이터를 가져오는 곳은 _데이터 계층_이다. 데이터 계층에서 위 User클래스에 데이터 꺼낸 후 도메인 계층으로 전달할 것이다.

 

그리고 도메인에서 사용자 정보를 이용해 로직을 처리하고, 프리젠테이션 계층으로 전달하면 마침내 사용자에게 UI로 그려져 정보가 제공될 것이다.

 

한달 뒤, 서버에서 전달하는 User 클래스의 스펙이 변경되었다고 생각보자. 서버는 애플리케이션과 완전히 별개의 프로그램으로 데이터의 전송스펙이 언제든지 바뀔 가능성이 존재한다.

// 변경된 User data spec
data class User(
    val name: String,
    val address: Address,
    val age: String,
    val phone: String
)

이제 서버가 보내는 데이터의 스펙에 따라서 애플리케이션의 사용자 정보 처리 로직도 변경되어야할 것이다.

하지만 우리는 첫번째 User 클래스를 모든 계층에서 다함께 공유하여 사용하였다.

수정해야할 곳이 Presentation, Domain, Data 계층에 걸쳐있는 상황이 되는 것이다.

 

별개의 데이터 모델 사용하기

이번엔 계층 별로 각자 사용하는 데이터 모델을 갖는 경우를 생각해본다.

data class UserNetworkDto(
    val name: String,
    val address: Address,
    val age: String,
    val phone: String
)

보통 외부의 서버에서 가져오는 데이터를 담는 모델은 Dto, NetworkDto라는 네이밍을 사용한다.

DTO(Data Transfer Object)의 약자로 데이터 전달을 위해 사용하는 객체라는 뜻이다.

서버에서 데이터를 가져오는데 성공하면, 도메인 계층으로 사용자 데이터를 전달한다.

이때 도메인은 UserNetworkDto를 바로 사용하지 않아야한다.

 

data class UserDomain(
    val name: String,
    val address: Address,
    val age: String,
    val phone: String
)

도메인은 UserNetworkDto에 관한 어떠한 정보도 가지고 있지 않는 것이 좋다.

 

즉, 데이터 계층 -> 도메인 계층으로 데이터 전달이 이뤄질 때, NetworkDto -> DomainModel로의 변환이 이루어져야한다는 얘기이다.

 

서로 다른 레이어를 건너갈때는 데이터 매퍼(Data Mapper)를 통해서 각 레이어에 맞는 모델로 데이터를 변환하여 전달하여아한다.

 

이렇게 도메인 계층에서 NetworkDto에 의존성을 갖지 않게 분리시키면 서버의 업데이트에 따라서 NetworkDto가 변경된 상황에서도 Data Layer에서만 수정을 하면 애플리케이션을 정상작동 시킬 수 있게 된다.

서버의 업데이트에 도메인 레이어가 영향을 받지 않게 되는 것이다.

 

하지만 위에서 보여준 User 데이터는 Domain 모델과 NetworkDto가 100% 동일한 데이터를 담고 있는 경우이다.

그래서 UserMapper를 작성한다면 아래와 같이 될 것이다.

class UserMapper {
    fun toUserDomain(userDto: UserNetworkDto) : UserDomain {
        return UserDomain(
            userDto.name,
            userDto.address,
            userDto.age,
            userDto.phone
        )
    }

    fun toUserDto(userDomain: UserDomain) : UserNetworkDto {
        return UserNetworkDto(
            userDomain.name,
            userDomain.address,
            userDomain.age,
            userDomain.phone
        )
    }
}

결과적으로 UserMapper가 하는 것은 동일한 사용자 데이터의 껍데기만 바꾼 것이다. 어떤 사람들은 이러한 클래스를 아무런 일도 하지 않는 쓸모없는 코드라고 얘기할 수도 있을 것이다.

 

기능적으로는 유용한 작업을 하지 않는 코드이지만, 레이어를 나누는 추상적인 계층을 구분 짓는 용도로써는 아주 중요한 기능을 담당하고 있다고 볼 수 있다.

 

UserMapper가 존재함으로써, 도메인 계층은 더 이상 UserDomain이라는 자료형 말고는 고려하지 않아도 된다.

데이터와 도메인 계층의 작업자가 서로 다른 사람이라면, 위와 같은 경계선은 충분히 그 역할을 다 할 것이다.

 

게다가 글 앞부분에서 들었던 예와 같이 서버의 데이터 스펙 전달이 바뀐상황이라면 도메인 계층은 수정하지 않아도 된다.

UserNetworkDto를 변경된 스펙에 맞게 고친 후, UserNetworkDto -> UserDomain으로 변환하는 것만 수정하면 앱은 정상적으로 작동할 것이다.

 

정리

  • 클린 아키텍처 구조를 따라가려면 크게 3가지 레이어로 기능을 구분지어서 구현하게 된다. (Presentation, Domain, Data)
  • 각 계층에서 사용하는 데이터 모델은 계층마다 존재해야한다.
  • 데이터 계층에서도 여러가지 데이터 출처(e.g. database, server, ...)가 존재한다면 각 데이터 출처마다 모델을 구분짓는 것이 좋다.
  • 서로 다른 계층에서 사용하는 모델들이 100% 일치하는 경우에도 구분 짓는 것이 더 유용할 수 있다.

이러한 모델들 간의 데이터 매핑과정을 잉여과정으로 보고, 동일한 모델을 여러 계층에서 공유하여 사용하는 케이스도 많이 볼 수 있다. 대표적으로 도메인 계층과 도메인 계층 간의 데이터는 유사할 가능성이 높고, 함께 데이터 모델을 공유하는 케이스가 많이 존재한다.

 

하지만 다른 모델과 다르게 NetworkDto는 서버의 스펙을 따라가는 자료형이므로, 애플리케이션의 의지와 상관없이 변경이 발생할 수 있는 데이터 모델이다. 그럼으로 NetworkDto는 반드시 별도의 자료형을 두어 사용하는 것이 적극 권장된다.

 

참고사이트