Spring

Spring API 공통 response 포맷 개발하기

wangkisa 2022. 7. 10. 18:24

기존 프로젝트 API 에서 response 되는 데이터를 살펴보니,

 

response 가 내려가긴 하는데,

성공인 경우와 실패의 경우가 각각 다른 포맷의 response 가 내려가고 있었다.

 

서버는 데이터를 주긴 하지만 프론트 입장에서는 이렇게 개발하기 난감할 것이라는 생각이 들기도 하고,

 

게다가 사용자 정의 Exception 처리도 없어, 공통 response 포맷을 개발하는 것이 필요하다고 생각하였다.

 

 

다른 블로그 및 글들을 찾아본 뒤 다음 응답 클래스를 만들어보기로 했다.

 

@Getter
public class ApiResponse<T> {

    private final int code;
    private final String message;
    private final List<String> errorDetails;
    private final T data;
    private final String responseTime;

    private ApiResponse(int code, String message, List<String> errorDetails, T data) {
        this.code = code;
        this.message = message;
        this.errorDetails = errorDetails;
        this.data = data;
        this.responseTime = LocalDateTimeUtil.getLocalDateTimeNowString("yyyy-MM-dd hh:mm:ss");
    }

}

 

1. 응답 클래스 설명

 

우선은 다른 기능은 없이 필요한 요소들만 우선 나열해보았다.

 

클래스에 <T> 를 추가해서 Response마다 각기 다른 타입을 리턴해서 사용해야 되므로 해당 조건에 맞는 사용이 가능한 제네릭을 사용할 수 있도록 하였다.

 

여기서 제네릭(Generic) 은 클래스 / 인터페이스 / 메서드 등의 타입을 파라미터로 사용할 수 있게 해주는 역할을 한다고 한다.

 

 

private final int code;

 

응답 코드를 나타내는 code

예전엔 String 으로 성공인 경우 "0000" 이라든지 문자열 값으로 많이 하였는데,

여기선

HttpStatus.OK

처럼 표준 HttpStatus 코드를 사용하기 위해 int 로 설정하였다.

HttpStatus.OK 의 값은 200 이므로 

프론트에서는 200 값을 성공으로 간주하게 된다.

예를 들어) request 파라미터 오류의 경우는 HttpStatus.BAD_REQUEST,

인증 오류의 경우는 HttpStatus.UNAUTHORIZED 등으로 사용이 가능하다.

 

이 부분은 문자열이 좋은지, 해당 HttpStatus 코드값이 좋은지는 알 수 없다.

 

각자의 회사 혹은 개인의 사정에 따라 더 좋은 경우에 맞춰서 하면 좋을것 같다.

 

private final String message;

 

사용자 정의 오류 메시지 혹은 런타임 오류 등에서 전달된 핵심 메시지를 나타낸다.


private final List<String> errorDetails;

 

위 message 에서 표현하기 힘든 자세한 에러 내용표시들을 나타낸다.

예) e.getMessage(), e.printStackTrace() 등등 을 나타내고 싶은 경우


private final T data;

 

실질적인 본문에 해당하는 데이터

각각의 응답 dto에 따라 내용이 달라진다.


private final String responseTime;

 

응답 시간

 

 

2. 응답 클래스 사용

 

우선적으로 많이 쓰는 응답이 데이터가 있는 경우

수정, 삭제 등 데이터가 없는 경우 가 있다고 생각하였다.

그 외 에러 인 경우

 public static <T> ApiResponse<T> success(T data) {
    return new ApiResponse<>(StatusCode.OK_CODE, null, null, data);
}

public static ApiResponse<?> successWithNoData() {
    return new ApiResponse<>(StatusCode.OK_CODE, null, null, null);
}

 

(1) 데이터가 있는 경우

success 함수에서 응답객체와 입력받는 객체를 T로

<T> ApiResponse<T> 이렇게 되도록 하였고, 

T data 로 각각 다른 포맷도 받아서 처리가 되도록 하였다.

 

ApiResponse.success(응답 dto) 이런식으로 사용하도록 하였다.

 

(2) 데이터가 없는 경우

successWithNoData 함수에서 

따로 받는 객체가 없으므로 ApiResponse<?> 로 되도록 하였고,

 

ApiResponse.successWithNoData() 라고만 호출하면 

data는 null 로만 설정하도록 하였다.

 

(3) 에러의 경우

 

public static ApiResponse<?> error(int code, String message, List<String> errorDetails) {

    return new ApiResponse<>(code, message, errorDetails, null);
}

에러 종류에 따른 다른 code 

등 위에서 설명했으므로 생략하고 

 

ApiResponse.error(HttpStatus.BAD_REQUEST.value(), e.getMessage(), Arrays.asList(request.getRequestURI()))

 

처럼 사용이 가능하다.

 

 

3. 커스텀 에러

 

@Getter
public class CustomException extends RuntimeException {

    public final int code;
    public final String message;
    public final List<String> errorDetails;

    public CustomException(int code, String message, List<String> errorDetails) {
        this.code = code;
        this.message = message;
        this.errorDetails = errorDetails;
    }
}

 

커스텀 에러는 RuntimeException 을 상속해서 사용하도록 해서 

throw new 로 사용할 수 있게끔 하였다.

 

해당 클래스를 호출 했을 때 받는 핸들러를  다음과 같이 설정하였다.

 

@Slf4j
@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(CustomException.class)
    @ResponseBody
    public ApiResponse<?> handleCustomException(CustomException ce, WebRequest request) {
		log.error("call handleCustomException()");
        return ApiResponse.error(ce.getCode(), ce.getMessage(), ce.getErrorDetails());
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(RuntimeException.class)
    public ApiResponse<?> handlerRuntimeException(RuntimeException e, HttpServletRequest request) {
		log.error("call handlerRuntimeException()");
        return ApiResponse.error(HttpStatus.BAD_REQUEST.value(), e.getMessage(), Arrays.asList(request.getRequestURI()));
    }

 

이렇게 하면 

throw new CustomException(StatusCode.ERROR_WITHDRAWAL_USER_CODE,
                          StatusCode.ERROR_WITHDRAWAL_USER_MSG, null);

호출 했을 때 'handleCustomException' 를 타서 동작하는 것을 확인 할 수 있었다.

 

 

4. 위에서 설명한 호출 결과들

 

(1) 응답 있는 성공의 경우

 

{
    "code": 200,
    "message": null,
    "errorDetails": null,
    "data": {
        "id": 21,
        "email": "test@test.com5",
        "accessToken": "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NTc0NTA1MjEsImVtYWlsIjoidGVzdEB0ZXN0LmNvbTUifQ.dvOVM5OyhyUj6ZXKjHmv-JAGCKHEI10w0K-OrQ173JA",
        "refreshToken": "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NTc2MTYxMjF9.PZ1pqRCMgDtgC9EwW-e-f_7qt2TYFABffgP8H7PuRjE",
        "isSignUp": false,
        "nickname": "12수업업슨슨",
        "userGenderType": null,
        "birthDate": null,
        "city": "금천구"
    },
    "responseTime": "2022-07-10 08:55:21"
}

 

(2) 응답 없는 성공의 경우

 

{
    "code": 200,
    "message": null,
    "errorDetails": null,
    "data": null,
    "responseTime": "2022-07-10 08:55:21"
}

 

 

(3) 인증 에러의 경우

 

{
    "code": 401,
    "message": "올바르지 못한 인증입니다.",
    "errorDetails": [
        "Full authentication is required to access this resource"
    ],
    "responseTime": "2022-07-10 09:20:15"
}

 

(4) 커스텀 에러의 경우

 

{
    "code": 8100,
    "message": "탈퇴한 유저입니다.",
    "errorDetails": null,
    "data": null,
    "responseTime": "2022-07-10 09:22:33"
}

 

 

참조: https://velog.io/@qotndus43/%EC%8A%A4%ED%94%84%EB%A7%81-API-%EA%B3%B5%ED%86%B5-%EC%9D%91%EB%8B%B5-%ED%8F%AC%EB%A7%B7-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0