기록

경험치 시스템 구현(1): 어드바이스와 커스텀 어노테이션 활용 본문

Web/Spring

경험치 시스템 구현(1): 어드바이스와 커스텀 어노테이션 활용

youngyin 2024. 6. 10. 12:00

시작하면서

이번 포스트에서는 Spring Framework에서 AOP와 커스텀 어노테이션을 사용하여 경험치(XP)를 부여하는 시스템을 구현하는 방법을 소개하고자 합니다.

 

(1) 프로젝트 구성
아래와 같이 구성된 API 서버에서 요구사항을 구현하려고 합니다:

  • Spring Boot
  • Gradle
  • Kotlin

(2) 요구사항
사용자가 특정 행위를 할 때 경험치를 부여합니다:

  1. 좋아요 버튼을 누를 때(/customer/emoji/like) 경험치 100을 부여합니다.
  2. 게시글을 작성할 때(/customer/challenge) 경험치 100을 부여합니다.

(3) AOP를 사용한 이유
좋아요를 누르거나 게시글을 작성할 때 경험치 100을 부여하는 로직이 반복되므로, 이를 공통화하는 방법을 고민하게 되었습니다. 매번 같은 로직을 각 행위 로직에 추가하는 것보다 어노테이션을 사용하여 공통화하는 것이 더 효율적일 것으로 판단했습니다.

어드바이스 구현

경험치 부여 로직을 구현하기 전에 먼저 커스텀 어노테이션을 정의해야 합니다. 아래는 @GrantXp 어노테이션의 예시입니다:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GrantXp {
    int value() default 100;
}

 

그리고 타겟이 되는 엔드포인트에 어노테이션을 추가할 수 있습니다:

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

@PostMapping("/customer/photo")
@GrantXp(value = 200)
fun save(@RequestBody @Valid request: ChallengeSaveReq): ResponseEntity<BaseResp> {
    return getRespEntity(service.save(request))
}

어드바이스 구현

어노테이션을 준비한 후에는 @GrantXp로 어노테이션된 메서드가 실행될 때 경험치를 부여하는 로직을 구현해야 합니다. 아래는 어드바이스 구현 예시입니다:

@Aspect
@Component
class GrantXpAspect {

    @Autowired
    private lateinit var customerService: CustomerService

    @Around("@annotation(grantXp)")
    fun grantAspect(joinPoint: ProceedingJoinPoint, grantXp: GrantXp) {
        val result = joinPoint.proceed()
        customerService.gainXp(userId = getUserId(), xp = grantXp.value)
    }
}

 

이 어드바이스가 제대로 동작하려면 @EnableAspectJAutoProxy를 등록해야 합니다.

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    // 기타 빈 설정
}

테스트 코드

테스트 코드를 통해 경험치 부여 기능이 정상적으로 동작하는지 확인할 수 있습니다.

@SpringBootTest
@AutoConfigureMockMvc
internal class XpProcessorTest {

    @Autowired
    lateinit var mockMvc: MockMvc
    @Autowired
    private lateinit var customerService: CustomerService

    @Test
    @DisplayName("게시글 등록 시 경험치가 누적된다.")
    fun test2() {
        // 게시글 등록 요청
        mockMvc.perform(
            post("/api/customer/challenge")
                .header(AUTHORIZATION, customerToken)
                .contentType(APPLICATION_JSON)
                // set content
        )
            .andDo { println(it.response) }
            .andExpect(status().isOk)

        // 사용자의 누적 경험치가 갱신되었는지 확인
        val customer = customerService.findByAuthReq(customerAuthReq)
        val xpPoint = customer.xpPoint
        Assertions.assertEquals(100, xpPoint)
    }

    @Test
    @DisplayName("좋아요 클릭 시 경험치가 누적된다.")
    fun test3() {
        // 좋아요 요청
        mockMvc.perform(
            post("/api/customer/emoji/like")
                .header(AUTHORIZATION, customerToken)
                .contentType(APPLICATION_JSON)
                // set param
        )
            .andDo { println(it.response) }
            .andExpect(status().isOk)

        // 사용자의 누적 경험치가 갱신되었는지 확인
        val customer = customerService.findByAuthReq(customerAuthReq)
        val xpPoint = customer.xpPoint
        Assertions.assertEquals(100, xpPoint)
    }
}

결론

이 포스트에서는 Spring의 AOP와 사용자 정의 어노테이션을 사용하여 경험치 부여 로직을 효율적으로 관리하는 방법을 살펴보았습니다. 그러나 아직 개선해야 할 사항이 있습니다.

 

(1) 현재 구현에서는 모든 활동에 대해 동일한 경험치를 부여하도록 되어 있습니다. 하지만, 좋아요를 누르거나 사진을 제출할 때 각각 다른 경험치를 부여하고 싶을 수 있습니다. 이를 위해서는 어노테이션에 경험치 값을 동적으로 전달할 수 있는 방법을 고려해야 합니다.

 

(2) 또한, 특정 조건을 만족할때만(ex.이번달에 등록된 게시글에 좋아요를 눌렀을 때만) 경험치를 부여하고 싶을 수 있습니다. 예를 들어, 특정 게시글에 좋아요를 누르는 경우에만 경험치를 부여하는 등의 조건적인 로직을 구현하기 위해, 경험치 부여 대상인지 판별하는 추가적인 로직이 필요합니다.

 

이를 위해 다음 포스트에서는 인터페이스를 활용하여 경험치 부여 로직을 더욱 유연하게 구현하는 방법을 다루겠습니다.

Comments