Cloud Native Spring in Action

중앙식 예외 핸들러와 이를 이용한 오류 처리

기록해연 2025. 2. 6. 16:30

112p ~

 

@RestControllerAdvice 클래스는 예외와 상태 코드 사이의 매핑을 제공. 

카탈로그에 이미 있는 책 생성시 422(처리할 수 없는 개체), 존재하지 않는 책을 가져오려할 때는 404(찾을 수 없음), Book 객체에서 하나 이상의 필드가 잘못되었을 때는 400(잘못된 요청) 응답 반환.

 

package com.polarbookshop.catalogservice.web;

import com.polarbookshop.catalogservice.domain.BookAlreadyExistsException;
import com.polarbookshop.catalogservice.domain.BookNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

// 클래스가 중앙식 예외 핸들러임을 표시
@RestControllerAdvice
public class BookControllerAdvice {
    // 해당 핸들러가 실행되어야 할 대상인 예외 정의
    @ExceptionHandler(BookNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    String bookNotFoundHandler(BookNotFoundException ex) {
        // HTTP 응답 본문에 포함할 메시지
        return ex.getMessage();
    }

    @ExceptionHandler(BookAlreadyExistsException.class)
    @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
    String bookAlreadyExistsHandler(BookAlreadyExistsException ex) {
        return ex.getMessage();
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
        var errors = new HashMap<String, String>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            // 빈 메시지 대신 의미 있는 오류 메시지를 위해 유효하지 않은 필드 확인
            errors.put(fieldName, errorMessage);
        });
        return errors;
    }
}

 

다시 빌드, 실행하여 위의 핸들러에 걸리는지 확인해보기.

(윈도우의 명령어는 gradlew.bat bootRun)

http POST :9001/books author="" title="" isbn="0004567891" price=9.55

ㅋㅋㅋㅋ 서버오류가 나버림 400을 바랬거늘 어찌하여.....하고 알게된 사실

 

@NotBlank → 문자열(String)에만 적용 가능, Double에는 사용 불가능

내가 @NotNull을 @NotBlank로 잘못 썼더라고...? 눈알이 옹이구멍인듯.

 

package com.polarbookshop.catalogservice.domain;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;

// 도메인 객체(Entity) 정의
// 도메인 모델은 불가변 객체인 레코드로 구현
public record Book(
        @NotBlank(message = "The book ISBN must be defined")
        @Pattern(
                regexp = "^[0-9]{10}|[0-9]{13}$",
                message = "The ISBN format must be valid"
        )
        String isbn,    // 책 고유번호(식별)
        @NotBlank(message = "The book title must be defined")
        String title,
        @NotBlank(message = "The book author must be defined")
        String author,
        @NotNull(message = "The book price must be defined")
        // @Positive: 널 값이어서는 안되고 0보다 큰 값이어야함.
        @Positive(message = "The book price must be greater than zero")
        Double price
) { }

 

빠른 수정 후 재도전.

400 에러 성공~


그리고 잠시 중앙식 예외 핸들러에 대해서 챗지선생님의 정리타임이 있겠습니다....

 

📌 스프링의 중앙식 예외 핸들러 (Global Exception Handling)란?

스프링에서 중앙식 예외 핸들러애플리케이션 전역에서 발생하는 예외를 한 곳에서 처리할 수 있도록 도와주는 기능입니다.
이를 통해 각 컨트롤러에서 개별적으로 예외 처리를 하지 않아도 공통된 로직을 적용할 수 있습니다.


✅ 1. 중앙식 예외 처리의 필요성

보통 컨트롤러에서 예외가 발생하면 이를 try-catch로 개별 처리하는데,
모든 컨트롤러에서 같은 방식으로 예외를 처리하면 코드 중복이 심해지고 유지보수가 어려워집니다.

💡 중앙식 예외 핸들러를 사용하면 모든 컨트롤러에서 발생하는 예외를 한 곳에서 일괄 처리할 수 있습니다.


✅ 2. @ControllerAdvice와 @ExceptionHandler를 사용한 전역 예외 처리

스프링에서는 @ControllerAdvice@ExceptionHandler 를 사용하여 중앙식 예외 처리를 구현할 수 있습니다.

📌 @ControllerAdvice

  • 모든 컨트롤러에서 발생하는 예외를 처리할 수 있도록 해주는 전역 예외 처리 클래스
  • @ExceptionHandler와 함께 사용하여 특정 예외를 처리함

📌 @ExceptionHandler(Exception.class)

  • 특정 예외가 발생했을 때 실행되는 메서드
  • 여러 개의 예외를 등록하여 다양한 예외를 처리할 수 있음

✅ 3. 중앙식 예외 핸들러 구현 예제

 

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

// 예외 처리를 담당하는 클래스
@ControllerAdvice
public class GlobalExceptionHandler {

    // 📌 특정 예외 처리 (예: BookNotFoundException)
    @ExceptionHandler(BookNotFoundException.class)
    public ResponseEntity<String> handleBookNotFoundException(BookNotFoundException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }

    // 📌 모든 예외를 처리하는 기본 핸들러
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralException(Exception ex) {
        return new ResponseEntity<>("서버 오류 발생: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

 

이렇게 하면 컨트롤러에서 BookNotFoundException 예외가 발생하면 404 Not Found를 응답으로 보내고,
그 외 모든 예외는 500 Internal Server Error로 처리됩니다.


✅ 4. @ResponseStatus를 활용한 간단한 예외 처리

스프링에서는 예외 클래스 자체에 HTTP 상태 코드를 부여할 수도 있습니다.

📌 예외 클래스 정의

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND) // 404 반환
public class BookNotFoundException extends RuntimeException {
    public BookNotFoundException(String message) {
        super(message);
    }
}

✅ 이렇게 하면 컨트롤러에서 throw new BookNotFoundException("해당 책을 찾을 수 없습니다.");
✅ 예외를 발생시키면 자동으로 404 Not Found 응답이 반환됩니다.


✅ 5. REST API에서 @RestControllerAdvice 사용

REST API에서는 예외 발생 시 JSON 형식으로 응답을 보내는 것이 일반적입니다.
이 경우 **@RestControllerAdvice**를 사용하면 JSON 응답을 쉽게 관리할 수 있습니다.

 

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 // REST 컨트롤러 전용 예외 처리
public class GlobalRestExceptionHandler {

    @ExceptionHandler(BookNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleBookNotFoundException(BookNotFoundException ex) {
        ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
        ErrorResponse errorResponse = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버 오류 발생");
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

// JSON 응답용 DTO
class ErrorResponse {
    private int status;
    private String message;

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

    // Getter & Setter
    public int getStatus() { return status; }
    public String getMessage() { return message; }
}

✅ 이렇게 하면 예외 발생 시 JSON 형식으로 상태 코드와 메시지를 반환할 수 있습니다.


✅ 6. 중앙식 예외 처리의 장점

장점설명

코드 중복 감소 모든 컨트롤러에서 개별적으로 예외를 처리할 필요 없음
유지보수 용이 예외 처리를 한 곳에서 관리 가능
일관된 응답 형식 유지 REST API의 경우 JSON 형식으로 일관된 응답 제공 가능
보안성 증가 내부 서버 오류가 클라이언트에게 노출되지 않도록 관리 가능

🚀 정리

  • @ControllerAdvice → 모든 컨트롤러에서 발생하는 예외를 한 곳에서 처리 가능
  • @ExceptionHandler → 특정 예외를 처리할 메서드 지정
  • @RestControllerAdvice → REST API에서 JSON 형태의 예외 응답 반환
  • @ResponseStatus → 예외 클래스에 HTTP 상태 코드를 직접 부여 가능

이제 중앙식 예외 처리를 활용하면 프로젝트에서 예외 처리를 더 체계적으로 관리할 수 있습니다! 🚀😊