Kotlin sealed class로 구현한 GiftMenu 설계 정리

Jayson·2025년 5월 3일
0
post-thumbnail

개요

이번 글에서는 간단한 프로그램인 크리스마스 이벤트 기능 구현 중 GiftMenu를 sealed class로 설계한 구조에 대해 설명합니다. 이 구조는 특정 조건에 따라 증정 상품을 결정하는 로직을 명확하고 안전하게 구현하는 데 초점을 맞췄습니다.

코틀린의 sealed class를 활용하면 상태별로 다른 객체를 타입 안정성과 함께 선언적으로 표현할 수 있어, 조건 분기 로직이 간결하고 유지보수성이 좋아집니다.

sealed class란?

sealed class는 Kotlin에서 클래스 상속을 같은 파일 내로 제한할 수 있도록 제공하는 기능입니다. Java의 enum보다 더 유연하며, abstract class보다 타입 안정성 면에서 유리합니다.

주요 특징

기능설명
하위 클래스 제한sealed class를 상속하는 클래스는 같은 파일 내에만 정의 가능
when 절에서 안전성 보장when 문에서 모든 하위 타입을 처리하지 않으면 컴파일러가 경고
상태 표현에 적합이벤트, 상태, 결과 값 등을 명확하게 타입으로 분기 가능

설계 목적

GiftMenu는 총 주문 금액에 따라 고객에게 제공되는 증정 상품을 결정하는 역할을 합니다.

  • 120,000원 초과 시 → 샴페인 1병 증정 (ChampagneGift)
  • 그 외의 경우 → 증정 없음 (NoGift)

이러한 조건을 단순한 if-else로 처리하는 것이 아닌, 타입으로 상태를 분리하여 표현하고자 했습니다.

코드 구조 설명

1. sealed class GiftMenu

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
        }
    }
}
  • name, quantity: 하위 클래스에서 값을 달리 지정할 수 있도록 open val로 선언
  • benefitAmount: 증정 혜택 금액을 계산하는 추상 함수 (하위 클래스에서 구현)
  • from(totalPrice: Int): 총 가격 기준으로 증정 상품 객체를 생성하는 팩토리 메서드

open val로 선언한 이유는?
생성자에서 하위 객체마다 서로 다른 값을 넣기 위해 필요합니다. val은 final이라 생성자에서 값 바꾸는 것이 불가능합니다.

2. object ChampagneGift

object ChampagneGift : GiftMenu(Menu.CHAMPAGNE.menuName, 1) {
    override fun benefitAmount(): Int = Menu.CHAMPAGNE.price
}
  • object로 정의하여 싱글턴(단일 인스턴스)으로 사용
  • name, quantity는 Menu.CHAMPAGNE 값을 기반으로 지정
  • benefitAmount()는 해당 메뉴의 가격을 반환

3. object NoGift

object NoGift : GiftMenu("None", 0) {
    override fun benefitAmount(): Int = 0
}
  • 증정이 없는 경우를 표현
  • "None"과 수량 0을 의미로 갖는 객체
  • 혜택 금액은 당연히 0 반환

설계상의 장점

1. 타입 안정성 보장

val gift = GiftMenu.from(totalPrice)
when (gift) {
    is ChampagneGift -> println("샴페인 증정!")
    is NoGift -> println("증정 없음")
    // else 없음 → 컴파일러가 모든 경우를 체크
}

→ 하위 클래스가 고정되어 있으므로 when 문에서 누락된 타입이 있으면 컴파일 시점에 경고합니다.

2. 의도된 클래스만 사용 가능

GiftMenu를 sealed class로 선언했기 때문에, 개발자가 외부에서 임의로 다른 하위 클래스를 만들 수 없습니다. 즉, ChampagneGift와 NoGift만 존재한다는 것이 보장됩니다.

3. 싱글턴으로 메모리 효율성 확보

각 증정 타입은 object로 구현하여 한 번만 생성되어 공통으로 사용됩니다. 매번 새로운 인스턴스를 만들지 않기 때문에 불필요한 객체 생성을 방지할 수 있습니다.

개선 고려 사항

  • Menu.CHAMPAGNE이 어디에서 정의되었는지에 따라 의존성이 생길 수 있으므로, Menu와의 관계가 지나치게 강하면 분리해보는 것도 고려해볼 수 있습니다.
  • GiftMenu의 타입을 enum으로 할 수도 있지만, enum은 각각의 인스턴스를 객체처럼 세밀하게 커스터마이징하기 어렵기 때문에 sealed class + object 조합이 더 유연합니다.

결론

sealed class를 활용하면 특정 상태에 대한 클래스를 명확하고 안전하게 분리할 수 있습니다. GiftMenu는 그 좋은 예로, 조건 분기를 단순한 로직이 아닌 객체로 추상화하여 클린한 코드를 만들 수 있었습니다.

이러한 설계는 다음과 같은 요구에 적합합니다:

  • 여러 상태를 서로 다른 객체로 나누고 싶을 때
  • 특정 조건에 따라 객체를 리턴하고 싶을 때
  • when 분기에서 컴파일러 도움을 받고 싶을 때
profile
Small Big Cycle

0개의 댓글