Spring Boot REST 예외 처리 — /error 리다이렉트 대신 JSON 에러 응답 구성하기

검증 환경

본 절은 원문 사례·문서에 등장한 버전/도구를 정리한 것이다. 별도 실험실 재현이 명시되지 않은 항목은 일반화하지 않는다.

항목본문·원문에서 확인된 범위
런타임/도구Spring Boot 1.2.1
런타임/도구Spring Boot 1.4
런타임/도구Spring Boot 1.3.1
런타임/도구Java 8
문서 정리일2026-08-31
  • OS/CI 세부 값은 프로젝트마다 다르므로, 적용 전 로컬에서 동일 오류 메시지를 재확인한다.

1. 문제 정의

Spring Boot 기반 REST 서버(Spring Boot 1.2.1 / Spring 4.1.5 / Java 8)에서 @RestController@RequestMapping으로 API를 구성하다 보면, 서비스 엔드포인트에서 예외외가 발생했을 때 /error 로 리다이렉트 되는 문제가 발생한다.

Spring Boot 문서에서 명시하듯 “Spring Boot는 기본적으로 /error 매핑핑를 제공하며, 서블릿 컨테이너에 전역 오류 페이지로 등록한다.” REST API 소비 예를 들어 Angular나 jQuery SPA라면 리다이렉트를 따르지 않고 그대로 응답 본문을 기대하기 때문에, 이 /error 리다이렉트는 사실상 쓸모가 없다.

목표: 컨트롤러 메서드에서 의도적으로 던지거나, Spring이 자동으로 생성한 예외(핸들러 매핑이 없는 URL 요청 시의 404 포함)를 가로채서 400/500/503/404 등의 정확한 상태 코드와 함께 일관된 JSON 에러 응답을 반환한다. 특히 예외를 NoSQL에 UUID와 함께 로깅한 뒤, 클라이언트에는 UUID와 HTTP 상태 코드를 JSON으로 돌려주는 패턴이 전형적이다.

대표 증상

1
2
3
GET /api/users/99999 -> 302 Found, Location: /error
(또는 컨트롤러에서 RuntimeException 발생 시 기본 /error 로 리다이렉트되어
  JSON 에러 바디 대신 오류 페이지 응답이 내려옴)

2. 먼저 확인할 것

해결책을 고르기 전에, 원인이 “컨트롤러 예외” 인지 “핸들러를 찾지 못한 404” 인지를 먼저 구분해야 한다. 이 둘은 잡는 지점이 다르다.

구분컨트롤러 예외핸들러 미발견 (404)
발생 지점@RequestMapping 메서드 실행 중DispatcherServlet이 매핑된 핸들러를 못 찾을 때
기본 동작/error 매핑으로 포워드정적 리소스 핸들러가 가로채거나 /error 로 이동
잡는 방법@ControllerAdvice / @RestControllerAdvicethrow-exception-if-no-handler-found + resources.add-mappings=false 설정 후 같은 advice로 처리
상태 코드예외/@ResponseStatus에 따라 결정404 Not Found

이 구분을 먼저 확인한 뒤 아래 문제 정의에 맞는 기존 동작을 재현해 보자.

1
2
3
4
# 핸들러가 있는 경로에 대해 예외를 유발 -> 리다이렉트 여부 확인
curl -si http://localhost:8080/api/users/1 | head -20
# 핸들러가 없는 경로 -> 404가 JSON이 아니라 잘못된 응답으로 오는지 확인
curl -si http://localhost:8080/no-such-endpoint | head -20

3. 원인별 분기표 (Fix Decision Tree)

증상이 어떤 갈레인지에 따라 선택할 구성이 달라진다. 아래 분기표를 따라 결정하라.

#상황확인 포인트선택 구성
A컨트롤러에서 던진 예외가 /error 로 포워드됨advice 없이도 동작 상태 확인@ControllerAdvice / @RestControllerAdvice 추가
B존재하지 않는 URL이 404가 아닌 다른 응답정적 리소스 핸들러가 가로챔spring.resources.add-mappings=false + spring.mvc.throw-exception-if-no-handler-found=true
C404도 JSON으로 통일하고 싶음 (A+B 결합)위 두 설정 후에도 404가 advice로 안 잡힘두 설정 모두 적용 + advice에서 NoHandlerFoundException 핸들러
D기본 /error 응답 형식 자체를 커스터마이즈상태 코드별 응답 본문을 바꾸고 싶음ErrorController (또는 BasicErrorController 상속) 오버라이드
E@EnableWebMvc를 advice에 함께 썼는데 부작용자동 설정 일부가 사라짐@EnableWebMvc 제거, Spring Boot 자동 설정 유지

4. 상황별 해결책

상황 A — 컨트롤러 예외를 JSON으로 (권장 기본)

별도 코드 없이 컨트롤러 예외를 전역으로 잡으려면 @ControllerAdvice를 하나 만든다. Spring Boot 1.4+/Spring 4.3+ 라면 @RestControllerAdvice를 쓰면 @ResponseBody를 매번 붙일 필요가 없다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = { Exception.class })
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiErrorResponse unknownException(Exception ex, WebRequest req) {
        // 여기서 예외를 UUID와 함께 NoSQL에 로그하고,
        // 클라이언트에는 로그 항목의 UUID를 담은 JSON을 반환한다.
        return new ApiErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage(), Instant.now());
    }
}

@RestControllerAdvice@ControllerAdvice + @ResponseBody의 조합이므로, 위처럼 @ExceptionHandler 메서드에서 객체를 그대로 반환하면 JSON으로 직렬화된다.

필요하다면 특징 예외를 개별 메서드로 분기한다 (Spring의 ResponseEntityExceptionHandler를 상속하면 spring.mvc.throw-exception-if-no-handler-found 로 던져지는 NoHandlerFoundException도 오버라이드 가능):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler({ HttpMessageNotReadableException.class,
                        MethodArgumentNotValidException.class,
                        HttpRequestMethodNotSupportedException.class })
    public ResponseEntity<Object> badRequest(HttpServletRequest req, Exception exception) {
        return ResponseEntity.badRequest().body(new ApiError(400, exception.getMessage(), Instant.now()));
    }

    @Override
    protected ResponseEntity<Object> handleNoHandlerFoundException(
            NoHandlerFoundException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
        Map<String, String> body = Map.of(
            "code", "1000",
            "message", "No handler found for your request.",
            "timestamp", Instant.now().toString());
        return new ResponseEntity<>(body, HttpStatus.NOT_FOUND);
    }
}

상황 B — 404를 NoHandlerFoundException으로 (application.properties)

존재하지 않는 URL이 정적 리소스 핸들러에 가로채져서 advice까지 도달하지 못하는 경우가 대부분. Spring Boot 1.3.1+ 에서는 다음 두 줄을 application.properties(또는 application.yml)에 추가한다. Boot 1.2.7 이하면 아래 DispatcherServlet 설정으로 같은 플래그를 켠다.

1
2
3
4
# 핸들러를 찾지 못하면 포워드 대신 NoHandlerFoundException을 던지게 함
spring.mvc.throw-exception-if-no-handler-found=true
# 정적 리소스 자동 매핑을 끔 (/** 로 먼저 가로채는 것을 차단)
spring.resources.add-mappings=false

왜 이 두 줄이 필요한가: Spring Boot 기본 정적 리소스 설정은 리소스 핸들러를 마지막 순서로 /** 에 매핑한다. 이 핸들러가 아직 처리가 안 된 요청을 먼저 받으므로 DispatcherServlet이 예외를 던질 수가 없다. 그리하여 404를 예외로 받으려면 리소스 매핑을 끊을 것과 동시에 throw-exception-if-no-handler-found를 켜야 한다.

Boot 1.2.7을 쓰는데 properties 키가 동작하지 않는다면, 기존 DispatcherServlet 빈에 플래그만 설정하는 방식이 덜 침습적이다:

1
2
3
4
5
6
7
8
9
@ComponentScan
@EnableAutoConfiguration
public class MyApplication extends SpringBootServletInitializer {
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(MyApplication.class, args);
        DispatcherServlet ds = (DispatcherServlet) ctx.getBean("dispatcherServlet");
        ds.setThrowExceptionIfNoHandlerFound(true);
    }
}

상황 D — /error 응답 자체를 커스텀 (ErrorController)

예외 핸들러가 아닌, 기본 /error 응답 형식을 상태 코드별로 바꾸고 싶다면 ErrorController(또는 BasicErrorController 상속)를 구현한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Controller
public class CustomErrorController extends BasicErrorController {

    public CustomErrorController(ServerProperties serverProperties) {
        super(new DefaultErrorAttributes(), serverProperties.getError());
    }

    @Override
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.INTERNAL_SERVER_ERROR) {
            return ResponseEntity.status(status).body(Map.of("message", "SERVER_ERROR"));
        } else if (status == HttpStatus.BAD_REQUEST) {
            return ResponseEntity.status(status).body(Map.of("message", "BAD_REQUEST"));
        }
        return super.error(request);
    }
}

5. 검증 명령

구성 후 아래로 상태 코드와 JSON 바디를 확인한다.

1
2
3
4
5
6
7
8
# 1) 핸들러가 있는 URL에서 예외 유발 -> 5xx + JSON 바디 확인
curl -si http://localhost:8080/api/users/1 | head -20

# 2) 존재하지 않는 URL -> 404 Not Found + JSON 바디 확인 (리다이렉트가 아님)
curl -si http://localhost:8080/no-such-endpoint | head -20

# 3) 리다이렉트 여부(Location: /error)가 사라졌는지 확인
curl -si -o /dev/null -w "HTTP %{http_code} redirect:%{redirect_url}\n" http://localhost:8080/no-such-endpoint

통합 테스트로 자동 검증하려면 MockMvc를 사용한다:

1
2
3
mockMvc.perform(get("/no-such-endpoint"))
       .andExpect(status().isNotFound())
       .andExpect(jsonPath("$.message").value("No handler found for your request."));

6. 예방 조치 (재발 방지)

항목조치
404 JSON화REST 전용 앱이면 spring.resources.add-mappings=false + throw-exception-if-no-handler-found=true를 프로젝트 표준 설정으로 고정
advice 표준화팀 공통 @RestControllerAdvice(또는 ResponseEntityExceptionHandler 상속)를 공용 라이브러리에 두고 모든 서비스가 재사용
예외 로깅advice에서 예외를 UUID와 함께 NoSQL에 로그하고, 클라이언트 응답에 UUID 포함하는 계약 표준화
테스트404/400/500 시나리오의 컨트롤러 테스트를 표준으로 작성해 응답 계약 회귀 방지
@EnableWebMvc 주의이 어노테이션은 Spring Boot의 대량의 유용한 자동 설정을 비활성화하므로, 특별한 이유가 없으면 advice에 붙이지 않는다

잘못된 해결책 (주의)

  • .에러 페이지로 포워드하는 ErrorController 예제만 붙여넣기 : 포워드를 없애지 않으면 여전히 리다이렉트/포워딩이 일어나 REST 클라이언트가 원하는 직접 응답을 못 받는다.
  • 예외 타입을 일일이 나열하는 것에 그치기 : Throwable 하나로 전역 커버가 가능하다. 타입마다 나열하느라 누락이 생겨 미처리 예외가 통째로 새는 경우가 많다.
  • 404만 @RequestMapping("*") 폴백으로 처리 : ModelAndView + error.html을 반환하므로 JSON 클라이언트에는 맞지 않는 접근이다.
  • @EnableWebMvc를 함부로 붙이기 : 컨트롤러 예외를 잡는 데 도움이 되어 보이지만 Spring Boot 자동 설정 상당수를 무력화하므로 극도로 주의해야 한다.

DevTrace verdict

이 문제의 핵심은 404가 advice로 안 들어오는 게 아니라, Spring Boot의 정적 리소스 핸들러가 /**로 요청을 먼저 가로채기 때문에 그에 앞서 리소스 매핑을 끄고 NoHandlerFoundException을 켜야 한다는 점이다.


출처: StackOverflow — Spring Boot REST service exception handling (Question 28902374)

본문 중 “일반적인 점검 기준"으로 명시된 부분(검증 명령, 예방 조치 표)은 출처 답변을 바탕으로 정리한 것이며, 적용 보조 환경(Spring Boot 버전 등)에 따라 결과가 다를 수 있습니다.

DevTrace 결론

일관된 JSON 에러 응답의 핵심은 컨트롤러 예외는 @ControllerAdvice로, 핸들러를 찾지 못한 404는 throw-exception-if-no-handler-found 설정으로 각각 잡아내는 두 갈래 구성이라는 점이다.

원문 출처는 문제 발견의 단서이며, 위 판단과 점검 항목은 DevTrace의 독자 분석이다.