UI Layer을 위한 새로운 코딩 컨벤션을 구축했던 이유

송규빈·2024년 2월 15일
1
post-thumbnail

개요

UI Model을 도입한 후 제안을 하였고 팀원들과 상의하여 우리의 코딩 컨벤션에 항목을 추가했다.

그것은 Data Layer에서의 DTODomain LayerEntitynullable하게, UI Model에 대해서는 not-null 형태로 관리하자는 것이였고 이에 대한 이유와 관리하기 위해 만든 간단한 확장함수를 소개해보고자 한다.

레이어의 책임과 null 타입 관리에 대한 내 생각

Data Layer의 DTO

/** 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하게 관리를 해야 한다고 생각했다.

Domain Layer의 Entity

/** 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 LayerDTO을 다루기에 항상 완벽하게 형성될 수 없다.
그러므로 nullable하게 처리하고, 비즈니스 로직에서 이러한 상황을 유연하게 처리할 수 있다.

UI Layer의 UI Model

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? = ""
 )

이 때 팔로워 리스트의 첫번째 MembernickNameTextView에 보여주는 코드를 작성해보겠다.

// 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의 경우는 현저하게 코드도 줄어들었고 가독성도 향상된 것을 볼 수 있다.

(작성해놓고 보니.. 코드의 양이 적어서 그런지 드라마틱하게 바뀐 것 같지는 않지만,, 또,,, 코드를 작성할 때 매번 ?.을 붙이는 것도 귀찮기도 하다.)

추가했던 코딩 컨벤션 orDefault(), orFalse() . .

위와 같은 생각으로 UI Model을 not-null로 처리하기로 하였고 또 다른 개선점을 찾았다.

코드의 일관성 문제

listString 같은 경우에는 orEmpty()라는 null 처리를 할 수 있는 메서드가 있다.

하지만 다른 Int, Double, Boolean 등과 같은 Primitive 타입에는 null 처리에 대한 메서드가 따로 존재하진 않고 엘비스 연산자(?:)를 통해 처리를 해야한다.

위 스크린샷은 Entity에서 UI Model로의 Mapper 메서드 내용의 일부분이다.

String형은 orEmpty() 메서드가 존재하기에 Nullable한 프로퍼티에서 not-null 프로퍼티로의 매핑 과정에서 해당 메서드를 사용하였지만, 다른 Primitive 타입에서는 엘비스 연산자를 사용하여 코드의 일관성이 떨어지는 모습을 볼 수 있다.

또 어떤 팀원은 일관성을 맞추기 위해 String도 엘비스 연산자를 사용하여 처리하는 경우도 있었다.

그래서 어떠한 컨벤션이 필요하다고 생각했고, String도 엘비스 연산자를 사용하여 일관성을 유지하는 방법도 있었지만 난 다른 방법을 제시하였다.

가독성을 겸비한 해결책 Syntax Highlighting

엘비스 연산자로 처리하는 방법도 나쁜 방법은 아니지만 가독성이 좋지 않다는 것을 느꼈다.

프로퍼티가 많을 경우 엘비스 연산자로 인해 코드가 어지러운 감이 있고, Syntax Highlighting도 적용이 되지 않아 가독성이 저하되는 것을 볼 수 있다.

그래서 StringorEmpty()처럼 각 Primitive 타입의 확장함수를 만들어서 관리하기로 하였다.

fun Int?.orDefault(default: Int = 0) = this ?: default
fun Boolean?.toFalse(): Boolean = this ?: false


이렇게 관리를 하고 컨벤션을 정해놓으니 코드의 일관성도 가져갈 수 있었고, Syntax Highlighting를 통한 가독성도 훨씬 향상 시킬 수 있었다.

profile
🚀 상상을 좋아하는 개발자

0개의 댓글