
kotlin으로 개발을 하는 사람이라면 sealed interface나 sealed class를 많이 써봤을 것이다. 단지 상속 가능한 타입을 컴파일 타임에서 알 수 있다는 장점만으로도 충분히 많이 잘 사용하는 거라고 생각한다. Class Discriminator를 사용해서 좋았던 경험을 적어볼려고 한다.
sealed는 상속 가능한 타입의 집합을 컴파일 타임에서 고정한다는 키워드이다.
우선 when을 사용할 때 exhaustive check가 가능하다. 새로운 하위 타입이 추가되면 컴파일 에러가 나서 빠트린 곳을 찾기 쉽다.
이건 sealed만 붙었지 사실 interface vs class라고 봐도 무방하다.
sealed class를 써야될 때는 공통 프로퍼티를 생성자로 강제할 때 주로 사용하는 것 같다.
사실 그외에선 sealed interface를 사용하는게 맞는 것 같다.
우선 sealed interface는 다중구현이 가능하고 공통 프로퍼티를 유연하게 상속하여 사용할 수 있어서 범용성 있게 사용할 수 있기 때문이다.
타입 판별자라는 뜻이다.
kotlinx.serialization는 다형적 타입을 직렬화/역직렬화 할때 discriminator라는 특별한 필드를 사용한다.
서버에서 내려준 값들을 역직렬화 할 때 보통 data class에 @Serializable 조합으로 많이 사용한다. 나도 그렇게 사용해왔다. 근데 작업을 하다보면 하나의 API인데 타입에 따라 값이 다른 경우가 있다. 그런 경우에서 data class를 정의할 때 타입에 따라 모든 값을 고려하여 필드를 정의해준 경험이 있다면 내가 알려주는 방식을 사용하면 좀 더 깔끔하게 타입을 나눌 수 있을 것이다.
우선 예를 들어보면 SNS관리하는 API가 있다고 가정하자. 각 SNS별로 화면에 필요한 값들이 공통적으로 있는데 몇개는 각 SNS별로 필드가 다른 상황이다. 그래서 각 사용자가 연결된 SNS에 따라 값들이 달라지는데 그걸 모두 정의했다고 생각해보자
@Serializable
data class SNSDetailResponse(
// 공통
val type : SNS,
val title : String,
val content : String,
val name : String,
// naver
val todayCount : Int?,
// instargram
val followerCount : Int?,
// youtube
val subscriber : Int?,
)
@Serializable
enum class SNS {
@SerialName("naver")
Naver,
@SerialName("instagram")
Instagram,
@SerialName("youtube")
Youtube
}
보통 이렇게 정의를 하여 공통인 부분들은 non nullable로 하고 타입마다 다르게 내려주는 값들은 nullable로 하여 역직렬화시 크래시를 방지해준다.
sealed interface SNSDetailResponse {
val title : String
val content : String
val name : String
@Serializable
@SerialName("naver")
data class Naver(
override val title: String,
override val content: String,
override val name: String,
val today : Int
) : SNSDetailResponse
@Serializable
@SerialName("instagram")
data class Naver(
override val title: String,
override val content: String,
override val name: String,
val followerCount : Int
) : SNSDetailResponse
@Serializable
@SerialName("youtube")
data class Naver(
override val title: String,
override val content: String,
override val name: String,
val subscriber : Int
) : SNSDetailResponse
}
이렇게 정의하면 끝난다! 지금처럼 한개만 별도 필드를 가지고 있을 때는 모든 필드를 정의하는 게 더 간단하게 보일지라도 각 SNS별로 각 필드가 많아진다면 당연히 가독성이 떨어질 것 이다.
@SerialName에서 타입을 정의해주면 디폴트가 type에 따라 나눠주기 때문에 서버에서 type으로 내려주면 된다.
{
"type": "instagram",
"title": "오늘의 일상",
"content": "맛집 다녀왔어요",
"name": "김철수",
"hashtags": ["맛집", "일상"],
"imageUrl": "https://..."
}
하지만 다른 값으로 판별을 하고 싶다면
val json = Json {
classDiscriminator = "name"
}
json을 생성할 때 바꿔주면 된다. 서버와 협의하여 편한걸로 하는게 좋을 것 같다.
기존에는 모든 필드를 고려했다면 저렇게 타입을 나눠서 분리하니깐 가독성이 좋아졌다.
물론 초기 생성할 때는 번거로울 수 있지만 나중에 유지보수를 할 때에는 도움이 많이 될 것 같다.