(UMC) 8. API 응답 통일 & 에러 핸들러 (1)

jimmy·2024년 8월 27일

UMC 6기

목록 보기
8/11

API 응답 통일이 필요한 이유

API 응답의 양식이 항상 일정해야 프론트 입장에서 개발이 편리해진다.
따라서 매우 중요한 작업이라고 할 수 있다!

{
	isSuccess : Boolean
	code : String
	message : String
	result : {응답으로 필요한 또 다른 json}
}

보통 위와 같은 양식을 따른다.
실패한 경우에는 result에 null을 주고 null일때는 표시를 하지 않는 설정 추가

API 응답 통일

ApiResponse 클래스 생성

API 응답 통일을 위한 ApiResponse 클래스 생성하겠다.

@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse<T> {

    private final Boolean isSuccess;
    private final String code;
    private final String message;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private final T result;

    //성공한 경우 응답 생성

    public static <T> ApiResponse<T> onSuccess(T result){
        return new ApiResponse<>(true, SuccessStatus._OK.getCode() , SuccessStatus._OK.getMessage(), result);
    }

    public static <T> ApiResponse<T> of(BaseCode code, T result){
	        return new ApiResponse<>(true, code.getReasonHttpStatus().getCode() , code.getReasonHttpStatus().getMessage(), result);
    }
    //실패
    public static <T> ApiResponse<T> onFailure(String code, String message, T data) {
        return new ApiResponse<>(false, code, message, data);
    }
}

HTTP 상태 코드

필수적으로 알고 있어야할 상태 코드 몇가지만 알고 넘어가겠다.

  1. 200번 대: 문제 없음
    a. 200: OK
    b. 201: Created: 너가 준 데이터로 새로운 리소스를 만들었다는 뜻
  2. 400번 대: 클라이언트 측 잘못으로 인한 에러
    a. 400: Bad Request: 필수한 정보 누락 등 요청이 이상할 때
    b. 401: Unauthorized : 인증이 안됨(로그인이 안되는 상황)
    c. 403: Forbidden : 권한 x(로그인은 o 접근만 x)
    d. 404: NotFound : 요청한 정보가 그냥 없음
  3. 500번대: 서버 측 잘못으로 인한 에러
    a. 500: Internal Server Error : 서버 터졌을 때
    b. 504: Gateway Timeout : 서버가 응답을 안 줌(터진 것과 마찬가지..)

상태코드는 이렇게 몇가지 정해진 상황에 대한 정보들만 알려줄 수 있기 때문에 더 세부적인 커스텀 code를 만들어서 넘겨주는 것이다.

BaseCode와 BaseErrorCode

먼저 BaseCode와 BaseErrorCode 두 인터페이스의 역할은 이를 구체화 하는 Status에서 두 개의 메소드를 반드시 Override할 것을 강제한다.
BaseCode

public interface BaseCode {

    public Reason getReason();

    public Reason getReasonHttpStatus();
}

BaseErrorCode

public interface BaseCode {

    public ErrorReason getReason();

    public ErrorReason getReasonHttpStatus();
}

ErrorStatus와 SuccessStatus

ErrorStatus

@Getter
@AllArgsConstructor
public enum CommonErrorStatus implements BaseErrorCode {

    // 가장 일반적인 응답
    _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
    _BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
    ;

    // ~~~ 무엇에 관한 응답


    private final HttpStatus httpStatus;
    private final String code;
    private final String message;

    @Override
    public ErrorReasonDTO getReason() {
        return ErrorReasonDTO.builder()
                .message(message)
                .code(code)
                .isSuccess(false)
                .build();
    }

    @Override
    public ErrorReasonDTO getReasonHttpStatus() {
        return ErrorReasonDTO.builder()
                .message(message)
                .code(code)
                .isSuccess(false)
                .httpStatus(httpStatus)
                .build()
                ;
    }
}

DTO만들 때 주의사항

큰 묶음으로 클래스를 만들고, 내부적으로 static 클래스를 만드는 것이 좋다.
DTO는 재사용이 많이 될 수 있기에 static class로 만들게 되면, 매번 class 파일을 만들지 않고, 범용적으로 사용할 수 있다.

DTO에도 빌더 패턴을 쓰자
우리가 만드는 인스턴스들은 모두 빌더 패턴을 사용한다고 생각하면 된다.
참고로, RequestDTO는 값을 받아오는 역할이므로 ResponseDTO에만 Builder패턴을 적용하면 된다.

에러 핸들러

General Exception

@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
    private BaseErrorCode code;

    public ErrorReasonDTO getErrorReason() {
        return this.code.getReason();
    }

    public ErrorReasonDTO getErrorReasonHttpStatus() {
        return this.code.getReasonHttpStatus();
    }
}

ExceptionAdvice

@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {


    @org.springframework.web.bind.annotation.ExceptionHandler
    public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
        String errorMessage = e.getConstraintViolations().stream()
                .map(constraintViolation -> constraintViolation.getMessage())
                .findFirst()
                .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));

        return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request);
    }

    @Override
    public ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {

        Map<String, String> errors = new LinkedHashMap<>();

        e.getBindingResult().getFieldErrors().stream()
                .forEach(fieldError -> {
                    String fieldName = fieldError.getField();
                    String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse("");
                    errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage);
                });

        return handleExceptionInternalArgs(e,HttpHeaders.EMPTY,ErrorStatus.valueOf("_BAD_REQUEST"),request,errors);
    }

    @org.springframework.web.bind.annotation.ExceptionHandler
    public ResponseEntity<Object> exception(Exception e, WebRequest request) {
        e.printStackTrace();

        return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage());
    }

    @ExceptionHandler(value = GeneralException.class)
    public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
        ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
        return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request);
    }

    private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason,
                                                           HttpHeaders headers, HttpServletRequest request) {

        ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null);
//        e.printStackTrace();

        WebRequest webRequest = new ServletWebRequest(request);
        return super.handleExceptionInternal(
                e,
                body,
                headers,
                reason.getHttpStatus(),
                webRequest
        );
    }

    private ResponseEntity<Object> handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus,
                                                                HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) {
        ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint);
        return super.handleExceptionInternal(
                e,
                body,
                headers,
                status,
                request
        );
    }

    private ResponseEntity<Object> handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus,
                                                               WebRequest request, Map<String, String> errorArgs) {
        ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs);
        return super.handleExceptionInternal(
                e,
                body,
                headers,
                errorCommonStatus.getHttpStatus(),
                request
        );
    }

    private ResponseEntity<Object> handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus,
                                                                     HttpHeaders headers, WebRequest request) {
        ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null);
        return super.handleExceptionInternal(
                e,
                body,
                headers,
                errorCommonStatus.getHttpStatus(),
                request
        );
    }
}

RestControllerAdvice의 개념

전역 예외 처리: RestControllerAdvice는 모든 REST 컨트롤러에서 발생하는 예외를 한 곳에서 처리할 수 있게 해준다. 이를 통해 중복 코드를 줄이고, 예외 처리 로직을 중앙 집중화할 수 있다.
일관된 에러 응답: 클라이언트에게 반환되는 에러 메시지와 코드의 형식을 일관되게 유지할 수 있다. 이는 API의 사용성을 높이고, 클라이언트 측에서 에러를 처리하는 데 도움을 준다.

예외 처리의 편리함

코드 간결성: RestControllerAdvice를 사용하지 않으면 각 컨트롤러에서 예외를 개별적으로 처리해야 하므로 코드가 복잡해지고 지저분해질 수 있다. 반면, RestControllerAdvice를 사용하면 모든 예외 처리를 하나의 클래스에서 관리할 수 있어 코드가 훨씬 깔끔해진다.
유효성 검사와 통합: @Valid 어노테이션과 함께 사용하면 유효성 검사에서 발생하는 예외도 쉽게 처리할 수 있다. 이를 통해 클라이언트에게 적절한 에러 메시지를 제공할 수 있다.

핸들러 추가

public class TempHandler extends GeneralException {

    public TempHandler(BaseErrorCode errorCode) {
        super(errorCode);
    }
}

에러 처리 로직

  1. 예외 발생: 서비스 계층에서 예외가 발생하면, 이 예외는 Controller로 전파되지 않고MasterExceptionHandler에 의해 감지된다.
  2. ExceptionAdvice 호출: MasterExceptionHandler는 ExceptionAdvice의 메서드를 호출하여 예외를 처리합니다. 이 과정에서 GeneralException이 발생하면, 해당 예외에 대한 처리 로직이 실행된다.
  3. 응답 생성: ExceptionAdvice 내의 메서드에서 응답을 생성하고, 클라이언트에게 반환한다. 이때, 서비스 계층에서 발생한 예외가 Controller를 거치지 않고 바로 ExceptionAdvice로 전달되어 응답이 생성된다.
profile
백문이 불여일기

0개의 댓글