문제 인식
Distributed Lock AOP 를 추가한 이후 프론트엔드에서 테스트를 진행하는데

서버오류가 발생했다........ 대체 왜!!! ㅠㅠ
Transaction 이랑 Lock 사이 race condition 때문에 손 바들바들 떨면서 날을 보낸지라 진짜 꼴보기 싫어서 약간 유기하고싶었지만... 너무 중요한 부분이라 지금 하기
에러메시지를 확인했다

| java.lang.IllegalStateException: Required to bind 2 arguments, but only bound 1 (JoinPointMatch was NOT bound in invocation) at org.springframework.aop.aspectj.AbstractAspectJAdvice.argBinding(AbstractAspectJAdvice.java:606) ~[spring-aop-6.2.18.jar:6.2.18] at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:631) ~[spring-aop-6.2.18.jar:6.2.18] at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71) ~[spring-aop-6.2.18.jar:6.2.18] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:173) ~[spring-aop-6.2.18.jar:6.2.18] at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728) ~[spring-aop-6.2.18.jar:6.2.18] at cohttp://m.forpets.domain.reservation.service.ReservationService$SpringCGLIB$0.complete(<generated>) ~[main/:na] at cohttp://m.forpets.domain.reservation.controller.ReservationController.complete(ReservationController.java:57) |
Spring AOP 바인딩 버그
현재 complete() 메서드에 AOP 가 2개 적용되어있다.
// 분산락 AOP, 모니터링용 tracking AOP
@DistributedLock(key = "'reservation:' + #reservationId")
@TrackExecutionTime("reservation.complete")
@Transactional
public ReservationResponseDto complete(Long memberId, Long reservationId) {
// 생략
}
두 개 모두 advice method parameter 로 바인딩 하는 형태인데
Spring AOP 가 두 Advice 를 chaining 할 때 두 번째 진입에서 JoinPointMatch (파라미터명 -> 인덱스 매핑) 이 누락되는 오류
--> 2개 바인딩이 필요한데 1개만 바인딩 되었다는 메시지가 출력된다.
(Required to bind 2 arguments, but only bound 1)
... 맞다.... ... ( 당연히...) 이해를 못 했음 ㅎㅎ 단어가 너무 어려워
Advice 는 @Around 가 붙은 메서드 그 자체라고 한다. 즉 원래 메서드 전후에 끼어들어서 실행되는 코드
여기서는 lock(), measure() 같은 메서드를 말함
그렇다면 Advice Method Parameter 는 ProceedingJoinPoint joinPoint, distributedLock distributedLock 정도가 있다
그럼 파라미터를 바인딩해준다 << 고 할 때 Binding 은 AOP 가 Advice Method Parameter 에 값을 넣어주는것
jointpoint 와 distributedLock 에 Spring 이 자동으로 값을 넣어주는데
이 때 두 자리에 뭘 넣어줄 지 Spring 이 알아서 채워주는 형태를 바인딩이라고 한다
체이닝은 한 메서드에 Advice 가 여러개 붙으면 줄줄이 호출되는 형태인데
예를들어
RedisLockAspect.lock() -> proceed() 가 호출되고 이어서 바로
ExecutionTimeAspect.measure() -> proceed() 가 호출 된 이후에
실제 complete() 가 실행되는 이 형태를 체이닝이라고 한다.
해결 과정 1
두 advice 에 argNames 를 명시에서 바인딩을 강제로 알려주기
// 기존 형태
// 분산락 AOP
@Around("@annotation(distributedLock)")
public Object lock(ProceedingJoinPoint joinPoint,
DistributedLock distributedLock) throws Throwable { }
// 시간측정 모니터링용도 AOP
@Around("@annotation(trackExecutionTime)")
public Object measure(ProceedingJoinPoint joinPoint,
TrackExecutionTime trackExecutionTime) throws Throwable { }
// 수정 형태 : Aspect 두개에 모두 argNames 명시
@Around(value = "@annotation(trackExecutionTime)", argNames = "joinPoint,trackExecutionTime")
@Around(value = "@annotation(distributedLock)", argNames = "joinPoint,distributedLock")
... 하지만 여전히 똑같은 오류가 뜬다
argName 를 작성한다 == advice parameter 이름을 직접 알려준다 의 의미이므로
Spring 이 자동으로 파라미터를 넣어주다가 오류가 나지 않도록 명시해주는 느낌이다...
여기선 해결방법이 되진 않았다
해결 과정 2
아예 advice 메서드 파라미터에서 annotation 을 제외하고 JoinPoint 에서 reflection 으로 꺼내는 방식을 사용해보기
사실 이게 표준이라고 한다
reflection: runtime 에 코드가 자기 자신 (class, method, annotation) 을 들여다보는 기능
즉 joinPoint 만 가져온 다음에 직접 signature.getMethod().getAnnotation() 이런식으로 가져오는 것을 말한다.
원래 쓰던 방식은 컴파일 타임에 메서드, 어노테이션 정보를 조회하고 있던 방식!
차이점
| 형태 | 의미 | 바인딩 |
| @annoation(distributedLock) | 어노테이션이 붙은 메서드를 매칭하고 어노테이션 인스턴스를 파라미터에 넣어줘 |
Spring 이 채워줌 |
| @annotation (com.forpets.. .DistributedLock) |
어노테이션이 붙은 메서드 매칭만 해줘 | 내가 Reflection 으로 꺼냄 signature.getMethod().getAnnotation() |
annotation 뒤에 붙은게 소문자면 바인딩 해주라는 의미고, 풀 클래스 명을 작성하면 그냥 매칭만 해달라는 의미!
이번 프로젝트에서 바인딩에서 오류가 났기 때문에 매칭만 해달라고 풀클래스 명 작성으로 우회
// 기존 방식: parameter 로 직접 DistributedLock 접근해서 key 가져옴
@Around(value = "@annotation(distributedLock)", argNames = "joinPoint,distributedLock")
public Object lock(ProceedingJoinPoint joinPoint,
DistributedLock distributedLock) throws Throwable {
String lockKey = LOCK_PREFIX+ parseKey(distributedLock.key(), joinPoint);
}
// 수정
// annotation 은 클래스 명을 다 적어줘서 매칭만 해주고
// 인스턴스는 Method().getAnnotation 으로 가져와서 .key() 로 reservationId 에 접근
@Around("@annotation(com.forpets.global.aspect.DistributedLock)")
public Object lock(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
DistributedLock distributedLock = signature.getMethod().getAnnotation(DistributedLock.class);
String lockKey = LOCK_PREFIX+ parseKey(distributedLock.key(), joinPoint);
}
이렇게 되면 JoinpointMatch 바인딩 자체가 필요없어지기 때문에 문제가 해결됨 !!
회고
AOP 가 알아서 채워주는 거에 의존하지 말고... 직접 꺼내 쓰는 방법도 알아두기 (reflection)