Kotlin - Sealed class for Restricted Class Hierarchie

WindSekirun (wind.seo)·2022년 4월 26일
0
post-custom-banner

이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.

작성 시점: 2017-09-05

함수형 프로그래밍에는 ADT(Algebriaic Data Type, 대수적 자료형) 이란 개념이 있는데, 데이터 유형이 미리 정의된 유형의 집합 중 하나가 될 수 있게 해주는 것이라고 한다.

대표적인 예로 Boolean 이 있는데, TRUE 이던가 FALSE 만 이루어져 있기 때문이다.

코틀린에는 ADT란 개념이 없지만, Sealed class는 있다. Sealed class는 '어떤 클래스가 어떠한 클래스를 상속할 수 있는지에 대한 제한을 강화할 수 있는 방법' 을 제공한다.

이전 코드

한 가지 상황을 가정해보자.

페이지의 정보를 가져와 뿌려주는 역할을 하는 코드를 짜본다고 해보자. 그러면, 필요한 것은 두 개 정도 일 것이다.

하나는 페이지에 대한 결과를 리턴해주는 클래스, 다른 하나는 해당 기능을 처리하는 코드이다.

클래스의 이름을 PageResult라 짓고, result (String)isError (Boolean) 을 파라미터로 갖게 하자.

class PageResult(val result: String, var isError: Boolean)

메소드의 이름은 getURLPage로, url를 파라미터로 받으면 PageResult를 리턴하게 한다.

메소드 안에서 유효한 작업이면 PageResult를 "The Result", false 로, 유효하지 않으면 "Error", true 로 리턴한다고 해보자.

fun getURLPage(url: String): PageResult {
    val wasValidCall = false
    // TODO: do something
    if (wasValidCall) {
        return PageResult("The Content", false);
    } else {
        return PageResult("Error", true);
    }
}

문제점

그런데, 여기서 문제가 있다. if (pageResult.isError) 같은 식으로 나눠서 체크하기도 해야되지만 두 가지의 타입이 한 클래스 안에서 판단되어야 한다는 것이 문제이다.

에러가 아니면, 에러 메세지엔 관심을 가질 필요가 없고, 콘텐츠에만 관심을 가지면 되고, 에러면 에러 메세지에 관심을 갖고 콘텐츠엔 관심을 가질 필요가 없다.

어떻게 보면 엄청나게 헷갈릴 수 있다.

해결 방법?

그러면, PageResult 안에 Success, Error 란 클래스를 만들어서 관리하면 어떨까.

일단 PageResult에 있던 파라미터를 지우고, 두 개의 Nested Class를 만든다.

class PageResult {
    class Success(val content: String)
    class Error(val code: Int, val message: String)
}

그런데 한 가지 더 문제가 있다.

위에서 만든 처리 메소드는 PageResult.Success 같은 게 아닌 PageResult 를 리턴하게 되어있다. 이 문제를 해결하려면 PageResult.Success 가 PageResult 를 상속하게 하면 된다.

코틀린은 기본적으로 클래스가 상속될 수 있게 하려면 open 이란 키워드를 붙인다. 그리고 각 클래스에 PageResult 를 리턴하게 한다.

open class PageResult {
    class Success(val content: String) : PageResult()
    class Error(val code: Int, val message: String) : PageResult()
}

그 다음 만든 처리 메소드에서는 결과 리턴 부분만 바꿔주면 된다.

if (wasValidCall) {
    return PageResult.Success("The content")
} else {
    return PageResult.Error(404, "Not Found")
}

마지막으로 실행 메소드를 짜보면...

val pageResult = getURLPage("/")
when (pageResult) {
    is PageResult.Success -> println(pageResult.content) // smart cast
    is PageResult.Error -> println(pageResult.message)
}

아까보다 많이 가독성이 좋아졌다. 아무것도 신경쓸 필요 없이, PageResult 는 Success, Error 만 가지고 있을 것이다.

그러면 Sealed 는 어디에 쓸까?

아까 쓴 open 대신 sealed 를 대신 붙여보자. 기본적으로 Sealed은 같은 파일 안에서는 상속이 가능하지만, 다른 파일에서는 상속이 불가능하게 제한 한다.

같은 파일 안이라면,

sealed class PageResult {
    class Success(val content: String) : PageResult()
    class Error(val code: Int, val message: String) : PageResult()
}

class Middle: PageResult()

이런 식으로 할 수 있고,

sealed class PageResult

class Success(val content: String) : PageResult()
class Error(val code: Int, val message: String) : PageResult()
class Middle: PageResult()

이런 식으로 할 수 있다.

다만 다른 파일 안에서는 오류 메세지를 내보낸다.

장점

그러면 이 Sealed class 의 장점은 무엇일까.

바로 when 을 사용할 때 이다. 만일 모든 케이스를 커버할 수 있다면, else 는 쓰지 않아도 된다.

마무리

Sealed class의 개념은 비교적 간단한 편이지만 전에 함수형 프로그래밍을 한 적이 없었으니 이러한 걸 떠올리지 못했고, 이제야 특정 기능을 조금 더 알아보기 쉽게 설계하고 구현할 많은 아이디어가 떠오르게 되었다.

아직까지는 지식이 별로 없어 100% 활용할 수 있다고 말하기는 매우 어렵지만, 점점 사용해가면서 그 특유의 사용성을 알아낼 수 있다고 생각한다.

profile
Android Developer @kakaobank
post-custom-banner

0개의 댓글