Backend/Spring Framework

[Spring Boot] AOP 단위 테스트 하기 (AspectJProxyFactory)

mopil 2023. 1. 13. 15:46
반응형

AOP로 기능을 구현하다 보면 다소 복잡한 기능을 추가적으로 구현해야 하는 경우가 있다.

기존까지 AOP기능은 통합 테스트를 진행했었는데, 이러면 테스트가 너무 무거워져서 단위 테스트 방법을 찾아봤다.

 

전적으로 다음 블로그를 많이 참조했다.

https://junhyunny.github.io/spring-boot/test-driven-development/improve-feign-client-aop-test/

 

FeignClient AOP 단위 테스트 개선하기

<br /><br />

junhyunny.github.io

 

# 배경

사용자가 방탈출에 관련한 인증 글을 작성했을 때, 10개 이상이 되면 탈출중독 이라는 뱃지를 추가하는 로직을 AOP로 작성했다.

 

# CheckBadgeAspect

@Aspect
@Component
class CheckBadgeAspect(private val userService: UserService) {
    val log = logger()

    @AfterReturning("@annotation(jsonweb.exitserver.util.badge.CheckBadge)")
    fun checkBadge(joinPoint: JoinPoint) {
        val signature = joinPoint.signature as MethodSignature
        val user = userService.getCurrentLoginUser()
		checkBoastBadge(user)
    }

    private fun checkBoastBadge(user: User) {
        // 인증 글 10개 작성, 탈출중독
        if (user.isNotGotten(BadgeEnum.ADDICTED_ESCAPE) &&
            user.myBoastList.size == 10
        ) {
            log.info("${user.nickname}님이 ${BadgeEnum.ADDICTED_ESCAPE.kor()} 뱃지 획득!")
            user.addBadge(BadgeEnum.ADDICTED_ESCAPE)
        }
    }
  }

인증 작성 로직은 핵심 비즈니스 로직이고, 글을 작성 후 뱃지의 개수를 카운트하는 작업은 분리하고 싶어서 AOP로 따로 제작했다.

@CheckBadge가 붙어있는 서비스 메소드에 AfterReturning (반환 완료) 후 해당 AOP가 수행된다.

 

# CheckBadge 어노테이션 사용 예시 (서비스 레이어)

@CheckBadge
@Transactional
fun createBoast(form: BoastRequest) {
    val user = userService.getCurrentLoginUser()
    val theme = themeRepository.findById(form.themeId).orElseThrow()
    val boast = boastRepository.save(Boast(
        user = user,
        theme = theme,
        imageUrl = form.imageUrl
    ))
    form.hashtags.forEach {
        boast.addHashtag(BoastHashtag(hashtag = "#$it", boast = boast))
    }
    user.addMyBoast(boast)
}

 

# 단위 테스트 작성하기

AspectJFactory를 통해서 BoastService 프록시 객체를 생성한다.

그리고 Aspect를 추가하고, BoastServiceProxy로 createBoast 메소드를 호출한다.

class BoastBadgeTest : AnnotationSpec() {
    private val userService: UserService = mockk()
    private val themeRepository: ThemeRepository = mockk()
    private val boastRepository: BoastRepository = mockk()
    private val boastLikeRepository: BoastLikeRepository = mockk()
    private val boastReportRepository: BoastReportRepository = mockk()

    private val boastService: BoastService = spyk(
        BoastService(userService, themeRepository, boastRepository, boastLikeRepository, boastReportRepository),
        recordPrivateCalls = true
    )

    private val mockUser = User(-1, "pwd", "male", "20~29", "랜덤 닉네임")
    private val mockTheme = Theme()

    lateinit var factory: AspectJProxyFactory
    lateinit var boastServiceProxy: BoastService

    @BeforeAll
    fun stub() {
        every { userService.getCurrentLoginUser() } returns mockUser
        every { themeRepository.findById(any()) } returns Optional.of(mockTheme)
        every { boastRepository.save(any()) } returns Boast(user = mockUser, theme = mockTheme, imageUrl = "")

        factory = AspectJProxyFactory(boastService)
        factory.addAspect(CheckBadgeAspect(userService))
        boastServiceProxy = factory.getProxy()
    }

    @AfterEach
    fun clear() {
        mockUser.clearMyBoast()
        mockUser.clearBadge()
    }

    @Test
    fun `인증 글 10개를 쓰면 탈출중독 뱃지를 얻는다`() {
        // given
        val form = BoastRequest(1L, "https://image.com/1", listOf("내용"))
        repeat(10) {
            boastServiceProxy.createBoast(form)
        }

        // when - then
        mockUser.badgeList[0].badge shouldBe BadgeEnum.ADDICTED_ESCAPE.kor()
    }
}

 

# 성공

항상 초록색은 기분 좋다.

AOP도 단위 테스트가 가능함을 알게 되었으니 앞으로 잘 활용해야겠다!

반응형