Architecture: Entities — MAD Skills[번역]

DwEnn
10 min readMay 15, 2022

--

📝 NOTE : 이 글은 영상의 스크립트를 번역한 것이며 오역이 있을 수 있습니다. 😅

안녕하세요. Architecture MAD Skills 로, 분리된 데이터 모델을 생성하는 방법에 관한 팁을 나누고자 합니다. 이 데이터 모델은 여러분의 프로젝트에 다양한 Architecture 계층을 기반으로 합니다. 우리는 이것을 Entities 라고 부르겠습니다.

저는 클린하고 확장 가능한 모델 아키텍처를 위해 프로젝트에서 계층당 하나 이상의 개별 Entities 를 생성할 것을 제안합니다. 그 이유는, 앱의 여러 계층은 서로 다른 책임을 가집니다. 주어진 데이터 유형에 대해 각 계층은 해당 상태를 수행하기 위해 계층별로 명확한 속성들이 필요할 가능성이 높습니다. 이러한 Enities 의 예는 Data Layer의 remote user entity, UI Layer의 user UiState entity 가 있습니다.

여러분의 프로젝트를 가정해보면 App Architecture 가이드에 제시된 것과 같이 위와 같은 계층으로 나뉩니다. 이상적으로는 프로젝트의 각 계층에 하나 이상의 고유한 Entities 가 있어야 합니다.

Separating Entities

제 경험에 근거하여 Entities 를 분리하는 가장 효과적인 방법에 대해 말해보겠습니다.
첫 번째는 Remote Entities 입니다. 이것은 서버 또는 네트워크 API에서 들어오는 json 응답을 표현하는 Entity 입니다.
Database Entities 는, 프로젝트에 데이터베이스가 있는 경우 데이터베이스의 데이터베이스 Entity 를 표현합니다.
Domain Eneities 는, 프로젝트에 Domain Layer 가 있는 경우 비즈니스 모델을 표현합니다.
UiState Entities 는, 각 화면의 상태를 표현합니다.
또한 Entities 간 매핑을 수행하는 Mapper 클래스도 있지만 이것은 선택 사항입니다.

이제 이것을 더 잘 이해하기 위해 Entities 를 사용하는 다양한 이점들에 대해 다뤄보려 합니다. 아래 예제를 보겠습니다.

Remote Entity

data class RemoteUser {
val userId: String,
val username: String,
val token: String,
val inventory: RemoteInventory,
val profile: RemoteProfile
...
)

User 응답을 위해 생성해두었던 Remote Api Entity 의 예제입니다. 고유 id, username, token, inventory, profile 정보를 가지는 User 객체가 있습니다. 이 비디오의 목적을 위해 추가 데이를 생략하였습니다.
Profile 객체의 형태는 아래와 같습니다.

data class RemoteProfile(
val email: String,
val fullName: String,
val gender: String,
@SerializedName("profile_image_url")
val profileImageUrl: String
...
)

서버에서 들어오는 일부 혼합 케이스(예: profileImageUrl 필드)는 카멜케이스 필드 이름에 매핑하기 위해 SerializedName 을 추가합니다.

data class RemoteInventory(
val list: List<RemoteInventoryItem>,
val type: String
...
)
data class RemoteInventoryItem(
val id: String,
val name: String
...
)

위는 Inventory List의 형태입니다. 응답 내에는 각각 id, name 등등을 가지는 Inventory item list 가 있습니다.
User 객체를 새 Entity에 매핑하지 않고 Room 과 같은 데이터베이스에 직접 저장하려는 경우에 데이터베이스 Entity는 아래와 같이 될 것입니다.

Database Entity

@Entity(tableName = "user")
data class RemoteUser (
@PrimaryKey
val userId: String,
val username: String,
@Ignore
val token: String,
@Ignore
val inventory: RemoteInventory,
@Ignore
val profile: RemoteProfile
...
)

token 을 안전한 곳에 저장하고 싶기 때문에, 데이터베이스에 토큰을 저장하지 않는다고 가정해보겠습니다. 그래서 token 에 Ignore Annotation 을 추가하였습니다. 또한 RemoteInventory 객체는 별도의 Room Entity 로 분리될 것이기 때문에 User 객체의 일부로 저장하지 않기 위해 Ignore Annotation 을 추가하였습니다. 그리고 RemoteProfile 은 아직 앱에서 사용되기 않기 때문에, 데이터베이스에 추가하지 않습니다.

이 접근 방식은 데이터베이스 Entity 를 혼란스럽게 하여 혼동스러울 수 있습니다. 그래서 우리는 이러한 경우에 분리된 Entities 를 만들어야 합니다. 변환된 데이터베이스 Entity 는 아래와 같습니다.

@Entity(tableName = "user")
data class UserEntity (
@PrimaryKey
val userId: String,
val username: String,
...
)

우리가 필요로 하는 데이터만을 포함하고 있습니다. 더 깨끗해 보입니다.

Advantage of Data Entities

모든 Entity 에는 필요한 데이터만 포함됩니다. 이렇게 Remote Entity 에 지속할 필요가 없는 추가 객체가 있을 수 있습니다. 이제 데이터베이스 Entity 내에서 Ignore Annotation 을 추가하는 것과는 작별할 수 있습니다.

Domain/Ui Entity

data class Inventory(
val id: String,
val name: String,
...
)

이제 사용자의 Inventory 객체에 대한 Domain Entity 를 생성해보겠습니다. Domain Entity 를 생성하여 비즈니스 규칙을 작성하고 이러한 규칙을 다른 모듈로 분리할 수 있습니다. 다른 모듈은 순수 코틀린 모듈일 수 있죠. 이것은 잠재적으로 build 시간을 개선할 수 있는 이점이 있고, 여러 플랫폼에서 Entities 를 공유할 수 있습니다. 이 단계는 선택 사항이기 때문에 프로젝트 별로 결정될 수 있습니다. 또한 UI Layer 내에서 동일한 Ui Entity 로 동일한 Entity 를 생성할 수도 있습니다.

Sending less data as a parcel

@Parcelize
data class Inventory(
val id: UUID,
val name: String,
...
): Parcelable

이제 한 화면에서 다른 화면으로 Inventory 객체를 전달하고자 합니다. 서버에서 Inventory 아이템의 세부 정보를 가져오려면 id 가 필요합니다. 서버에서 세부 정보를 가져오는 동안 toolbar 에 Inventory 의 name 이 표시되어야 합니다. 데이터를 한 화면에서 다른 화면으로 전달하는 동안 별도의 Domain 또는 UI Entity 를 보유하여, 우리는 각자 원하는 대로 주석을 추가할 수 있고 제한된 데이터만 보낼 수 있으며 과거의 레이블을 대량으로 만들 필요가 없습니다. 전달할 수 있는 데이터의 양에 제한이 있기 때문에 필요한 것만 보내야 합니다.

RecyclerView UiState Wrapper

data class InventoryUiState (
val selected: Boolean,
val item: Inventory
)

이제 UiState Entity 를 만드는 것이 유용한 예제를 보겠습니다. 우리는 사용자 Inventory 를 목록으로 보여주길 원하고 사용자는 그 중 하나의 Inventory 아이템을 선택할 수 있습니다. 이러한 경우 별도의 UiState 를 만드는 것은 다양한 이점을 가집니다. UiState 에는 사용 가능한 Inventory list 의 하나의 Inventory 아이템이 한 번 선택된 이후 selected boolean 을 UiState 에 유지하는 형태의 상태 관련 데이터가 있을 수 있습니다. RecyclerView 리스트 아이템에 대한 UiState 를 갖는 것은 우리가 필요한 데이터들로만 효율적으로 데이터를 구분하는 것에 도움이 될 수 있습니다.

Other Advantages

다른 코드 베이스에서 다른 Entity 를 만드는 것이 어떻게 이점을 가지는지 확인하였습니다. 원격 API 를다른 API 와 교환하고자 할 경우 변경사항이 API 계층으로만 제한되고 다른 모든 사항은 동일하게 유지되기 때문에 앱은 훨씬 더 유지보수가 쉬워집니다. 또한 Ui 변경사항이 있는 경우, Ui Layer만 해당 변경사항에 대해 알면 되기 때문에 나머지는 동일하게 유지됩니다.

Disadvantages

동전의 양면과 같이 많은 장점들이 있는 반면 단점들도 존재합니다. 먼저, 이러한 Entity를 생성하고 유지 관리하면 개발 시간이 늘어날 수 있습니다. 여러 개의 Entity 를 갖는 것은 어떤 Entity를 사용할지 개발자들 사이에 혼란을 야기할 수 있습니다. 항상 다른 Entity 를 전달하기 위해 서로 다른 Entity 간의 매핑이 번거로워질 수도 있습니다. 이러한 계층화는 감당하기 힘들 수 있으며 소규모 프로젝트에 도움이 되지 않을 수 있다는 것을 이해합니다. 하지만 대규모 프로젝트의 경우 계층화는 제 경험에서는 항상 매우 도움이 되었습니다.
위에 설명한 불이익을 완화하기 위하여, 팀 전체가 유사한 naming 컨벤션과 Entity 분리에 대한 규칙을 사용할 수 있도록 팀 내에서 문서화하고 표준화하는 것이 유리할 것입니다. 다른 Entity 들이 다른 이점을 제공하지만, 그것과는 별개로 여러분에게 가장 도움이 될 수 있는 것을 사용하시면 됩니다.

저는 이것들은 여전히 프로젝트에서 분리되어야 하는 최소한의 것들이라고 생각합니다. Data Layer, Remote Entity, (만약 여러분의 프로젝트에 데이터베이스가 있다면) Database Entity 를 추가하거나, UI Layer 의 UiState 또는 (Domain Layer 가 있는 경우) Domain Entity 를 추가하는 것이죠.

이제, 즐겁게 분리를 즐기시길 바랍니다. 클린하고 확장 가능한 Entity 에 대한 저의 조언이었습니다. 여러분이 내용을 즐기고 여러분의 다음 프로젝트에 적용할 수 있기를 바랍니다. 😁

--

--