카테고리 없음

📌 TIL - GlobalExceptionHandler를 통한 글로벌 예외 처리

creator7087 2025. 4. 14. 12:53

🧠 오늘 배운 핵심 내용

스프링 부트에서 발생 가능한 다양한 예외를 전역적으로 처리하기 위해 @RestControllerAdvice를 활용하고, 예외에 따라 HTTP 상태 코드, 메시지, 경로 등을 포맷화된 JSON 형태로 반환하는 방식 이해.

 

✅ 주요 예외 처리 흐름 정리

1️⃣ 커스텀 예외 처리: BaseException

  • 직접 정의한 BaseException을 잡아 예외 코드와 메시지를 응답에 담음.
  • 내부적으로 ErrorCode enum을 활용하여 메시지, 상태 코드 관리.

💡 팁
예외를 커스터마이징해 관리하면 재사용성과 유지보수성이 훨씬 높아집니다. 에러 메시지를 일관되게 관리할 수 있어 API 응답 품질이 향상됩니다.

 

2️⃣ @Valid 실패: MethodArgumentNotValidException

  • 필드 유효성 검증 실패 시 예외 발생
  • 실패한 필드명, 메시지를 fieldErrors로 묶어서 응답에 포함

💡 팁
유효성 검증 실패 시 어떤 필드가 문제였는지 알려주는 것은 사용자 친화적인 API 설계의 핵심입니다. 클라이언트 개발자가 디버깅하기 쉬워집니다

 

3️⃣ 주요 시스템 예외 핸들링

예외 클래스처리 내용

 

ConstraintViolationException DB 제약 조건 위반
MethodArgumentTypeMismatchException 타입 불일치 (예: int 자리에 abc 등)
MethodValidationException Javax/Spring validation 실패
MissingRequestHeaderException 필수 헤더 누락
MissingServletRequestParameterException 필수 쿼리 파라미터 누락
HttpMessageNotReadableException JSON body 파싱 불가
NoResourceFoundException 매핑된 핸들러 없음 (404)
HttpRequestMethodNotSupportedException 지원하지 않는 HTTP 메서드 사용
Exception 알 수 없는 예외 처리 (fallback)

. ✔️ 스프링이 위와 같은 예외들을 자동으로 감지하고 처리하긴 하지만,
응답은 대부분 500 또는 HTML/기본 에러 메시지로 처리되며, JSON 기반의 일관된 메시지나 상세 원인 제공은 불가능하기 때문에 👉 직접 @RestControllerAdvice로 커스터마이징한 것이다,

 

4️⃣ 응답 포맷 통일: buildErrorResponse()

private ResponseEntity<Map<String, Object>> buildErrorResponse(ErrorCode errorCode, String path,List<Map<String, String>> fieldErrors) {
    Map<String, Object> body = new LinkedHashMap<>();
    body.put("status", errorCode.getHttpStatus().value());
    body.put("error", errorCode.getHttpStatus().getReasonPhrase());
    body.put("code", errorCode.getErrorCode());
    body.put("message", errorCode.getMessage());
    body.put("path", path);
    body.put("timestamp", LocalDateTime.now());
    if (fieldErrors != null && !fieldErrors.isEmpty()) {
        body.put("fieldErrors", fieldErrors);
    }


    return new ResponseEntity<>(body, errorCode.getHttpStatus());
}
  • 통일된 JSON 구조를 반환
  • API 문서화 및 클라이언트 디버깅에 매우 유용

💡 팁
공통 에러 포맷은 API 문서에서 예외 응답 예시로 활용 가능하며, 프론트엔드 개발자와의 협업 시 커뮤니케이션 비용을 줄여줍니다.

 

5️⃣전체 코드

 

더보기
package inxj.newsfeed.exception;

import inxj.newsfeed.exception.customException.BaseException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.method.MethodValidationException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    // ✅ BaseException 처리
    @ExceptionHandler(BaseException.class)
    public ResponseEntity<Map<String, Object>> handleBaseException(BaseException ex, HttpServletRequest request) {
        return buildErrorResponse(ex.getErrorCode(), request.getRequestURI(),null);
    }

    // ✅ @Valid 유효성 검사 실패
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidationException(MethodArgumentNotValidException ex, HttpServletRequest request) {
        List<Map<String, String>> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
                .map(error -> Map.of(
                        "field", error.getField(),
                        "message", Optional.ofNullable(error.getDefaultMessage()).orElse("Validation failed")
                ))
                .collect(Collectors.toList());

        return buildErrorResponse(ErrorCode.VALID_ERROR, request.getRequestURI(),fieldErrors);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<Map<String, Object>> handleConstraintViolation(ConstraintViolationException ex, HttpServletRequest request) {
        return buildErrorResponse(ErrorCode.CONSTRAINT_VIOLATION, request.getRequestURI(),null);
    }

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResponseEntity<Map<String, Object>> handleTypeMismatch(MethodArgumentTypeMismatchException ex, HttpServletRequest request) {
        log.error("MethodArgumentTypeMismatchException : {}", ex.getMessage());
        return buildErrorResponse(ErrorCode.TYPE_MISMATCH, request.getRequestURI(),null);
    }

    @ExceptionHandler(MethodValidationException.class)
    public ResponseEntity<Map<String, Object>> handleValidationExceptionSpring6(MethodValidationException ex, HttpServletRequest request) {
        log.error("MethodValidationException : {}", ex.getMessage());
        return buildErrorResponse(ErrorCode.VALIDATION_FAILED, request.getRequestURI(),null);
    }

    @ExceptionHandler(MissingRequestHeaderException.class)
    public ResponseEntity<Map<String, Object>> handleMissingHeader(MissingRequestHeaderException ex, HttpServletRequest request) {
        log.error("MissingRequestHeaderException : {}", ex.getMessage());
        return buildErrorResponse(ErrorCode.MISSING_HEADER, request.getRequestURI(),null);
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ResponseEntity<Map<String, Object>> handleMissingParameter(MissingServletRequestParameterException ex, HttpServletRequest request) {
        log.error("MissingServletRequestParameterException : {}", ex.getMessage());
        return buildErrorResponse(ErrorCode.MISSING_PARAMETER, request.getRequestURI(),null);
    }

    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<Map<String, Object>> handleUnreadableMessage(HttpMessageNotReadableException ex, HttpServletRequest request) {
        log.error("HttpMessageNotReadableException : {}", ex.getMessage());
        return buildErrorResponse(ErrorCode.NOT_READABLE_MESSAGE, request.getRequestURI(),null);
    }

    @ExceptionHandler(NoResourceFoundException.class)
    public ResponseEntity<Map<String, Object>> handleNoHandler(NoResourceFoundException ex, HttpServletRequest request) {
        log.error("NoHandlerFoundException : {}", ex.getMessage());
        return buildErrorResponse(ErrorCode.NO_HANDLER_FOUND, request.getRequestURI(),null);
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResponseEntity<Map<String, Object>> handleUnsupportedMethod(HttpRequestMethodNotSupportedException ex, HttpServletRequest request) {
        log.error("HttpRequestMethodNotSupportedException : {}", ex.getMessage());
        return buildErrorResponse(ErrorCode.METHOD_NOT_SUPPORTED, request.getRequestURI(),null);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleUnknownException(Exception ex, HttpServletRequest request) {
        log.error("Exception : {} {}", ex.getClass(), ex.getMessage());
        return buildErrorResponse(ErrorCode.INTERNAL_ERROR, request.getRequestURI(),null);
    }
    private ResponseEntity<Map<String, Object>> buildErrorResponse(ErrorCode errorCode, String path,List<Map<String, String>> fieldErrors) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("status", errorCode.getHttpStatus().value());
        body.put("error", errorCode.getHttpStatus().getReasonPhrase());
        body.put("code", errorCode.getErrorCode());
        body.put("message", errorCode.getMessage());
        body.put("path", path);
        body.put("timestamp", LocalDateTime.now());
        if (fieldErrors != null && !fieldErrors.isEmpty()) {
            body.put("fieldErrors", fieldErrors);
        }


        return new ResponseEntity<>(body, errorCode.getHttpStatus());
    }



}