이번 글에서는 간단한 프로그램인 크리스마스 이벤트 기능 구현 중 GiftMenu를 sealed class로 설계한 구조에 대해 설명합니다. 이 구조는 특정 조건에 따라 증정 상품을 결정하는 로직을 명확하고 안전하게 구현하는 데 초점을 맞췄습니다.
코틀린의 sealed class를 활용하면 상태별로 다른 객체를 타입 안정성과 함께 선언적으로 표현할 수 있어, 조건 분기 로직이 간결하고 유지보수성이 좋아집니다.
sealed class
는 Kotlin에서 클래스 상속을 같은 파일 내로 제한할 수 있도록 제공하는 기능입니다. Java의 enum
보다 더 유연하며, abstract class
보다 타입 안정성 면에서 유리합니다.
기능 | 설명 |
---|---|
하위 클래스 제한 | sealed class 를 상속하는 클래스는 같은 파일 내에만 정의 가능 |
when 절에서 안전성 보장 | when 문에서 모든 하위 타입을 처리하지 않으면 컴파일러가 경고 |
상태 표현에 적합 | 이벤트, 상태, 결과 값 등을 명확하게 타입으로 분기 가능 |
GiftMenu는 총 주문 금액에 따라 고객에게 제공되는 증정 상품을 결정하는 역할을 합니다.
이러한 조건을 단순한 if-else로 처리하는 것이 아닌, 타입으로 상태를 분리하여 표현하고자 했습니다.
sealed class GiftMenu(
open val name: String,
open val quantity: Int
) {
abstract fun benefitAmount(): Int
companion object {
private const val GIFT_THRESHOLD = 120_000
fun from(totalPrice: Int): GiftMenu {
if (totalPrice > GIFT_THRESHOLD) return ChampagneGift
return NoGift
}
}
}
open val로 선언한 이유는?
생성자에서 하위 객체마다 서로 다른 값을 넣기 위해 필요합니다. val은 final이라 생성자에서 값 바꾸는 것이 불가능합니다.
object ChampagneGift : GiftMenu(Menu.CHAMPAGNE.menuName, 1) {
override fun benefitAmount(): Int = Menu.CHAMPAGNE.price
}
object NoGift : GiftMenu("None", 0) {
override fun benefitAmount(): Int = 0
}
val gift = GiftMenu.from(totalPrice)
when (gift) {
is ChampagneGift -> println("샴페인 증정!")
is NoGift -> println("증정 없음")
// else 없음 → 컴파일러가 모든 경우를 체크
}
→ 하위 클래스가 고정되어 있으므로 when 문에서 누락된 타입이 있으면 컴파일 시점에 경고합니다.
GiftMenu를 sealed class로 선언했기 때문에, 개발자가 외부에서 임의로 다른 하위 클래스를 만들 수 없습니다. 즉, ChampagneGift와 NoGift만 존재한다는 것이 보장됩니다.
각 증정 타입은 object로 구현하여 한 번만 생성되어 공통으로 사용됩니다. 매번 새로운 인스턴스를 만들지 않기 때문에 불필요한 객체 생성을 방지할 수 있습니다.
sealed class를 활용하면 특정 상태에 대한 클래스를 명확하고 안전하게 분리할 수 있습니다. GiftMenu는 그 좋은 예로, 조건 분기를 단순한 로직이 아닌 객체로 추상화하여 클린한 코드를 만들 수 있었습니다.
이러한 설계는 다음과 같은 요구에 적합합니다: