본문 바로가기

Project

결제 시나리오 1. 결제 재시도 - Self-Invocation 문제

문제 상황

결제 시나리오 1번 재시도 로직을 구현하던 중 @Transactional 이 적용된 메서드 안에서 @Retryable 이 적용된 메서드를 호출하는데 @Retryable 이 적용되지 않는 문제를 발견했습니다.

[기대한 결과]

[실제 결과]

  • 실제 결과를 보면 예상과 달리 재시도 로직이 실행되지 않고 바로 예외가 터지는 모습을 보여줍니다.

해결 과정

위 문제를 해결하기 위해 총 7가지 테스트 케이스를 만들어 확인을 진행하였습니다.

@Retryable 단독 케이스(Case 1~3)

케이스  설명 결과
Case1 @Retryable 이 적용된 메서드 단독 실행 성공
Case2 같은 클래스 내부에서 일반 메서드가 @Retryable 메서드 호츌 실패
Case3 일반 메서드에서 다른 클래스의 @Retryable 메서드 호출 성공

@Transactional + @Retryable 조합 테스트 (Case 4~7)

케이스 설명 결과
Case4 @Transactional 이 적용된 메서드에서 같은 클래스의 @Retryable 메서드 호출 실패
Case5 같은 클래스 내에서 @Retryable 과 @Transactional 을 분리하여 호출 실패
Case6 @Transactional 이 적용된 메서드에서 다른 클래스의 @Retryable 메서드 호출 성공
Case7 일반 메서드에서 같은 클래스의 @Transactional 메서드와 다른 클래스 @Retryable 메서드 호출 성공

같은 클래스 내부에서 호출을 한다면 동작하지 않고 다른 클래스에서 주입을 받아 호출을한다면 정상적으로 동작한다는 것을 알 수 있었습니다.
다른 클래스에서 주입 받았을 경우 결과 재시도 로직이 정상적으로 수행됨을 보여줬습니다.

[원인 분석]

왜 같은 같은 클래스 내부에서 호출을 한다면 동작하지 않고 다른 클래스에서 주입을 받아 호출을한다면 정상적으로 동작하는지에 대해서 분석을 해보기로 했습니다.

원인은 Intellij 에서 제공하는 노란 밑 줄로 힌트를 얻었습니다.

  • 원인 분석 결과 Self-Invocation 문제였습니다. @Transactional, @Retryable 등과 같은 AOP Annotation은 프록시 객체를 통해 호출될 때만 동작을 합니다.
  • 하지만 동인 클래스 내부에서 직접 자신의 메서드를 호출하게 된다면 프록시 객체를 거치지 않고 원래 객체 메서드를 호출하게 되어 AOP Annotation 기능이 적용되지 않게 되는 것이었습니다.

[질문]

Q : 클래스 안에 AOP 어노테이션(@Transactional, @Retryable ...)을 사용하면 스프링 컨테이너에 등록할 때는 Proxy가 적용되는건가요?

A : 네 AOP 어노테이션이 적용된 클래스는 Spring 컨테이너에 등록될 때 프록시 객체가 저장됩니다. 즉 실제 Bean으로 등록되는 것은 원본 객체가 아니라 프록시 객체입니다.

[해결 방법]

Self-Invocation은 2가지 케이스로 해결할 수 있었습니다.

Case1. 다른 클래스에서 @Retryable 메서드 호출
→ @Retryable이 적용된 메서드를 별도 클래스로 분리해서 해결할 수 있습니다.

Case2. ApplicationContext에서 자기 자신을 주입 받아 호출
→ SpringContainer에서 자기 자신을 가져와 프록시를 거치도록 처리하여 해결할 수 있었습니다.


결론 및 회고

Case 2를 선택할 시 코드의 구조 변경은 하지 않지만 만약 다른 사람이 내 코드를 본다면 왜 이렇게 의도한지 눈에 들어오지 않아 직관성이 떨어질 것입니다. 코드는 나 자신 혼자 짜는 것이 아니라 협력하는 것이라 생각하여 코드가 조금 분리 되더라도 직관성을 해치지 않는게 우선이라 판단하여 Case 1을 선택하여 해결했습니다.

  1. 재시도 로직이 실행되지 않는 문제 발생
  2. 테스트 케이스를 통해 Self-Invocation 문제 라고 판단
  3. 클래스를 분리하여, 프록시를 주입해 문제해결

@Transactional과 @Retryable이 같은 클래스에서 함께 동작하지 않는 이유를 분석하면서 여태까지 애매하게 생각 되었던 Spring 컨테이너부터 어떻게 객체가 주입되고 Spring Aop가 프록시 기반으로 동작한다는 점과 Self-Invocation 문제가 발생하면 AOP가 적용되지 않는 다는 사실까지 알게되었습니다.