Spring Security 환경에서 예외 처리

이지니·2025년 12월 15일

TIL

목록 보기
11/11

리뷰 작성 테스트를 진행 하던 중에 원하는 오류 내용과 다른 오류가 발생하는 문제를 확인했습니다.
이 문제를 해결하며 배운 오류 처리 흐름에 대해 기록합니다.


1. 문제 상황

  • 공통 예외 응답 처리를 위해 BizException을 작성하고, 패키지로 관리 중입니다.
  • 리뷰 서비스에서는 이 라이브러리의 의존성을 추가하여 사용합니다.

리뷰 작성 API를 수행할 때 아래의 비즈니스 로직을 수행합니다.

public void verifyUserCanReview(
        String accessToken,
        UUID authUserId,
        UUID orderId,
        UUID productId
    ) {
        String encodedJwt = "Bearer " + accessToken;

        OrderDetailResponse order =
            orderClient.getOrderDetail(encodedJwt, orderId);

        if(order == null) {
            throw new BizException(ReviewErrorCode.ORDER_INFO_NOT_FOUND);
        }

        // 주문자 == 현재 로그인 유저인지 확인
        if (!order.userId().equals(authUserId)) {
            throw new BizException(ReviewErrorCode.USER_NOT_ORDER_OWNER);
        }

        // 주문 안에 해당 상품이 포함되어 있는지 확인
        OrderItemResponse item = order.items().stream()
            .filter(i -> i.productId().equals(productId))
            .findFirst()
            .orElseThrow(() -> new BizException(ReviewErrorCode.PRODUCT_NOT_IN_ORDER));

        // 주문 상태가 리뷰 가능한 상태인지 확인 (배송 완료)
        if (!item.status().isReviewable()) {
            throw new BizException(ReviewErrorCode.ORDER_NOT_REVIEWABLE);
        }
    }
  • 기대 상황 : 주문 상태를 확인하고 ORDER_NOT_REVIEWABLE 오류 발생
  • 실제 결과 : 인증 오류 발생


2. 전체 호출 흐름 (문제 재현 시점)

Client
  → ReviewController
    → ReviewService
      → OrderClientService
        → OrderClient (feign, order-service 호출)
      → OrderClientService (검증 로직)
        → BizException(ORDER_NOT_REVIEWABLE) 발생

호출 흐름만 생각했을 때는 revier-service 내부 비즈니스 예외인데,
응답은 인증 오류처럼 보입니다.


3. 로그 확인

✅ 중요 포인트 :

  • POST /error가 추가로 호출된 것을 확인할 수 있습니다.
  • 그러나 실제로 클라이언트가 보낸 요청이 아닙니다.

4. 원인 해결

핵심 원인

직접 작성한 BizException이 처리되지 않고 있었습니다.

공통 모듈에 아래와 같은 전역 예외 처리기가 등록되어 있습니다.

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BizException.class)
    public ResponseEntity<ApiResponse<?>> handleBizError(BizException e) {
        return ResponseEntity
            .status(e.getResponseCode().getStatus())
            .body(ApiResponse.error(e.getResponseCode()));
    }
}

하지만 멀티 모듈 구조에서 GlobalExceptionHandler가 review-service의 컴포넌트 스캔 범위 밖에 위치하기 때문에 실제로는 review-service에서 등록되지 않고 있었던 것입니다.

Spring Boot의 예외 규칙

컨트롤러/서비스에서 발생한 예외가 @ExceptionHandler에 의해 처리되지 않으면
기본 에러 처리 경로 /error로 내부 디스패치(forward)한다.

즉,
1. BizException(ORDER_NOT_REVIEWABLE) 발생
2. review-service에서 이 예외를 잡아줄 @RestControllerAdvice가 실제로 적용되지 않음
3. 예외 미처리 → Spring이 /error로 포워딩
4. /error 요청이 다시 Spring Security 필터 체인을 탐
5. 이 과정에서 인증 정보가 없으므로 → 인증 오류(SERV-006) 응답이 생성됨

그래서 비즈니스 예외가 인증 오류로 덮여 보이는 현상이 발생한 것이었습니다.

해결 방법

review-service에 명시적으로 ExceptionHandler 등록

@RestControllerAdvice
public class CustomExceptionHandler extends GlobalExceptionHandler {
}

review-service 모듈 내부에 ExceptionHandler를 배치하면 컴포넌트 스캔 대상이 되면서 BizException이 정상적으로 처리됩니다.

정리

이번 이슈를 통해서 오류가 어디서 발생하는지보다 어디서 처리하는지가 더 중요하다는 것을 배울 수 있었습니다.
특히 Spring Security가 결합된 환경에서 예외 처리 흐름을 이해하지 못하고 개발하는 경우 실제 원인과 전혀 다른 오류가 전달될 수 있습니다. 이 혼선을 예방하기 위해 /error 요청을 permitAll() 로 분리하여 의도와 다른 오류로 고통받지 않도록 예외의 책임을 명확히 설계하는 습관을 들여야겠습니다.

profile
화이팅

0개의 댓글