ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin] 상태패턴으로 배송상태를 변경해보자
    Kotlin/Kotlin 2023. 8. 29. 00:01

    Github

    시작전에 모든 코드는 github에서 확인할 수 있습니다.

    https://github.com/Junuu/spring-study/tree/main/delivery-state-pattern

     

    상태패턴을 알아보기 전 배송상태부터 정의

    배송상태

    5개의 배송상태를 정의해 보았습니다.

    위의 코드를 구현한다면 각 상태를 전이하기 전에 유효성 체크등을 잘 수행해주어야 합니다.

     

    다음과 같이 pseudo 코드를 작성해 볼 수 있습니다.

    if(state == '준비'){
        if(currentState == '관리자 확인전  대기') {
            //준비로 상태 변경
        } else{
            //변경할 수 없는 상태 Exception 또는 아무것도 하지 않음
        }
    }

     

    예를 들어 상태가 20~30가지라면? 변경해야 할 상태들이 추가된다면? 단일 메서드나 중첩된 if로 구현하기는 까다로울 수 있습니다.

    이럴 때 상태패턴을 고려해 볼 수 있습니다.

     

    상태패턴이란?

    주로 상태 전이를 위한 조건 로직이 복잡하여 이를 해소하기 위해 객체의 내부 상태에 따라 스스로 행동을 변경할 수 있도록 구현하는 패턴입니다.

    객체는 자신의 클래스를 바꾸면서 상태를 변경해 나갑니다.

    다만, 상태 전이 로직을 쉽게 이해할 수 있다면 굳이 상태패턴으로 리팩터링 할 필요는 없습니다.

     

    구현

    Delivery & DeliveyStatus Enum

    data class Delivery(
        var state: DeliveryState,
    )
    
    enum class DeliveryStatus{
        WAITING,
        READY,
        DELIVERING,
        COMPLETE,
        REFUND,
    }

    배송에 대한 부가적인 정보들이 필요하지만 현재관심사인 state 프로퍼티만 가집니다.

    해당 프로퍼티는 DeliveryState interface가 관심사입니다.

     

    DeliveryStatus는 발송전대기, 준비, 배송, 완료, 환불에 대한 상태를 가집니다.

     

    DeliveryState interface

    interface DeliveryState {
        fun cancel(delivery: Delivery)
        fun next(delivery: Delivery)
        fun refund(delivery: Delivery)
        fun getCurrentState(): DeliveryStatus
    }

    배송상태에 대한 interface로는 현재 상태에서 취소, 다음상태로 전이, 환불, 현재상태조회(enum)를 정의했습니다.

    이제 각각 상태를 구현해 보겠습니다.

     

    WaitingState

    object WaitingState : DeliveryState{
        override fun cancel(delivery: Delivery) {
            throw IllegalStateException("관리자 확인전 상태에서는 이전으로 되돌아갈 수 없습니다.")
        }
    
        override fun next(delivery: Delivery) {
            println("WaitingState is ready for ReadyState.")
            delivery.state = ReadyState
    
        }
    
        override fun refund(delivery: Delivery) {
            println("WaitingState is ready for RefundState.")
            delivery.state = RefundState
        }
    
        override fun getCurrentState(): DeliveryStatus {
            return DeliveryStatus.WAITING
        }
    }

    DeliveryState 인터페이스를 구현하고 있으며 각 상태에 대해 해야 할 일들을 구현합니다.

    예를 들어 대기상태에서는 이전단계로 취소할 수 없고, 다음상태로는 배송준비상태, 환불상태로 변경할 수 있습니다.

    kotlin의 object를 활용하여 싱글톤을 간편하게 구현할 수 있습니다.

     

    ReadyState

    object ReadyState : DeliveryState{
        override fun cancel(delivery: Delivery) {
            println("ReadyState is ready for WaitingState.")
            delivery.state = WaitingState
        }
    
        override fun next(delivery: Delivery) {
            if(delivery.invoiceCode.isNullOrBlank()){
             throw IllegalStateException("invoiceCode가 비어있다면 배송중 상태로 변경할 수 없습니다.")
            }
            println("ReadyState is ready for preparation.")
            delivery.state = DeliveringState
        }
    
        override fun refund(delivery: Delivery) {
            println("ReadyState is ready for RefundState.")
            delivery.state = RefundState
    
        }
    
        override fun getCurrentState(): DeliveryStatus {
            return DeliveryStatus.READY
        }
    }

    ReadyState -> DeliveryState로 넘어갈 때는 invoiceCode가 비어있는 경우에는 상태를 변경할 수 없도록 제약을 걸었습니다.

     

     

    DeliveryState

    object DeliveringState : DeliveryState{
        override fun cancel(delivery: Delivery) {
            println("DeliveringState is ready for ReadyState.")
            delivery.invoiceCode = null
            delivery.state = ReadyState
        }
    
        override fun next(delivery: Delivery) {
            println("DeliveringState is ready for ReadyState.")
            delivery.state = CompleteState
        }
    
        override fun refund(delivery: Delivery) {
            println("DeliveringState is ready for RefundState.")
            delivery.state = RefundState
        }
    
        override fun getCurrentState(): DeliveryStatus {
            return DeliveryStatus.DELIVERING
        }
    }

    유사하게 deliveringState에서 취소하는 경우에는 송장번호가 null으로 빠지도록 구성하였습니다.

     

    ComplteState

    object CompleteState : DeliveryState{
        override fun cancel(delivery: Delivery) {
            println("CompleteState is ready for DeliveringState.")
            delivery.state = DeliveringState
        }
    
        override fun next(delivery: Delivery) {
            throw IllegalStateException("배송 완료 상태는 다음 프로세스가 존재하지 않습니다.")
        }
    
        override fun refund(delivery: Delivery) {
            println("CompleteState is ready for RefundState.")
            delivery.state = RefundState
        }
    
        override fun getCurrentState(): DeliveryStatus {
            return DeliveryStatus.COMPLETE
        }
    }

     

    RefundState

    object RefundState : DeliveryState{
        override fun cancel(delivery: Delivery) {
            println("RefundState is ready for WaitingState.")
            delivery.invoiceCode = null
            delivery.state = WaitingState
        }
    
        override fun next(delivery: Delivery) {
            throw IllegalStateException("환불 상태는 다음 프로세스가 존재하지 않습니다.")
        }
    
        override fun refund(delivery: Delivery) {
            throw IllegalStateException("환불 상태에서 또다시 환불을 진행할 수 없습니다.")
        }
    
        override fun getCurrentState(): DeliveryStatus {
            return DeliveryStatus.REFUND
        }
    }

     

     

    이제 각 상태들을 제어하는 부분은 바로 Delivery에서 수행합니다.

    data class Delivery(
        var invoiceCode: String? = null,
        var state: DeliveryState,
    ){
        fun nextProcess(invoiceCode: String?= null){
            if(invoiceCode.isNullOrBlank()){
                this.invoiceCode = invoiceCode
            }
            state.next(this)
        }
    
        fun cancel(){
            state.cancel(this)
        }
    
        fun currentState(): DeliveryStatus{
            return state.getCurrentState()
        }
    }

     

    DeliveryStatePatternTestService

    @Service
    class DeliveryStatePatternTestService {
    
        val concurrentMap = ConcurrentHashMap<String, Delivery>()
    
        fun changeStatus(
            deliveryId: String,
            invoiceCode: String? = null,
            behavior: String = "nextProcess",
        ): String {
            val delivery = concurrentMap[deliveryId] ?: throw NoSuchElementException()
            when (behavior) {
                "nextProcess" -> delivery.nextProcess(invoiceCode)
                "refund" -> delivery.refund()
                else -> delivery.cancel()
            }
            return delivery.currentState().name
        }
    
        @PostConstruct
        fun dataSave() {
            concurrentMap["1"] = Delivery(state = WaitingState)
        }
    }

    Spring Boot 환경에서 코드를 실행하여서 Delivery를 제어하는 Service입니다.

    순수 Kotlin만 사용한다면 main에서 실행해도 될 것 같습니다.

    behavior에 따라 다음프로세스로 실행할지 취소할지 환불할지를 판단합니다.

     

     

    후기

    상태의 전이를 명확하게 정의하면서 구현할 수 있다는 것이 장점으로 다가왔습니다.

    구현을 하며 다음상태에 도달하기 위해서 어떻게 진행해야 할지 고민해 보며 구현전에는 고민하지 못했던 부분을 찾아 적용하게도 했습니다.

     

     

     

     

    참고자료

    https://johngrib.github.io/wiki/pattern/state/

     

    스테이트 패턴 (State Pattern)

    객체의 내부 상태에 따라 행동을 변경할 수 있다.

    johngrib.github.io

     

    'Kotlin > Kotlin' 카테고리의 다른 글

    Kotlin Synchonized와 MultiThread  (0) 2023.11.22
    Kotlin과 Java의 컴파일 순서  (0) 2023.11.11
    [Kotlin] Result란?  (0) 2022.12.04
    [Kotlin] 연산자 오버로딩 사용 예시  (0) 2022.11.25
    [Kotlin] @JvmStatic이란?  (0) 2022.10.28

    댓글

Designed by Tistory.