프로그래밍 언어/Kotlin

[코틀린 기초] Kotlin sealed 클래스(실드 클래스)

코딩하는흑구 2022. 10. 5. 07:12

실드 클래스

하나의 Parent 클래스가 있을 때, 이를 상속하는(인터페이스라면 구현하는) 다른 Child1, Child2, ... 클래스가 있다고 가정하면, 컴파일러는 현재 시점에서 어떤 클래스가 Parent를 상속하고 상속하지 않는지 알수가 없다.

abstract class AbstractRoom {
    abstract val roomType: String
    abstract val title: String
    abstract fun tellStatus()
}

data class RoomApt(override val title: String) : AbstractRoom() {
    override val roomType: String = "아파트";
    override fun tellStatus() {
        println("$roomType 매물은 현재 계약 가능합니다.")
    }
}

data class RoomOfficetel(override val title: String) : AbstractRoom() {
    override val roomType: String = "오피스텔";
    override fun tellStatus() {
        println("$roomType 매물은 현재 계약 가능합니다.")
    }
}

object Dabang {
    val map = mutableMapOf<String, AbstractRoom>()

    fun getRoom(roomType: String) = map[roomType]
    fun addRoom(room: AbstractRoom) {
        when(room) {
            is RoomApt -> map[room.roomType] = room
            is RoomOfficetel -> map[room.roomType] = room
            else -> {
                println("그런 매물 없습니다.")
            }
        }
    }
}

fun main() {

    val roomApt = RoomApt(title = "아파트 매물입니다.")
    Dabang.addRoom(roomApt)
    val roomOfficetel = RoomOfficetel(title = "오피스텔 매물입니다.")
    Dabang.addRoom(roomOfficetel)

    println(Dabang.getRoom("아파트"))
    println(Dabang.getRoom("오피스텔"))

}
//RoomApt(title=아파트 매물입니다.)
//RoomOfficetel(title=오피스텔 매물입니다.)

- AbstractRoom : Parent 클래스를 담당하는 추상 클래스이다.

- RoomApt, RoomOfficetel : AbstractRoom 클래스를 상속하는 구현 클래스이다.

- Dabang: 두 매물을 담아 광고하는 일종의 저장소이다.

 

이때, 해당 코드의 main 실행결과는 무난하게 출력된다.

 

하지만 여기서 하나의 코드만 수정해보자.

object Dabang {
    val map = mutableMapOf<String, AbstractRoom>()

    fun getRoom(roomType: String) = map[roomType]
    fun addRoom(room: AbstractRoom) = when (room) {
        is RoomApt -> map[room.roomType] = room
        is RoomOfficetel -> map[room.roomType] = room
    }
    
}

코드를 이런식으로 when 표현식으로 변경 후에 함수의 결과로써 행해져야 한다면.. 이야기가 달라진다.

우선 컴파일이 되지 않는다.

Kotlin: 'when' expression must be exhaustive, add necessary 'else' branch

else 구문을 사용하라고 한다. 가만보니 when 절에 RoomApt, RoomOfficetel 타입 말고는 따로 지정해서 정해준적이 없다. 그러니 함수의 인자로 다른 타입의 인스턴스가 넘어오는 경우 제대로된 처리를 할 수 없게되니 else로 고려하라는 말인 것 같다.

 

그런데 여기서 코드를 하나만 더 바꿔보자.

sealed class AbstractRoom {
    abstract val roomType: String
    abstract val title: String
    abstract fun tellStatus()
}

abstract  키워드를 sealed로 수정했다.

 

과연 컴파일이 잘될까?

 -> 매우 잘된다. 


이런 문법이 왜있는걸까?

 

else 절만 잘 써놓으면 이런 불필요한 컴파일 에러는 잘 넘길 수 있는게 아닌가??

 

이 질문에 대한 답은 의외로 우리 주변에서 쉽게 볼 수 있다. 우리는 항상 신규개발만 진행하지 않는다. 유지보수도 진행한다. 근데 본인이 진행했던 개발만 유지보수하지 않는다. 다른사람의 코드도 유지보수하는 경우가 너무나도 많다.

 

그 많은 코드를 스스로 짜더라도 자신의 코드를 기억하지 못하는 개발자가 더 많을텐데 else 절로 저렇게 퉁쳐버리면 어떤 코드에서 어떤 불필요한 버그가 발생할지 런타임이 아닌 이상 확인하기 어렵다.

 

런타임이 아닌 이상 확인하기 어렵다는 말은 달리 말하면 실행해보고 하지 않으면 모를 수 있다는 뜻이다.

 

테스트를 하면 되지 않나요?

 

하지만.. 테스트 코드도 런타임이다.. 즉 실행해보아야 알 수 있다...! 이건 위에서 말한것과 동일하다. 런.타.임.

 

그런데 이렇게 런타임에서 발생할 수 있을 법한 이슈를 sealed 라는 키워드로 제한해놓고 컴파일단에서 확인할 수 있게 해준다면 코틀린 언어라는 자체에서 개발자로 하여금 얻을 수 있게 해주는 방어요소가 너무나도 많은 것 같다.(위에서 언급한 런타임 에러)

 


새로운 언어를 공부할 때, 가장 간과하기 쉬운 부분이 이러한 문법적으로 기존 언어들과 다른 부분을 제공할 때, 왜 이런게 나왔지??? 라는 질문을 던져보는 것이 좋아보인다.

 

결국 기존 개발언어에서 특정 패턴이 반복되어 디자인 패턴이 고안되고 신규 언어는 디자인 패턴으로부터 언어적 문법 요소를 차용할 가능성이 높다.

 

싱글톤 선언 키워드도 그랬고 이러한 sealed 클래스도 그러한 부분이 아닌가 싶다. 물론 sealed 키워드는 어떤 디자인 패턴적인 요소는 아니지만 자바에서 switch문을 썻을 때 반드시 default를 걸어놓아서 예외처리를 한다던가 등등의 개발 패턴에서의 단점을 kotlin에서 해결하고자 한것으로 판단되니 말이다.(필자의 개인적 생각입니다.)

 

결국 왜 이런 문법이 생겼는가 하고 짚고 넘어가게 된다면 단순히 개발언어의 문법을 공부하는 것보다는 좀더 많은 통찰과 시각을 넓힐 수 있는 기회를 가질 수 있지 않나 생각이 들었다.