예외(Exceptions), Spring boot에서 예외처리 (@RestControllerAdvice + @ExceptionHandler)

예외(Exceptions)란?

 개발을 하다 보면 예기치 않은 상황이 발생할 수 있다. 예를 들어, DB에 저장하고자 하는 데이터가 Null 값이거나, 네트워크 연결이 끊겨 호출이 실패하거나, 파일 입출력에 문제가 생기는 등 다양한 예외 상황이 존재한다. 예외는 프로그램의 정상적인 흐름을 깨뜨릴 수 있기 때문에, 이들을 체계적으로 처리(Handling)하는 방식이 필요하다.

 

종류

  • Checked Exception: 컴파일 단계에서 체크되는 예외로, 반드시 처리해야 한다. 예를 들어, IOException, SQLException 등이 있다.
  • Unchecked Exception(런타임 예외): 컴파일 단계에서 체크되지 않는 예외로, 개발자의 부주의로 발생하는 경우가 많다. 예를 들어, NullPointerException, ArrayIndexOutOfBoundsException 등이 있다.

Java에서는 예외 처리를 위해 try-catch-finally 블록을 사용하고, 필요하다면 throw 키워드를 통해 예외를 직접 발생시킬 수 있다. 예외가 발생하면 그 시점 이후의 로직은 실행되지 않고, 해당 예외를 처리할 수 있는 catch 블록(혹은 상위 호출부의 예외 처리 블록)을 찾아 올라가면서(Exception Propagation) 예외가 처리된다.

 

스프링 부트에서 예외 처리는 어떻게 할까?

스프링 부트에서 예외 처리는 크게 다음과 같은 방식으로 진행된다.

  1. 기본 예외 처리
    • 스프링 MVC는 컨트롤러에서 발생한 예외를 처리할 때, 기본적으로 BasicErrorController를 사용해 에러 정보를 담은 JSON 혹은 HTML 형태의 응답을 반환한다.
  2. 컨트롤러 수준 예외 처리
    • @ExceptionHandler를 사용하여 특정 컨트롤러에 발생한 예외만 처리할 수 있다.
  3. 전역(Global) 예외 처리
    • @RestControllerAdvice 혹은 @ControllerAdvice를 사용하면, 전체 컨트롤러에서 발생하는 예외를 한 곳에서 통합적으로 처리할 수 있다.
  4. ResponseStatusException, @ResponseStatus
    • 예외에 HTTP 상태 코드를 직접 매핑할 수 있다.
    • 예: @ResponseStatus(HttpStatus.NOT_FOUND) 등으로 예외에 대한 응답 상태 코드를 지정.

 

예외 처리 예제 코드

커스텀 예외 작성하기

필요에 따라 프로젝트에서 자주 발생하거나 특정 상황을 표현하기 위한 커스텀 예외를 만들 수 있다. 주로 RuntimeException(Unchecked Exception)을 상속받아 사용하는 경우가 많습니다.

 

package com.example.demo.exception;

public class CustomException extends RuntimeException {

    private final String errorCode;

    public CustomException(String message, String errorCode) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}

 

  • RuntimeException을 상속받아 만듦으로써, 별도로 try-catch를 강제하지 않아도 된다.
  • 필요에 따라 errorCode와 같은 추가 정보를 담아둘 수 있다.

서비스 레이어에서 예외 발생시키기

 아래는 가상의 User 정보를 조회하는 예시이다. 만약 user 데이터가 존재하지 않는다면 CustomException을 던진다.

package com.example.demo.service;

import com.example.demo.exception.CustomException;
import com.example.demo.model.User;
import org.springframework.stereotype.Service;

@Service
public class SampleService {

    public User getUserById(Long userId) {
        // 가정: DB 조회 로직 대신 가짜 코드
        if (userId == null || userId <= 0) {
            throw new CustomException("존재하지 않는 사용자입니다.", "USER_NOT_FOUND");
        }

        // 실제로는 JPA나 MyBatis 등을 사용해 DB에서 유저를 조회
        return new User(userId, "홍길동");
    }
}

 

여기서 userId <= 0 혹은 null인 경우 가상의 예외 상황으로 간주하여 CustomException을 발생

 

Controller에서 호출

 예외 처리를 별도로 하지 않는 경우, 스프링 부트 기본 에러 핸들러가 동작하여 브라우저/클라이언트에 기본 에러 형식(JSON/HTML)을 반환한다.

package com.example.demo.controller;

import com.example.demo.model.User;
import com.example.demo.service.SampleService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController {

    private final SampleService sampleService;

    public SampleController(SampleService sampleService) {
        this.sampleService = sampleService;
    }

    @GetMapping("/users/{userId}")
    public User getUser(@PathVariable("userId") Long userId) {
        return sampleService.getUserById(userId);
    }
}

 

  • /users/{userId} 로 호출 시, userId가 0 이하이거나 null이면 CustomException이 발생
  • 정상적인 userId(예: 1L)를 넘기면 User 객체가 반환된다.

전역 예외 처리 - @RestControllerAdvice

 예외가 발생할 때, 응답 형식(예: JSON, HTTP Status 등)을 일관성 있게 관리하기 위해 전역 예외 처리를 등록하는 방법이 다.

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * CustomException 처리
     */
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ErrorResponse> handleCustomException(CustomException ex) {
        ErrorResponse errorResponse = new ErrorResponse(
            ex.getErrorCode(),
            ex.getMessage()
        );
        // 필요에 따라 HttpStatus를 다르게 설정할 수도 있습니다.
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(errorResponse);
    }

    /**
     * 그 외 발생할 수 있는 예외 처리
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception ex) {
        ErrorResponse errorResponse = new ErrorResponse(
            "INTERNAL_SERVER_ERROR",
            ex.getMessage()
        );
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(errorResponse);
    }

    /**
     * 에러 응답 형식 정의(내부 클래스 혹은 별도 파일)
     */
    public static class ErrorResponse {
        private String errorCode;
        private String message;

        public ErrorResponse(String errorCode, String message) {
            this.errorCode = errorCode;
            this.message = message;
        }

        // Getter/Setter
        public String getErrorCode() {
            return errorCode;
        }

        public String getMessage() {
            return message;
        }
    }
}

 

  • @RestControllerAdvice는 모든 @RestController에서 발생하는 예외를 잡아 처리할 수 있는 어노테이션
  • @ExceptionHandler(CustomException.class)를 통해 CustomException이 발생할 때 처리되는 로직을 작성한다.
  • @ExceptionHandler(Exception.class)를 통해 그 외 모든 예외를 처리할 수 있는 로직을 추가한다.
  • 상황에 따라 HTTP 상태 코드를 BAD_REQUEST(400)이 아닌 다른 값으로 설정할 수도 있다.
  1.