기록

경험치 시스템 구현(2): 인터페이스를 사용한 동적 경험치 부여 본문

Web/Spring

경험치 시스템 구현(2): 인터페이스를 사용한 동적 경험치 부여

youngyin 2024. 6. 17. 12:00

시작하면서

이전 포스팅에서는 어드바이저와 커스텀 어노테이션을 사용하여 경험치 시스템을 구현했습니다.(2024.06.07 - [Web/Spring] - 경험치 시스템 구현(1): 어드바이스와 커스텀 어노테이션 활용) 그러나 현재 구현에서는 모든 활동에 대해 동일한 경험치를 부여하도록 되어 있습니다. 이번에는 좋아요를 누르거나 사진을 제출할 때 각각 다른 경험치를 부여하고, 특정 조건을 만족할 때만 경험치를 부여하는 기능을 추가하고자 합니다.

 

A. 프로젝트 구성

아래처럼 구성된 API 서버에에서 아래의 요구사항을 구현하려고 합니다.

Spring Boot, Gradle, kotlin

 

B. 요구사항

1. 사용자가 게시글에 좋아요를 누를 수 있다. (/api/customer/emoji/like)

2. 이번달에 등록된 게시글에 좋아요를 눌렀을 때, 좋아요를 누른 사용자에게 경험치를 10만큼 부여합니다.

 

인터페이스 활용

경험치 부여 여부와 경험치 값을 동적으로 설정하기 위해 인터페이스를 도입했습니다. XpProcessor 인터페이스에는 경험치 부여 대상인지 판별하는 isTarget() 메서드와 경험치 요구 정보를 반환하는 getXpReq() 메서드가 정의되어 있습니다. 이를 구현한 MonthlyLikeXpProcessor 클래스에서는 이번 달에 등록된 게시글에 좋아요를 눌렀을 때만 경험치를 부여하도록 로직을 구현했습니다.

interface XpProcessor {
    fun isTarget(args: Array<Any>): Boolean
    fun getXpReq(args: Array<Any>): XpReqDto
}

@Component
class MonthlyLikeXpProcessor : XpProcessor {

    @Autowired
    private lateinit var challengeService: ChallengeService

    override fun isTarget(args: Array<Any>): Boolean {
        val request = args[0] as EmojiRegisterReq
        val challenge = challengeService.findById(req.challengeId)
        
        val challengeMonth = YearMonth.from(challenge.regDtime)
        val actionMonth = YearMonth.from(LocalDate.now())
        return challengeMonth == actionMonth
    }

    override fun getXpReq(args: Array<Any>): XpReqDto {
        return XpReqDto(10, getLoginUser())
    }

}

 

어노테이션 활용

@GrantXp 어노테이션은 경험치 부여 로직을 수행할 XpProcessor 구현체의 클래스 정보를 저장합니다. 컨트롤러의 메서드에 @GrantXp 어노테이션을 적용하면, AOP 어드바이저에서 해당 어노테이션을 찾아 경험치 부여 여부와 경험치 값을 계산하여 서비스에 전달합니다.

 

(1) 어노테이션 선언

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class GrantXp(val processor: KClass<out XpProcessor>)

 

(2) 컨트롤러에서 @GrantXp 사용

    @PostMapping("/customer/emoji/like")
    @GrantXp(processor = MonthlyLikeXpProcessor::class)
    fun registerLike(request : EmojiRegisterReq): ResponseEntity<BaseResp> {
        return getRespEntity(service.register(request ))
    }

 

(3) 어드바이저 구현

@Aspect
@Component
class XpGrantingAspect(private val applicationContext: ApplicationContext) {

	@Autowired 
    private lateinit var customerService: CustomerService
    
    @Around("@annotation(grantXp)")
    fun grantXp(joinPoint: ProceedingJoinPoint, grantXp: GrantXp): Any? {
        val result = joinPoint.proceed()
        val params = joinPoint.args

        val processorBean = applicationContext.getBean(grantXp.processor.java)
        if (processorBean is XpProcessor) {
            if (processorBean.isTarget(params)) {
                val xpReq = processorBean.getXpReq(params)
                customerService.gainXp(xpReq)
            }
        }
        
        return result
    }
}

 

결론

이번 포스팅에서는 @GrantXp 어노테이션과 XpProcessor 인터페이스를 활용하여 액션에 대한 경험치 부여 로직을 구현하는 방법을 소개했습니다. 이를 통해 경험치 부여 대상 여부와 경험치 값을 동적으로 설정할 있게 되었습니다

그러나 AOP 사용하여 비즈니스 로직을 처리하는 것이 항상 최선의 방법은 아닐 있습니다. AOP는 주로 로깅, 트랜잭션 관리, 보안  횡단 관심사를 처리하는  유용합니다. 비즈니스 로직을 어드바이스에 포함시키는 것은 AOP 본래 목적에 맞지 않을  있습니다. 

특히, 로직이 복잡해지거나 성능에 영향을 끼칠 경우, AOP보다는 다른 방법을 고려하는 것이 좋습니다. 스프링 배치를 활용하여 대량의 데이터 처리나 복잡한 로직을 처리하는 것을 고려해야 합니다.

Comments