본문 바로가기

MY개발생각

[개발생각] FSM을 사용하자.

미션 보상 서비스를 급하게 하나 만들어야 하는데... 하.. 만들지말까..정말..

(오픈준비를 하는 시점에, 기획 해달라고 예전부터 요청한걸 묵혔다가,

기획 정리도 안된 예전 기획서 대로 그냥 만들어달라고하여, 꼭지가 돈다)

 

하지만, 아무리 그래도, 개발은 해야할듯하여, (개발자 직업을 택한 내가 죄인이다...ㅠ)

주말에 어떻게 구현을 할것인가를 생각을 해봤는데..

미션 보상 조건이 상당히 복잡하다.. 

 

하지만 좀더 정리를 해보니, 여러조건이 순서대로 일어나고,

해당 순서대로 조건이 부합될때 보상을 해주는 흐름인것으로 파악을 했고,

순서가 있는 여러조건의 흐름에 맞는 설계는 FSM을 이용한 설계가 적합하지 않을까 생각이 들어서,

시스템 설계를 좀 해봤다.

 

우선, 기본적인 유저의 행동에 대한 이벤트는 이미 구현이 되어,

실제 행동에 대한 이벤트는 이벤트큐로 받을수 있는 상태라 그나마 다행이였다.

 

여기서 중요한건, 미션 보상을 위한 이벤트를 더 추가할 생각은 없다는것이다.

이벤트가 많아지고, 그 이벤트로 동작하는 구현들이 많아지면, 나중에는 흐름이 복잡해고 파악이 상당히 어렵기 때문이다.

그래서, 실제 해당 유저이벤트를 사용하는 리스너도 상당히 제한해서 사용하고 있다.

 

따라서, 미션 보상 서비스를 쉽게 만들기 위해서 이벤트를 더 늘리지는 않으려고 한다.

 

우선 코틀린에서 사용할수있는 FSM중에 KStateMachine을 사용하기로 했다.

spring fsm도 있지만, 경험상 상대적으로 너무 무겁고, 오버스펙으로 보여서,

우선 가볍고 사용하기 쉬운 KStateMachine으로 선택을 하였다.

 

그리고 FSM을 사용할때,

고민인 부분인 FSM을 유저기준으로 여러개를 만들것인지,

그때그때 만들고 버리는 방법을 사용할것인지를 선택을 잘해야한다.

물론 이 선택의 기본조건은 persistence하게 state를 유지해야한다는 것이다.

(나는 persistence 레이어를 레디스를 사용하기로 했다)

 

우선 좀더 자세한것은, 출근하면...더 보기로 하고...ㅎㅎ

 

아래와 같이 간단하게 뼈대를 구성했다. 

@Configuration
class ComplexRewardEventConsumeJob(
    private val kafkaTopicProperties: KafkaTopicProperties,
    private val redisTemplate: RedisTemplate<String, String>,
    private val friendRewardFsm: FriendRewardFsm
) {
    private val log = logger()

    @Bean
    fun complexRewardEventConsumeRouteBuilder() = hawaiiBatch {
        fromKafka(
            topic = kafkaTopicProperties.userEvent.name,
            groupId = "complex-reward-batch-consumer",
            consumersCount = kafkaTopicProperties.userEvent.consumersCount,
            maxPollRecords = kafkaTopicProperties.userEvent.maxPollRecords,
            batching = false
        ).prepared { exchange, _, body ->
            val event = try {
                hawaiiObjectMapper.readValue(body as String, RawUserEvent::class.java)
            } catch (ex: Exception) {
                log.error("*complexRewardEventConsumeRouteBuilder Error : ${ex.message} , rawEvent: ${body as String}")
                return@prepared
            }

            val userId = event.event["userId"] as Long
            val currentState = redisTemplate.opsForValue().get("fsm_state:${userId}") ?: UserRewardState.Pending.name!!
            val fsm = friendRewardFsm.createFriendRewardFsm(currentState,ComplexRewardContext(userId = userId))

            //fsm작동 (제휴사별 fsm을 만들어야함...)
            when(event.eventType) {
                UserEventTypeEnum.REGISTER -> {
                    fsm.processEventBlocking(RewardEvent.UserRegistered)
                }

                UserEventTypeEnum.RECOMMENDER_USER -> {
                    fsm.processEventBlocking(RewardEvent.RecommenderUserCompleted)
                }

                UserEventTypeEnum.DR_SUCCESS -> {
                    fsm.processEventBlocking(RewardEvent.RewardIssued)
                }
                else -> {}
            }

        }
    }
}

 

코드의 설명은,

camel로 user event를 컨슘하고, 해당 이벤트의 타입별로 state전이 이벤트를 발생시킨다.

여기서 fsm을 생성할때는 redis에서 userId별로 저장된 최근 state를 가져와서 초기 상태를 설정하여, fsm을 생성한다.

이렇게 하여 지속적인 상태를 유지하며, 전이를 발생시킨다.

 

우선은 이런 흐름으로 가고, 해당 이벤트에 따른 전이시,

각 이벤트에서 체크를 해야하는 부분은 guard에 구현을 하여 조건을 판별하도록 구현을 하면 될듯하다.

 

이런 미션 이벤트의 조건이 여러셋이 있기에, 이런 fsm의 흐름을 조건별로 세트로 만들어두고 해당 fsm들에 이벤트를 던져 fsm내에서 조건을 판별하여, 전이가 일어나도록 하면, 복잡하고 조건이 많은 보상조건도 잘 정리를 하여 개발이 가능할듯하다..

 

나중에는 FriendRewardFsm같이 여러 Fsm을 만들고, 체이닝하여 순회하게만 만들면, 여러 조건셋에 대한 보상기능으로도 확장이 된다.

 

예전에 처음 FSM을 사용했을때가 생각이난다.

너무 많은 유저대상으로 개별로 FSM을 풀로 생성하는 방법을 사용했다가, GC가 발생했던 문제나,

멀티 인스턴스에서 FSM풀을 사용하고, FSM을 삭제할때, 해당 인스턴스에서만 삭제를 하여, 다른 인스턴스에 FSM이 남아있게되어, GC가 발생했던 문제...

(라운드로빈으로 멀티 인스턴스로 요청이 들어가기에, 여러 인스턴스에 같은 유저기준으로 fsm이 만들어지게 되기에, fsm을 삭제시는 여러 인스턴스 모두에서 해당 유저기준 fsm이 있는지 확인하여 모두 삭제를 해야한다. 물론 이게 풀로 해당 fsm을 유지했기에 더문제가 컸다)

 

이런 이유때문에,

풀기능은 사용하지않고, 가벼운 fsm을 선택했고, 필요할때마다 사용하고 버리는 방식으로 구현을 하려고 한다.

spring-fsm이 상당히 무거웠다.

다행히 KStateMachine은 상당히 가벼운 객체이기에, 풀을 사용하지 않아도, 그때그때 사용하고 버려도 되겠다는 판단을 하였다.

 

시간이 좀만더 주어졌더라면, FSM + RULE(DSL) 구조로 더 잘 만들수있었겠지.. 

참 아쉽다..(나는 이런 상황이 참 아쉽고, 화가난다.. 더 잘할수있었는데, 못하게 되는 상황...)

 

기획/요건을 제때만 주었어도, 좀더 멋들어지고, 쿨하고, 유지보수/확장성도 좋게 개발에서 알아서 만들어 줄텐데...

왜 이런 복을 자꾸 차버리는 것일까??

참으로 안타깝다.. 모두를 점점 더 힘들게 만드는 이런식의 일처리..