-
Spring API 공통 response 포맷 개발하기Spring 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" }
'Spring' 카테고리의 다른 글
@Transactional 에 대한 고찰 (0) 2023.03.26 JPA Hibernate Proxy (1) 2022.09.13