Kotlin/Kotlin

kotlin enum 상태 관리 구현하기

Junuuu 2024. 9. 27. 21:20
728x90

개요

복잡한 소프트웨어의 상태

 

소프트웨어를 개발하다 보면 내부적인 상태를 관리해야 합니다.

 

이때 상태는 5가지 이내로 간단하게 표현될 수도 있고 50가지 혹은 그 이상으로 표현될 수도 있습니다.

 

어떻게 하면 수많은 상태가 존재할 때 안정적으로 상태를 관리하고 가독성 및 유지보수성을 챙길 수 있을까요?

 

 

유한상태기계(Finite State Machine)

상태에 대해 공부하다 보면 유한상태기계 (FSM)이라는 개념이 종종 보입니다.

 

유한상태기계란 결국 한정되어 있는 개수의 복잡한 상태를 설계하는 데 사용하는 수학적 기법입니다.

 

이러한 기계는 한 번에 오로지 하나의 상태만을 가지게 됩니다.

 

이러한 기계는 어떠한 사건(Event)에 의해 한 상태에서 다른 상태로 변화할 수 있으며, 이를 전이(Transition)이라 합니다.

 

결국 중요한 건 여러 상태중 하나의 상태를 가지며 이벤트에 의해 다른 상태로 변경될 수 있음을 뜻합니다.

 

유한상태기계의 장점은 무엇일까?

https://junuuu.tistory.com/819

동그라미는 상태를 뜻하며 화살표는 이벤트로 인한 전이를 의미합니다.

 

위 플로우를 보면 확실히 스테이트 머신의 장점이 보입니다.

 

1. 반드시 설계한 대로만 플로우가 흐르게 됩니다.

1. A -> B -> C 상태로 플로우가 흘러야 하는데 A -> C로 바로 이동할 수 없게 됩니다.
2. 각 상태 전이가 명시적으로 정의되어 있기 때문에 오류 발생 가능성이 줄어듭니다

 

 

2. 정해진 이벤트를 통해 상태가 전이되기 때문에 전체 프로세스에서 어떤 이벤트들이 발생하는지 파악하기 쉽습니다.

 

3. 전체적인 흐름이 명확하게 보인다.

1. 나중에 합류하신 분이 코드를 보더라도 명확하고 한눈에 들어온다.
2. 설령 코드 구현체 자체가 복잡하더라도 설계 문서 한 장만 보더라도 비즈니스 흐름에 대한 이해가 가능해진다.

 

 

그러면 스테이트 머신을 구현하기 위해서는 어떤 방법들이 있을까요?

 

spring framework에서 지원하는 spring-statemachine-core 라이브러리 등을 활용해 볼 수 있습니다.

 

하지만 라이브러리에 대한 학습곡선과 한계점등이 존재할 수 있는 단점이 있습니다.

 

대안으로는 객체지향을 상태패턴을 활용하여 구현해 볼 수 있습니다.

 

상태패턴을 활용해서 구현해 보기

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

 

위와 같은 DeliveryState를 두면 모든 각각의 배송상태는 Delivery Interface를 구현해야 합니다.

 

getCurrentState 함수를 통하여 현재 상태를 가져올 수 있으며 cancel, refund, confirmAdmin 등의 Event에 의해 상태전이가 발생하게 됩니다.

 

하지만 이벤트가 추가된다면, 상태가 추가된다면 어떻게 될까요?

 

상태가 30개라고 가정해 본다면 이벤트(인터페이스의 메서드)가 추가되면 30개의 상태에 대해 상태전이를 정의해야 합니다.

 

반대로 상태가 추가된다면 어떨까요? 자신의 관심사가 아닌 이벤트들도 처리하게 됩니다.

 

귀찮음을 감수하고 코드를 수정할 수 있지만, 30개의 클래스가 생길 것이고 추후 관리하기도 힘들어질 것 같습니다.

 

위의 단점을 해소하기 위해 enum을 활용하여 상태관리를 구현해보겠습니다.

 

Enum으로 상태를 관리해 보기

enum class LoanProductStatus(
    val productContinuation: ProductContinuation, // 사용자에게 어떤 화면을 노출해야할지를 담고있는 프로퍼티
    val isUserCancelable: Boolean = false, // 취소 가능여부를 담고 있는 프로퍼티
){
    내부심사중(productContinuation = ProductContinuation.심사_진행중, isUserCancelable = true){
        override val allowedNextStatus by lazy { setOf(내부심사부결,내부심사통과,사용자심사취소)}
    },
    내부심사통과(productContinuation = ProductContinuation.심사_진행중, isUserCancelable = true){
        override val allowedNextStatus by lazy { setOf(외부심사중,사용자심사취소)}
    },
    내부심사부결(productContinuation = ProductContinuation.심사_부결, isUserCancelable = true){
        override val allowedNextStatus by lazy { setOf(사용자부결확인)}
    },
    사용자심사취소(productContinuation = ProductContinuation.종료, isUserCancelable = false){
        override val allowedNextStatus by lazy { END_STATUS }
    },
    사용자부결확인(productContinuation = ProductContinuation.종료, isUserCancelable = false){
        override val allowedNextStatus by lazy { END_STATUS}
    },
    외부심사중(productContinuation = ProductContinuation.심사_진행중, isUserCancelable = false) {
        override val allowedNextStatus by lazy { setOf(외부심사통과)}
    },
    외부심사통과(productContinuation = ProductContinuation.약정_가능,isUserCancelable = false) {
        override val allowedNextStatus by lazy { setOf(대출실행)}
    },
    대출실행(productContinuation = ProductContinuation.대출실행완료) {
        override val allowedNextStatus by lazy { END_STATUS }
    };

    // by lazy를 활용하면 맨 처음 변수에 접근할 때 초기화가 이루어진다.
    // 대안으로 init을 활용하여 변경 가능한 상태를 정의하는 방법이 존재하지만 상태와 변경가능한 코드가 분리되기 때문에 응집도가 떨어지고, 가독성이 나빠지게 된다.
    abstract val allowedNextStatus: Set<LoanProductStatus>

    fun isTransferable(to: LoanProductStatus): Boolean{
        return this.allowedNextStatus.contains(to)
    }

    companion object{
        val END_STATUS = emptySet<LoanProductStatus>()
    }
}

LoanProductStatus라는 enum에서 각각 상태를 정의할 수 있습니다.

 

각 상태는 화면에 대한 정보를 포함하는 프로퍼티와, 취소 가능 여부를 담고 있는 프로퍼티, 다음 상태로 이동

가능한 상태들을 담고 있는 프로퍼티를 가지고 있습니다.

 

이때 프로퍼티는 상황에 따라서 유연하게 더 추가할 수 있습니다.

 

정의된 enum은 어떻게 활용될 수 있을까요?

data class LoanProductStatusEntity(
    val id: Long,
    var status: LoanProductStatus,
) {
    // 발생할 수 있는 event들 정의

    private fun updateStatus(newStatus: LoanProductStatus){
        if(status.isTransferable(newStatus)){
            status = newStatus
        }
        throw IllegalStateException("$status 에서 $newStatus 로 변경될 수 없습니다. 변경가능한 상태를 확인해야 합니다.")
    }
    fun 사용자대출취소() {
        if(this.status.isUserCancelable.not()){
            throw IllegalArgumentException("현재 상태는 사용자가 대출을 취소할 수 없는 상태입니다.")
        }
        updateStatus(LoanProductStatus.사용자심사취소)
    }

    fun 심사통과() {
        val newStatus = when(this.status){
            LoanProductStatus.내부심사중 -> LoanProductStatus.내부심사통과
            LoanProductStatus.외부심사중 -> LoanProductStatus.외부심사통과
            else -> throw IllegalArgumentException("현재 상태는 사용자가 대출 심사에 통과할 수 없는 상태입니다.")
        }
        updateStatus(LoanProductStatus.사용자심사취소)
    }

    fun 사용자부결확인() {
        if(this.status.isUserCancelable.not()){
            throw IllegalArgumentException("현재 상태는 사용자가 대출을 취소할 수 없는 상태입니다.")
        }
        updateStatus(LoanProductStatus.사용자부결확인)
    }

}

LoanProductStatusEntity에서 실제 발생하는 이벤트를 정의합니다.

 

각 상태에 대해 이벤트가 별도의 LoanProductStatusEntity 클래스로 분리되지만 오히려 어떤 이벤트들이 발생하는지 한 번에 모아볼 수 있는 장점도 가지게 됩니다. (이벤트와 상태를 분리하는 관점)

 

이후 해당 이벤트를 Application Layer에서 호출하며 실제 상태를 변경해 나갈 수 있습니다.

 

마무리

kotlin enum을 활용하여 상태관리를 구현해보았으며, 다음에는 spring state machine을 활용하여 프레임워크의 도움을 받아 구현해보면서 trade off를 알아보고자 합니다.

 

 

참고자료

https://tecoble.techcourse.co.kr/post/2021-04-26-state-pattern/

https://refactoring.guru/ko/design-patterns/state

https://underflow101.tistory.com/55

https://dev.gmarket.com/52

https://dealicious-inc.github.io/2021/04/12/state-machine.html

https://i-nara.oopy.io/c174c93c-be04-4bfb-8473-8e55f9987ab9