무던하게

스프링 부트(Spring Boot)는 웹 애플리케이션에서 발생하는 예외를 관리하고 사용자에게 적절한 응답을 제공하기 위한 강력한 예외 처리 메커니즘을 제공합니다. 이 글에서는 @ExceptionHandler@ControllerAdvice를 활용하여 예외를 처리하는 방법을 단계별로 알아봅니다.


1. 예외 처리의 필요성

예외 처리는 다음과 같은 이유로 중요합니다:

  • 사용자 경험 향상: 의미 있는 에러 메시지를 제공하여 사용자가 문제를 이해하도록 돕습니다.
  • 보안 강화: 내부 시스템 정보를 사용자에게 노출하지 않도록 보호합니다.
  • 유지보수성: 중앙화된 예외 처리를 통해 코드의 가독성과 유지보수성을 높입니다.

2. @ExceptionHandler로 개별 컨트롤러에서 예외 처리

@ExceptionHandler는 특정 컨트롤러에서 발생한 예외를 처리하는 메서드에 적용됩니다.

사용 예제: 개별 컨트롤러 예외 처리

Controller:

@RestController
@RequestMapping("/products")
public class ProductController {

    @GetMapping("/{id}")
    public Product getProductById(@PathVariable Long id) {
        // 예외를 강제로 발생시킴
        throw new ProductNotFoundException("Product with ID " + id + " not found.");
    }

    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<String> handleProductNotFoundException(ProductNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }
}

Custom Exception:

public class ProductNotFoundException extends RuntimeException {
    public ProductNotFoundException(String message) {
        super(message);
    }
}

설명:

  • @ExceptionHandler를 사용하여 특정 예외(ProductNotFoundException)를 처리합니다.
  • 예외 발생 시 handleProductNotFoundException 메서드가 호출되어 응답을 생성합니다.

3. @ControllerAdvice로 전역 예외 처리

@ControllerAdvice는 애플리케이션 전역에서 발생하는 예외를 처리할 수 있는 메커니즘을 제공합니다. 모든 컨트롤러에 동일한 예외 처리를 적용할 때 유용합니다.

사용 예제: 전역 예외 처리

GlobalExceptionHandler:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<String> handleProductNotFoundException(ProductNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred: " + ex.getMessage());
    }
}

설명:

  • @RestControllerAdvice는 모든 컨트롤러의 예외를 전역적으로 처리합니다.
  • 특정 예외(ProductNotFoundException)와 일반적인 예외(Exception)를 각각 처리합니다.
  • 처리 결과는 HTTP 상태 코드와 메시지로 반환됩니다.

4. @ExceptionHandler와 @ControllerAdvice의 비교

특징 @ExceptionHandler @ControllerAdvice
적용 범위 개별 컨트롤러 애플리케이션 전역
사용 목적 특정 컨트롤러에 한정된 예외 처리 모든 컨트롤러의 공통 예외 처리
구현 위치 컨트롤러 클래스 내부 별도의 클래스에서 관리
유지보수성 한정적이고 관리 대상이 많아질 수 있음 관리가 용이하고 재사용성이 높음

5. ResponseEntity와 커스텀 응답 생성

스프링의 ResponseEntity를 활용하면 예외 응답의 상태 코드, 헤더, 메시지를 자유롭게 커스터마이징할 수 있습니다.

사용 예제: 응답 커스터마이징

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleProductNotFoundException(ProductNotFoundException ex) {
        ErrorResponse errorResponse = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            System.currentTimeMillis()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
    }
}

ErrorResponse 객체:

public class ErrorResponse {
    private int status;
    private String message;
    private long timestamp;

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

    // Getters and Setters
}

결과 예시 (JSON):

{
    "status": 404,
    "message": "Product with ID 1 not found.",
    "timestamp": 1732113600000
}

6. @ControllerAdvice에서 데이터 유효성 검증 에러 처리

스프링 부트는 데이터 검증 실패 시 MethodArgumentNotValidException을 발생시킵니다. 이를 @ControllerAdvice에서 처리하여 사용자에게 친절한 메시지를 제공할 수 있습니다.

사용 예제: 유효성 검증 에러 처리

UserForm 클래스:

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Email;

public class UserForm {
    @NotEmpty(message = "사용자명을 입력하세요.")
    private String username;

    @Email(message = "유효하지 않은 이메일입니다.")
    private String email;

    // Getters and Setters
}

Controller:

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping
    public String createUser(@Valid @RequestBody UserForm userForm) {
        return "User created: " + userForm.getUsername();
    }
}

GlobalExceptionHandler:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage())
        );
        return ResponseEntity.badRequest().body(errors);
    }
}

결과 예시 (JSON):

{
    "username": "사용자명을 입력하세요.",
    "email": "유효하지 않은 이메일입니다."
}

7. 주요 개념 요약

  1. @ExceptionHandler: 개별 컨트롤러에서 발생한 예외를 처리합니다.
  2. @ControllerAdvice: 애플리케이션 전역에서 발생한 예외를 처리합니다.
  3. ResponseEntity: 상태 코드와 커스텀 응답 메시지를 유연하게 조합할 수 있습니다.
  4. 데이터 검증 예외 처리: @ValidBindingResult를 통해 사용자 입력 에러를 처리합니다.
profile

무던하게

@moodone

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!