본문 바로가기

트러블슈팅

@Retryable 사용 시 'Cannot locate recovery method' 500 에러 해결하기

Spring Retry는 네트워크 오류나 DB 비관적 락 구현 등의 상황에서 유용합니다. 

오늘은 비관적 락 구현을 위해 사용한 @Retryable에서 발생했던 500 에러를 바탕으로 @Retryable과 @Recover의 동작 원리와 주의점을 정리해 보겠습니다.

 

 

@Retryable의 동작 원리

 

@Retryable이 붙은 메서드가 포함된 클래스는 Spring에 의해 프록시 객체로 빈 등록이 됩니다.

외부에서 메서드를 호출하면 프록시가 가로채서 다음과 같은 흐름으로 로직을 처리합니다.

 

 

재시도 및 복구 흐름

 

비즈니스 로직 실행 중 에러가 발생하면 설정에 따라 분기됩니다.

  1. include에 정의된 에러: 정의된 횟수만큼 재시도를 반복합니다.
    • 횟수를 초과하면 해당 에러를 처리할 @Recover 메서드를 찾아 실행합니다.
  2. 정의되지 않은 에러 (또는 exclude된 에러): 재시도를 하지 않고 즉시 해당 에러를 처리할 @Recover 메서드를 찾습니다.

 

'Cannot locate recovery method' 500 에러

 

재시도가 끝나거나 즉시 실패했을 때, 프레임워크는 발생한 예외 타입을 파라미터로 받는 @Recover 메서드를 찾습니다.

 

이때 비즈니스 로직에서 CustomException이 발생했는데 이를 처리할 @Recover 메서드가 정의되어 있지 않다면?

실제 비즈니스 에러 대신 Cannot locate recovery method라는 500 에러를 반환하게 됩니다.

 

따라서 실제 에러 내용을 명확히 클라이언트에게 전달하고 싶다면 반드시 발생 가능한 예외(CustomException 등)를 파라미터로 가지는 @Recover 메서드를 정의해야 합니다.

 

/**
* OptimisticLockingFailureException가 아닌 CustomException이 발생했을 때 재시도 없이 예외를 던집니다.
* @param e @Retryable에서 발생한 예외
*/
@Recover
public PointHistory recover(CustomException e) {
	log.warn("포인트 업데이트 중 비즈니스 로직 예외 발생: {}", e.getMessage());
	throw e;
}

 

해당 @Recover 메서드를 정의하면 비즈니스 로직 실행 중 CustomException이 발생했을 때

인터셉터는 즉시 해당 메서드를 찾아 에러를 던지게 되며, GlobalExceptionHandler에 의해 클라이언트에게 명확한 에러 코드 및 메세지를 반환할 수 있습니다.

 

 

디버깅 결과, CustomException 발생 시 남은 로직들은 무시한 채 @Recover 메서드로 향하는 것을 확인할 수 있었습니다.

 

 

@Retryable의 exclude 속성에 넣어도 500 에러가 나는 이유

 

처음에는 단순히 재시도를 하지 않도록 @Retryable(exclude = CustomException.class)로 설정하면 원하는 대로 실제 에러가 던져지지 않을까 했지만 exclude 속성에 포함해도 @Recover 메서드가 없으면 동일한 500 에러가 발생합니다!

 

  • exclude: 재시도 여부만 결정합니다.
  • 프레임워크: 이미 @Retryable을 통해 인터셉터에 의해 실행되고 있는 메서드이므로, 재시도는 안 하겠지만 에러가 터졌으니 복구(Recover)는 시도하게 되며 이때 @Recover 메서드가 없으면 동일하게 500 에러를 반환합니다.

 

결국 exclude 여부와 상관없이, 에러가 발생한 시점에서 프레임워크는 복구 프로세스로 진입하기 때문에 적절한 매칭 메서드가 존재해야 한다는 것을 알 수 있습니다.