[SWYP] Spring Boot API 공통 응답 포맷 개발하기
[Version]
⦁ 2024.05.10 / [SWYP] Spring Boot API 공통 응답 포맷 개발하기
클라이언트와 서버가 통신하는 구조에서, 클라이언트는 서버에 요청을 보내고 서버는 요청에 대한 결과를 응답한다.
@ResponseBody
@GetMapping("/member")
public ResponseEntity<CommonApiResponse<MemberDTO.Response>> getMemberProfileByLoginId(Authentication authentication) {
String loginId = authentication.getName();
Member member = memberService.findMemberAndSurveyByLoginId(loginId);
return member;
}
예시와 같이 클라이언트에서 회원의 정보를 요청하는 경우 서버는 현재 로그인한 회원의 정보를 응답하게 된다. API로 통신하는 경우 보통 Json으로 응답하는데, `@ResponseBody` 을 사용하고 객체를 반환하면 `HttpMessageConverter`에 의해 객체가 Json으로 변환된다.
상태 라인(HTTP Version / Status Code) |
헤더(Header) |
공백 라인 |
응답 메세지(Body) |
하지만 HTTP 응답 메시지에는 실제 전송을 할 데이터뿐만 아니라 HTTP Version, Status Code, Header, Body 등으로 이루어져 있다. 서버에서는 어떻게 데이터 외 HTTP Version, Status Code, Header 등의 정보를 포함하여 클라이언트로 응답할 수 있을까?
ResponseEntity 활용
위 질문에 대한 답으로, `ResponseEntity` 클래스를 활용하여 데이터 외 추가적인 정보를 함께 응답할 수 있다. 먼저 `ResponseEntity` 클래스의 구조부터 확인해 보자!
public class ResponseEntity<T> extends HttpEntity<T> {
public ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, HttpStatus status) {
super(body, headers);
this.status = status;
}
public ResponseEntity(HttpStatus status) {
this(null, null, status);
}
public ResponseEntity(@Nullable T body, HttpStatus status) {
this(body, null, status);
}
}
위 코드의 생성자를 보면 body, headers, status 세 가지의 파라미터를 가지는 것을 확인할 수 있다. 생성자를 활용하여 인스턴스를 반환하면 객체의 데이터뿐만 아니라 Header, Status Code도 함께 지정하여 응답할 수 있다.
@GetMapping("/member")
public ResponseEntity<Member> getMemberProfileByLoginId(Authentication authentication) {
String loginId = authentication.getName();
Member member = memberService.findMemberAndSurveyByLoginId(loginId);
return ResponseEntity.status(HttpStatus.OK).body(member);
}
@ResponseBody을 사용한 응답, 항상 같을까?
@ExceptionHandler(StorageException.class)
public ResponseEntity<String> handleStorageException(StorageException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
예를 들어 위와 같은 예외가 발생했을 때 응답은 어떻게 전달될까? `e.getMessage()`의 경우 String 형태이기 때문에 `plain/text`로 응답을 전달한다. 이 말은 즉, 동일한 API를 호출해도 예외가 발생했을 때와 그렇지 않을 때의 응답 데이터 모양이 달라지는 것을 의미한다.
경우에 따라 응답 데이터의 형식이 달라진다면 클라이언트에서 응답 포맷에 따라 로직을 달리 구성해야 할 것이다. 따라서 모든 경우에 대해 공통된 응답 포맷을 제공해야 할 필요가 있다.
공통 API 응답 클래스 생성
모든 경우에 대한 공통 포맷을 생성하기 위해 `CommonApiResponse` 클래스를 생성했다. 내가 응답으로 보낼 Json 구조는 다음과 같다.
/* 성공 응답 포맷 */
{
"status": "success",
"data": {
"email": "newtownboy@naver.com",
"name": "newtownboy",
"phone": "010-1234-5678",
"role": "ADMIN"
},
"message": null
}
/* 예외 응답 포맷 */
{
"status": "error",
"data": null
"message": "예외 메세지가 리턴됩니다"
}
/* 데이터 유효성 오류 응답 포맷 */
{
"status": "fail",
"data": {
"email": "이메일 형식이 일치하지 않습니다",
"name": "이름은 빈 값일 수 없습니다"
},
"message": null
}
위 포맷을 응답으로 리턴하기 위한 클래스는 아래와 같이 구성할 수 있다.
package com.swyg.oneului.common;
import lombok.Getter;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Getter
public class CommonApiResponse<T> {
private static final String SUCCESS_STATUS = "success";
private static final String FAIL_STATUS = "fail";
private static final String ERROR_STATUS = "error";
private static final String SUCCESS_MESSAGE = "정상적으로 처리되었습니다.";
private static final String FAIL_MESSAGE = "유효하지 않은 값을 입력하셨습니다.";
private static final String ERROR_MESSAGE = "에러가 발생했습니다.";
private String status;
private T data;
private String message;
public CommonApiResponse() {
}
private CommonApiResponse(String status, T data, String message) {
this.status = status;
this.data = data;
this.message = message;
}
public static <T> CommonApiResponse<T> createSuccess(T data) {
return new CommonApiResponse<>(SUCCESS_STATUS, data, SUCCESS_MESSAGE);
}
public static CommonApiResponse<?> createSuccessWithNoContent() {
return new CommonApiResponse<>(SUCCESS_STATUS, null, SUCCESS_MESSAGE);
}
public static CommonApiResponse<?> createFail(BindingResult bindingResult) {
Map<String, String> errors = new HashMap<>();
List<ObjectError> allErrors = bindingResult.getAllErrors();
for (ObjectError error : allErrors) {
if (error instanceof FieldError) {
errors.put(((FieldError) error).getField(), error.getDefaultMessage());
} else {
errors.put( error.getObjectName(), error.getDefaultMessage());
}
}
return new CommonApiResponse<>(FAIL_STATUS, errors, FAIL_MESSAGE);
}
public static CommonApiResponse<?> createError(String message) {
return new CommonApiResponse<>(ERROR_STATUS, null, message);
}
}
`CommonApiResponse` 클래스는 status, data, message 세 가지 필드를 갖는다. 각 필드에 대해 정의하자면 다음과 같다.
- status: 정상(success), 실패(fail), 예외(error) 중 하나의 값을 갖는다.
- data: 정상 처리되었을 경우 전송될 데이터를 반환하고, 실패 처리되었을 경우 유효성 검증에 실패한 데이터의 목록을 응답한다.
- message: 에러가 발생했을 경우 예외 메시지를 응답한다.
생성한 `CommonApiResponse` 클래스를 실제 코드에 적용하면 다음과 같이 변경할 수 있다.
@GetMapping("/member")
public ResponseEntity<CommonApiResponse<MemberDTO.Response>> getMemberProfileByLoginId(Authentication authentication) {
String loginId = authentication.getName();
Member member = memberService.findMemberAndSurveyByLoginId(loginId);
return ResponseEntity.status(HttpStatus.OK).body(CommonApiResponse.createSuccess(MemberDTO.Response.of(member)));
}
정상, 실패, 예외 상황에서 ResponseEntity 클래스를 통해 각 상황에 적절한 Status Code를 설정하고 Body로 공통 응답 클래스인 `CommonApiResponse`의 인스턴스를 세팅하여 반환하도록 했다. 이로써 각 상황에 따라 일관된 응답 형식을 반환할 수 있게 되었다.