Enum 활용 Spring 전역예외처리(Global Exception Handler)

진동선·2023년 10월 15일
2

Spring

목록 보기
1/2

java 의 enum 객체를 활용하여 spring boot의 프로젝트에 global exception handler 적용시키기

1. 예외 발생시에 날려줄 code, message 등을 enum 객체로 생성

public enum ExceptionCode { // 예외 발생시, body에 실어 날려줄 상태, code, message 커스텀


    POST_NOT_FOUND(404, "POST_001", "해당되는 id 의 글을 찾을 수 없습니다."),
    REPLY_NOT_FOUND(404, "REPLY_001", "해당되는 id의 댓글을 찾을 수 없습니다."),
    ALREADY_LIKED(400, "LIKE_001", "이미 '좋아요'를 누른 상태입니다."),



    USER_NOT_FOUND(404, "USER_004", "해당 유저를 찾을 수 없습니다."),
    TOKEN_NOT_VALID(401, "TOKEN_001", "토큰이 만료되었습니다. 다시 로그인 해주세요."),
    USER_CAN_NOT_BE_NULL(400, "USER_005", "사용자는 null이 될 수 없습니다."),
    USER_ID_NOT_FOUND(404, "USER_006", "해당되는 id의 사용자를 찾을 수 없습니다."),


    NULL_POINT_ERROR(404, "G010", "NullPointerException 발생"),

    // @RequestBody 및 @RequestParam, @PathVariable 값이 유효하지 않음
    NOT_VALID_ERROR(404, "G011", "Validation Exception 발생");
    
    
    // 1. status = 날려줄 상태코드
    // 2. code = 해당 오류가 어느부분과 관련있는지 카테고리화 해주는 코드. 예외 원인 식별하기 편하기에 추가
    // 3. message = 발생한 예외에 대한 설명.

    private final int status;
    private final String code;
    private final String message;

    ExceptionCode(int status, String code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }

    public int getStatus() {
        return status;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}
  • 이 enum 클래스는 각각의 예외 상황에 대한 정보를 담고 있다. 이 정보에는 HTTP 상태 코드(status), 오류 코드(code), 그리고 오류 메시지(message)가 포함되어있다. 이렇게 하면 프로그램 전체에서 일관된 방식으로 예외를 처리할 수 있으며, 특정 예외가 발생했을 때 필요한 모든 정보를 한 곳에서 관리할 수 있다.







2. RuntimeException(런타임 익셉션) 커스텀 클래스 생성

package com.spring.exception;


public class BusinessException extends RuntimeException{
    // runtimeException 발생시 날려줄 메시지들을 enum에 작성해놓고,
    // CustomException(현재 클래스) 에서 enum 객체를 생성
    private final ExceptionCode exceptionCode;

    public BusinessException(ExceptionCode exceptionCode){

        this.exceptionCode = exceptionCode;
    }

    public ExceptionCode getExceptionCode() {
        return exceptionCode;
    }



}
  • 이 클래스는 RuntimeException을 상속받아서 만든 사용자 정의 예외 클래스다. 생성자에서 ExceptionCode 인스턴스를 받아서 초기화하며, 이 값을 기반으로 HTTP 상태 코드와 오류 메시지 등 필요한 정보를 제공한다.

이런 식으로 구현하면 다음과 같은 장점이 있다

  • 일관성: 프로그램 전체에서 동일한 형식의 오류 응답을 제공할 수 있으므로 API 사용자가 오류 응답을 쉽게 이해하고 처리할 수 있다.
  • 중앙 집중식 관리: 모든 오류 코드와 메시지가 ExceptionCode enum에 중앙 집중식으로 관리되므로, 나중에 메시지를 변경하거나 새로운 종류의 예외를 추가하는 것이 편리하다.
  • 타입 안전성: 컴파일 타임에 오류 타입을 확인할 수 있으므로 실행 시간 중에 발생하는 문제를 줄일 수 있다.

이런 구조는 RESTful API 서버 개발 시 많이 사용되는 패턴 중 하나다. 서버 애플리케이션은 다양한 종류의 오류 상황을 잘 처리해야 하는데, 위와 같은 방법으로 구현하면 이 작업을 체계적이고 효율적으로 할 수 있다는 점에서 유용하다.







3. ErrorResponse클래스 생성

package com.spring.exception.globalHandler;

import com.spring.exception.ExceptionCode;
import lombok.*;
import org.springframework.validation.BindingResult;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// globalExceptionHandler(전역예외처리)에서 예외 발생 처리시 보내줄 응답 객체(status, code, message)
public class ErrorResponse {

    private int status;
    private String code;
    private String message;
    private List<FieldError> errors;    // 상세 에러 메시지
    private String reason;              // 에러 이유



    @Builder
    public ErrorResponse(final ExceptionCode code) {
        this.status = code.getStatus();
        this.code = code.getCode();
        this.message = code.getMessage();
        this.errors = new ArrayList<>();
    }

    @Builder
    protected ErrorResponse(final ExceptionCode code, final String reason) {
        this.message = code.getMessage();
        this.status = code.getStatus();
        this.code = code.getCode();
        this.reason = reason;
    }

    @Builder
    protected ErrorResponse(final ExceptionCode code, final List<FieldError> errors) {
        this.status = code.getStatus();
        this.code = code.getCode();
        this.message = message;  // 주어진 메시지를 사용합니다.
        this.errors = errors;
    }


    public static ErrorResponse of(final ExceptionCode code, final BindingResult bindingResult) {
        return new ErrorResponse(code, FieldError.of(bindingResult));
    }

    @Builder
    public static ErrorResponse of(final ExceptionCode code) {
        return new ErrorResponse(code);
    }

    @Builder
    public static ErrorResponse of(final ExceptionCode code, final String reason){
        return new ErrorResponse(code, reason);
    }




    public int getStatus(){
        return status;
    }

    public void setStatus(int status){
        this.status = status;
    }


    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }


    /**
     * 에러를 e.getBindingResult() 형태로 전달 받는 경우 해당 내용을 상세 내용으로 변경하는 기능을 수행한다.
     */
    @Getter
    public static class FieldError {
        private final String field;
        private final String value;
        private final String reason;

        public static List<FieldError> of(final String field, final String value, final String reason) {
            List<FieldError> fieldErrors = new ArrayList<>();
            fieldErrors.add(new FieldError(field, value, reason));
            return fieldErrors;
        }

        private static List<FieldError> of(final BindingResult bindingResult) {
            final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();
            return fieldErrors.stream()
                    .map(error -> new FieldError(
                            error.getField(),
                            error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
                            error.getDefaultMessage()))
                    .collect(Collectors.toList());
        }

        @Builder
        FieldError(String field, String value, String reason) {
            this.field = field;
            this.value = value;
            this.reason = reason;
        }
    }
}

이 클래스는 클라이언트에게 반환할 오류 응답의 형식을 정의한다.

  • status : HTTP 상태 코드
  • code : 오류 코드
  • message : 오류 메시지
  • field: 오류가 발생한 필드의 이름
  • reason: 왜 이 값이 유효하지 않은지에 대한 설명







4. 예외처리를 전역으로 처리해줄 클래스 생성

@Slf4j // Lombok라이브러리의 다양한 logging시스템을 쓸 수 있음(예 : log.debug(), log.info(), log.error())
@RestControllerAdvice // 비동기식 controller의 예외처리 관리시 붙이는 어노테이션
public class GlobalExceptionHandler {

    private final HttpStatus HTTP_STATUS_OK = HttpStatus.OK;

    //비즈니스 로직의 예외처리(Unchecked Exception 발생시 처리)
    @ExceptionHandler(BusinessException.class) // 만들어준 커스텀익셉션 발생시 처리해주는 곳
    public ResponseEntity<ErrorResponse> handleCustomException(BusinessException ex) {
        log.error("Business Exception Error", ex);

        final ErrorResponse errorResponse = ErrorResponse.of(ex.getExceptionCode(), ex.getMessage());


        return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(errorResponse.getStatus()));

    }

    //여기부턴 클라이언트 측의 잘못된 요청에 의한 에러를 처리해줌.
    @ExceptionHandler(NullPointerException.class) // nullPointerExceptiono발생시
    protected ResponseEntity<ErrorResponse> handleNullPointerException(NullPointerException e) {
        log.error("handleNullPointerException", e);
        final ErrorResponse response = ErrorResponse.of(ExceptionCode.NULL_POINT_ERROR, e.getMessage());
        return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
    }


    // @valid 유효성 검증에 실패했을 경우 발생하는 예외 처리
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        log.error("handleMethodArgumentNotValidException", ex);
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder stringBuilder = new StringBuilder();
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            stringBuilder.append(fieldError.getField()).append(":");
            stringBuilder.append(fieldError.getDefaultMessage());
            stringBuilder.append(", ");
        }
        final ErrorResponse response = ErrorResponse.of(ExceptionCode.NOT_VALID_ERROR, String.valueOf(stringBuilder));
        return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus()));
    }


}

이 클래스는 애플리케이션 전체에서 발생하는 예외를 처리하는 역할을 한다.

@RestControllerAdvice: 이 어노테이션이 붙은 클래스는 모든 컨트롤러가 실행되는 동안 발생할 수 있는 예외를 잡아서 처리한다.
@ExceptionHandler(CustomException.class): 이 어노테이션은 해당 메소드가 CustomException 타입의 예외를 처리하도록 지정한다.
handleIdNotFoundException(): 이 메소드는 CustomException이 발생했을 때 호출된다. 인자로 받은 예외 객체로부터 상태 코드, 오류 코드, 오류 메시지를 가져와서 ErrorResponse 객체를 생성하고, 이 객체와 함께 HTTP 응답을 생성하여 반환한다.


따라서 위의 구조로 구현하면 애플리케이션 전체에서 일관된 방식으로 에러 응답을 제공할 수 있다. 또한 모든 컨트롤러나 서비스 로직에서 개별적으로 예외 처리 로직을 작성하지 않아도 되므로 유지보수성도 향상된다.







profile
Second brain

0개의 댓글