-
[Kotlin] 상태패턴으로 배송상태를 변경해보자Kotlin/Kotlin 2023. 8. 29. 00:01728x90
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/
'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