미소를뿌리는감자의 코딩

[Fight Club] Global Handler 설정 - ExceptionHandler 본문

프로젝트

[Fight Club] Global Handler 설정 - ExceptionHandler

미뿌감 2024. 12. 21. 19:56
728x90

0. 개요

이번에 새로운 프로젝트를 시작하면서, global handler를 작성하게 되었다.

이를 잘 남겨두어 추후의 프로젝트에도 요긴하게 사용되길 바라면서 자세히 작성해 볼 것이다.

  1. Exception Handler
  2. Swagger
  3. dto -> record 사용

이렇게 3가지 방법으로 나누어서 사용해볼 것이다.

 

1. Exception Handler

이 부분에서는 ServiceException과 TokenException에 대해서 다루어 볼 것이다.

 

서비스에서 에러를 던질 때, 아래와 같은 방식으로 에러를 던지도록 할 것이다.

throw new ServiceException.of(ErrorCode.INVALID_FORMAT_REQUST);

throw new ServiceExcpetion.of(ErrorCode.INVALID_FORMAT_REQUEST, "New error state");

 

ErrorCode Enum에 이미 에러로 사용할 message가 정의되어 있지만, 이와 다르게 사용하고 싶다면, 2 번째 인자로 작성해서도 사용할 수 있도록 할 것이다.

 

그렇다면 파일 구조가 어떻게 되는지 먼저, 정리한 후 본격적인 내용 설명을 시작하도록 해 볼 것이다.

global/
├── exception/
│   ├── handler/
│   │   └── CustomExceptionHandler.java
│   ├── custom/
│   │   ├── ServiceException.java
│   │   ├── TokenException.java
│   ├── util/
│   │   └── CustomResponseUtil.java
│   ├── dto/
│   │   ├── BaseResponse.java
│   │   ├── ErrorResponseDto.java
│   │   └── ResponseDto.java
│   ├── SuccessCode.java
│   └── ErrorCode.java

 

이런식으로 CustomExceptionHandler, CustomResponseUtil, ServiceException, TokenException, ErrorCode, SuccessCode, ApiCode 총 9개의 파일을 정의할 것이다.

 

우선적으로 ErrorCode, SuccessCode, ApiCode에 대해서 알아보자.

package com.sparta.fritown.global.exception;

import org.springframework.http.HttpStatus;

public enum SuccessCode implements ApiCode {
    OK(HttpStatus.OK, "C001", "Well done"),
    CREATED(HttpStatus.CREATED, "C002", "Created successfully");

    private final HttpStatus status;
    private final String code;
    private final String message;

    SuccessCode(HttpStatus status, String code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }

    public HttpStatus getStatus() { return status; }
    public String getCode() { return code; }
    public String getMessage() { return message; }
}

 

successCode는 enum으로 HttpStatus, code, message를 정의하고 사용된다. 이를 통해서, service에서 더 정돈되게 error를 내뱉을 수 있다.

 

errorCode의 역할도 successCode와 유사하다.

package com.sparta.fritown.global.exception;


import org.springframework.http.HttpStatus;

public enum ErrorCode implements ApiCode {
    IO_EXCEPTION(HttpStatus.BAD_REQUEST, "E001", "IO error");

    private final HttpStatus status;
    private final String code;
    private final String message;

    ErrorCode(HttpStatus status, String code, String message) {
        this.status = status;
        this.code = code;
        this.message = message;
    }

    public HttpStatus getStatus() { return status; }
    public String getCode() { return code; }
    public String getMessage() { return message; }
}

 

이 두 enum은 동일한 구조를 가지고 있다. 따라서, 이를 ApiCode라는 인터페이스로 묶어주어 추후에 instanceOf 와 같이 사용될 수 있도록 하였다.

package com.sparta.fritown.global.exception;

import org.springframework.http.HttpStatus;

public interface ApiCode {
    HttpStatus getStatus();
    String getCode();
    String getMessage();
}

 

다음으로 넘어가서 CustomExceptionHandler에 대해서 알아볼 것이다.

CustomExceptionHandler는 발생한 error들을 잡아내서 global하게 처리하는 것이라고 생각하면 된다.

@Slf4j
@RestControllerAdvice // global exception handling annotation
@RequiredArgsConstructor
public class CustomExceptionHandler {

    @ExceptionHandler(ServiceException.class)
    public ResponseEntity<ErrorResponseDto> handleServiceException(ServiceException e) {
        return CustomResponseUtil.fail(e.getCode(), e.getMessage(), e.getStatus());
    }

    @ExceptionHandler(TokenException.class)
    public ResponseEntity<ErrorResponseDto> handleTokenException(TokenException e) {
        return CustomResponseUtil.fail(e.getCode(), e.getMessage(), e.getStatus());
    }

}

 

우선은 이렇게 ServiceException과 TokenException을 잡아내는 코드를 작성하였다.

 

그렇다면, 인자로 받는 ServiceExceptionTokenException은 어떻게 구성이 되어 있을까? 각각 알아보도록 하자.

  • ServiceException
@Getter
public class ServiceException extends RuntimeException {

    private final HttpStatus status;
    private final String code;

    private ServiceException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.status = errorCode.getStatus();
        this.code = errorCode.getCode();
    }

    private ServiceException(ErrorCode errorCode, String customMessage) {
        super(customMessage);
        this.status = errorCode.getStatus();
        this.code = errorCode.getCode();
    }

    public static ServiceException of(ErrorCode errorCode) {
        return new ServiceException(errorCode);
    }

    public static ServiceException of(ErrorCode errorCode, String customMessage) {
        return new ServiceException(errorCode, customMessage);

ServiceException은 message, status, code의 값을 받아오는 역할을 하는 class라고 보면 될 것 같다.

encapsulation을 지켜주기 위해서 정적 팩토리를 이용해 주었다.

 

@Getter
public class TokenException extends AuthenticationException {
    private final HttpStatus status;
    private final String code;

    private TokenException(ErrorCode errorCode) { // capsulation
        super(errorCode.getMessage());
        this.status = errorCode.getStatus();
        this.code = errorCode.getCode();
    }

    private TokenException(HttpStatus status, String code, String customMessage) {
        super(customMessage);
        this.status = status;
        this.code = code;
    }

    public static TokenException of(ErrorCode errorCode) {
        return new TokenException(errorCode);
    }

    public static TokenException of(ErrorCode errorCode, String customMessage) {
        return new TokenException(errorCode.getStatus(), errorCode.getCode(), customMessage);
    }

}

TokenException도  ServiceException과 유사하게 구성해 주었다.

 

 

이제 CustomExceptionHandler에서 호출하는 CustomResponseUtil에 대해서 이야기 해보자.

CustomResponseUtil은, log를 찍고, ServiceException에서 받아온 값들을 errorResponseDto에 매핑해주고 이를 ResponseEntity를 이용해 에러를 client에게 보내주는 클래스이다.

 

ResponseEntity란 Spring Framework에서 제공하는 클래스로, 상태 코드, 헤더, 본문을 설정할 수 있다.

이걸 사용하면 HttpServletResponse를 사용하지 않고 보다 쉽게 반환 json을 작성할 수 있다.

@Slf4j
public class CustomResponseUtil {
    private static final ObjectMapper objectMapper = new ObjectMapper();

    public static ResponseEntity<ResponseDto<Void>> success(SuccessCode successCode) {
        logging("Success", successCode.getCode(), successCode.getMessage());
        ResponseDto<Void> successResponse = ResponseDto.success(successCode); // successResponse 생성을 통해 json에 넣어줄 값 정리
        return ResponseEntity.status(successCode.getStatus()).body(successResponse);
    }

    public static ResponseEntity<ErrorResponseDto> fail(String code, String message, HttpStatus status) {
        logging("Failure", code, message);
        ErrorResponseDto errorResponseDto = ErrorResponseDto.failure(status, code, message);
        return ResponseEntity.status(status).body(errorResponseDto);
    }


    private static void logging(String type, String code, String message) {
        log.info("{} CustomResponse : [{}] {}", type, code, message);
    }

}

이렇게 구성하였다.

 

이제 ErrorResponseDto와 ResponseDto를 확인해 볼 차례이다.

package com.sparta.fritown.global.exception.dto;

import com.sparta.fritown.global.exception.SuccessCode;
import org.springframework.http.HttpStatus;

public class ResponseDto<T> extends BaseResponse {
    private final T data;

    private ResponseDto(int status, String message, T data) {
        // private로 constructor 관리
        super(status, message);
        this.data = data;
    }

    public static <T> ResponseDto<T> success(String message, T data) {
        // 정적 팩토리 메서드
        // type 매개변수 사용을 통해, 여러 형태의 data type이 이용 가능하도록 함.
        // static 메서드 사용을 통해 특정 인스턴스에 의존하지 않도록 함. 이를 통해 객체를 생성하지 않고도 클래스 이름으로 직접 호출이 가능하도록 하였다.
        return new ResponseDto<>(HttpStatus.OK.value(), message, data);
    }

    public static ResponseDto<Void> success(SuccessCode successCode) {
        return success(successCode.getMessage(), null);
    }
}

 

 

ResponseDto도 encapsulation을 지키고자 노력하였으며, 추후에 나올 ErrorResponseDto와 공통된 부분을 정의하는 건 BaseResponse에 정의해 두었다.

 

BaseResposne에 없는 T data;만 추가로 정의해 주었다.

public class ResponseDto<T> extends BaseResponse {
    private final T data;

    private ResponseDto(int status, String message, T data) {
        // private로 constructor 관리
        super(status, message);
        this.data = data;
    }

    public static <T> ResponseDto<T> success(String message, T data) {
        // 정적 팩토리 메서드
        // type 매개변수 사용을 통해, 여러 형태의 data type이 이용 가능하도록 함.
        // static 메서드 사용을 통해 특정 인스턴스에 의존하지 않도록 함. 이를 통해 객체를 생성하지 않고도 클래스 이름으로 직접 호출이 가능하도록 하였다.
        return new ResponseDto<>(HttpStatus.OK.value(), message, data);
    }

    public static ResponseDto<Void> success(SuccessCode successCode) {
        return success(successCode.getMessage(), null);
    }
}

 

 

ErrorResponseDto도 이와 유사하게 작성하였다.

@Getter
public class ErrorResponseDto extends BaseResponse {
    private final String code;

    private ErrorResponseDto(int status, String message, String code) {
        super(status, message);
        this.code = code;
    }

    public static ErrorResponseDto failure(HttpStatus status, String code, String message) {
        return new ErrorResponseDto(status.value(), message, code);
    }
}

 

다음으로는 SecurityConfig에서 이용할 requestMatchers에 대해서 작성해 줄 것이다.

아무래도, 로그인 요청이나, test api의 경우엔 security에 들어가면 잘 작동하지 않기 때문에 이에 대한 예외처리를 해줄 것이다.

이를 securityconfig에서 보통 정의해주고 있지만, 해당되는 permitAll()에 해당되는 api들이 많아질수록, 복잡해지기 때문에, 따로 분리해 주었다.

@NoArgsConstructor
public class AuthenticatedMatchers {
    public static final String[] loginArray = {
            "/aa",
            "/bb/**",
            "/cc"
    };

    public static final String[] testArray = {
            "/aa/**"
    };
}

 

이런식으로 작성해 준 후, securityConfig파일에서 아래와 같이 정의해 주었다.

.requestMatchers(AuthenticatedMatchers.loginArray).permitAll()

 

728x90