카테고리 없음

Spring AOP 바인딩 오류

kjw81024 2026. 6. 2. 21:44

문제 인식 

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)