Spring 입문 (예외 처리)

KimGwangmin·2026년 9월 8일

스프링 예외 처리

기존 실습에서, 잘못된 요청(존재하지 않는 유저에 대한 GET 요청)을 보내면 다음과 같은 응답이 온다.

참고로, 브라우저에서 동일한 요청을 보내면 다음과 같은 화면이 나타난다.

500은 서버의 잘못으로 응답에 실패했음을 의미한다. 잘못된 요청은 클라이언트의 잘못이므로 적절한 에러 코드가 아니다. 예외처리를 구현하면 잘못된 상황에서 올바른 예외 응답을 보낼 수 있다.
자바의 try-catch 구문을 써도 되지만, 스프링에서 제공하는 도구들이 있다.

충돌 시 우선순위가 높은 순서대로,

  1. @ExceptionHandler
  2. @RestControllerAdvice
  3. Spring 기본 예외 처리

가 존재한다.

1. @ExceptionHandler

  • 개별 Controller 단위로 핸들러 메서드를 등록하는 방식이다.
  • UserController에 다음 메서드를 추가한다.
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<String> handlerIllegalStateException(IllegalStateException exception) {
	return ResponseEntity
    		.status(HttpStatus.BAD_REQUEST)
            .body("Error: "+exception.getMessage());
}

Postman API 호출 결과

브라우저 접속 결과

  • 개별 컨트롤러마다 설정해줘야 하니, 중복이 심해져 잘 사용하지 않는다.

2. @RestControllerAdvice

  • 전역 예외 처리 방식이다.
  • 사실상 표준에 가까운 방법이다.
  • common 패키지를 추가하고, 그 안에 GlobalExceptionHandler 클래스를 작성하자.
  • 메서드 선언 방식은 동일하다.
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IllegalStateException.class)
    public ResponseEntity<String> handlerIllegalStateException(IllegalStateException exception) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Error: "+exception.getMessage());
    }
}
  • 전역적으로 IllegalStateException이 발생하는 모든 상황에 대해 공통적으로 처리할 수 있다.

로그

로그 레벨

로그의 중요도를 나눈 체계이다.

로그 레벨설명
TRACE가장 상세한 흐름 정보(거의 안씀)
DEBUG개발 단계의 디버깅을 위한 상세 정보
INFO중요한 비즈니스 흐름, 운영 정보
WARN당장 문제는 아니지만, 잠재적 위험 경고
ERROR기능 수행이 불가능한 심각한 오류
  • application.properties에서 logging.level.root=WARN과 같은 라인을 추가하여 로그 레벨을 설정할 수 있다. (기본은 INFO)
  • 설정한 레벨 이상의 로그들만 나타난다.

@Slf4j

  • 클래스에 이 어노테이션을 붙이면, log.info()와 같은 메서드들을 클래스 내에서 사용할 수 있다.
  • System.out.println에 비해 성능이 좋고, 설정한 문자열 외에도 자동으로 여러 정보를 기록해주기에 디버깅에는 이 어노테이션을 사용하는 것이 좋다.

커스텀 에러 클래스

  • 자바 기본 예외는 범용적이라, 예외 상황을 곧바로 파악하기 어렵다.
  • RuntimeException을 상속받아 특정 상황에 맞는 예외를 직접 만들어 사용할 수 있다.

먼저 HTTP 에러 코드 정보를 보유한, 범용 부모 클래스를 정의한다. (핸들러가 공통의 진입점을 가질 수 있기 위함이다.)

@Getter
public class ServiceException extends RuntimeException {
    private final HttpStatus status;
    
    public ServiceException(HttpStatus status, String message) {
        super(message);
        this.status = status;
    }
}

이제 이 클래스를 상속받아 특정 상황에 대한 예외 클래스를 작성할 수 있다.

public class UserNotFoundException extends ServiceException {
    public UserNotFoundException(String message) {
        super(HttpStatus.NOT_FOUND, message);
    }
}

Service에서 해당 예외를 발생시킨다.

@Transactional  
public GetUserResponse getOne(Long userId) {  
    User user = userRepository.findById(userId).orElseThrow(  
            () -> new UserNotFoundException("User not found.")  
    );  
    return GetUserResponse.from(user);  
}

핸들러는 다음과 같이 처리할 수 있다.

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ServiceException.class)
    public ResponseEntity<String> handlerServiceException(ServiceException exception) {
        return ResponseEntity
                .status(exception.getStatus())
                .body("Error: "+exception.getMessage());
    }
}

올바르게 처리되는 것을 확인할 수 있다.

Bean Validation Error

Bean Validation은 내부적으로 MethodArgumentNotValidException를 던진다.

핸들러에게 다음 메서드를 추가해주면, 클라이언트에게 원하는 에러 메시지를 보여줄 수 있다.

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
    String errorMessage = ex.getBindingResult().getFieldErrors().stream()
            .findFirst() // 첫 번째 에러를 Optional로 가져옴
            .map(fieldError -> fieldError.getDefaultMessage()) // 있다면 메시지로 변환
            .orElse("invalid value."); // 없다면 기본 메시지 사용

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMessage);
}

0개의 댓글