UI Model
을 도입한 후 제안을 하였고 팀원들과 상의하여 우리의 코딩 컨벤션에 항목을 추가했다.
그것은 Data Layer
에서의 DTO
과 Domain Layer
의 Entity
는 nullable하게, UI Model
에 대해서는 not-null 형태로 관리하자는 것이였고 이에 대한 이유와 관리하기 위해 만든 간단한 확장함수를 소개해보고자 한다.
/** data source **/
@GET(ApiVersion.VERSION + "/store/{memberIdx}")
suspend fun getStoreProfileInfo(@Path("memberIdx") memberIdx: String): StoreProfileInfoResponse
data class StoreProfileInfoResponse(
val memberIdx: String?,
val isFollowing: Boolean?,
val followerCount: Int?
)
Data Layer
는 데이터 소스와 직접적으로 상호작용하고, 여기에서 Model
은 데이터 구조를 반영한다.
데이터 소스로부터 가져오는 데이터는 완전하지 않거나 선택적인 필드를 포함할 수 도 있고, 특히 외부 시스템과의 통신을 하는 것이므로 데이터의 불확실성을 관리해야 한다.
그러므로 nullable하게 관리를 해야 한다고 생각했다.
/** repository **/
suspend fun getStoreProfileInfo(memberIdx: String) : Resource<StoreProfileInfoEntity>
data class StoreProfileInfoEntity(
val memberIdx: String?,
val isFollowing: Boolean?,
val followerCount: Int?
)
Domain Layer
는 애플리케이션의 비즈니스 로직을 포함하며, Entity
는 순수한 비즈니스 규칙을 표현한다.
이 레이어에서는 Data Layer
에서 가져온 즉, 데이터베이스나 네트워크로부터 가져온 데이터를 다루게 된다.
이 또한 불확실성을 가진 Data Layer
의 DTO
을 다루기에 항상 완벽하게 형성될 수 없다.
그러므로 nullable하게 처리하고, 비즈니스 로직에서 이러한 상황을 유연하게 처리할 수 있다.
data class StoreProfileInfoUiModel(
val memberIdx: String = "",
val isFollowing: Boolean = false,
val followerCount: Int = 0
)
UI Layer
는 사용자 인터페이스와 관련된 로직을 담당하고, UI Model
은 화면에서 표시될 데이터를 정의한다.
사용자에게 표시되는 데이터는 nullable 상태를 직접 다루기보다는 안정적인 상태로 처리되어야 한다고 생각한다.
왜냐하면 안정적인 상태에서는 null exception
등의 가능성을 줄여 사용자 경험을 향상시키기 때문이다.
그러므로 not-null로 관리를 하여 사용자 인터페이스가 안정적이게 처리할 수 있다.
이렇게 not-null로 관리하니 오류 가능성도 줄이면서 가독성 또한 향상이 되었다.
위의 예시에서는 편의상 프로퍼티들을 축소했지만, 기존에 있는 프로퍼티 한 개를 추가해보겠다.
다음과 같이 리스트에 MemberInfoUiModel
들이 담긴 Follwers
가 추가되었다.
data class StoreProfileInfoUiModel(
val memberIdx: String = "",
val isFollowing: Boolean = false,
val followerCount: Int = 0,
val followers: List<MemberInfoUiModel> = emptyList()
)
data class MemberInfoUiModel(
val memberIdx: String = ""
)
followers
가 nullable이고 MemberInfoUiModel
도 nullable인 상태라고 보자.
data class StoreProfileInfoUiModel(
val memberIdx: String? = "",
val isFollowing: Boolean? = false,
val followerCount: Int? = 0,
val followers: List<MemberInfoUiModel?>? = emptyList()
)
data class MemberInfoUiModel(
val nickName: String? = ""
)
이 때 팔로워 리스트의 첫번째 Member
의 nickName
을 TextView
에 보여주는 코드를 작성해보겠다.
// nullable
val storeProfileInfo = storeInfo?.followers?.firstOrNull()
textNickName.text = storeProfileInfo?.nickName.orEmpty()
// non-null
val storeProfileInfo = storeInfo.followers.first()
textNickName.text = storeProfileInfo.nickName
nullable 코드의 경우는 매번 프로퍼티에 접근 할 때 마다 null safety 코드를 작성해야 하고 가독성도 그리 좋지 않다.
하지만 not-null의 경우는 현저하게 코드도 줄어들었고 가독성도 향상된 것을 볼 수 있다.
(작성해놓고 보니.. 코드의 양이 적어서 그런지 드라마틱하게 바뀐 것 같지는 않지만,, 또,,, 코드를 작성할 때 매번 ?.을 붙이는 것도 귀찮기도 하다.)
위와 같은 생각으로 UI Model
을 not-null로 처리하기로 하였고 또 다른 개선점을 찾았다.
list
와 String
같은 경우에는 orEmpty()
라는 null 처리를 할 수 있는 메서드가 있다.
하지만 다른 Int
, Double
, Boolean
등과 같은 Primitive
타입에는 null 처리에 대한 메서드가 따로 존재하진 않고 엘비스 연산자(?:)를 통해 처리를 해야한다.
위 스크린샷은 Entity
에서 UI Model
로의 Mapper
메서드 내용의 일부분이다.
String
형은 orEmpty()
메서드가 존재하기에 Nullable한 프로퍼티에서 not-null 프로퍼티로의 매핑 과정에서 해당 메서드를 사용하였지만, 다른 Primitive
타입에서는 엘비스 연산자를 사용하여 코드의 일관성이 떨어지는 모습을 볼 수 있다.
또 어떤 팀원은 일관성을 맞추기 위해 String
도 엘비스 연산자를 사용하여 처리하는 경우도 있었다.
그래서 어떠한 컨벤션이 필요하다고 생각했고, String
도 엘비스 연산자를 사용하여 일관성을 유지하는 방법도 있었지만 난 다른 방법을 제시하였다.
엘비스 연산자로 처리하는 방법도 나쁜 방법은 아니지만 가독성이 좋지 않다는 것을 느꼈다.
프로퍼티가 많을 경우 엘비스 연산자로 인해 코드가 어지러운 감이 있고, Syntax Highlighting도 적용이 되지 않아 가독성이 저하되는 것을 볼 수 있다.
그래서 String
의 orEmpty()
처럼 각 Primitive
타입의 확장함수를 만들어서 관리하기로 하였다.
fun Int?.orDefault(default: Int = 0) = this ?: default
fun Boolean?.toFalse(): Boolean = this ?: false
이렇게 관리를 하고 컨벤션을 정해놓으니 코드의 일관성도 가져갈 수 있었고, Syntax Highlighting를 통한 가독성도 훨씬 향상 시킬 수 있었다.