🤔 개발 초심자의 입장에서 API 응답 통일을 파헤처 봅시다.
(본 내용은 UMC의 백엔드 스터디 워크북과 여러 블로그를 참고하여 만들었습니다.)
프로젝트를 진행하면서 Rest API를 설계하게 되는데요, 이때 프로젝트에 사용할 API 응답을 정해두고 통일하여 사용하게 됩니다.
Controller에서 API를 통해 요청을 하면 HTTP 응답이 날라오게 되는데....
위의 사진에서 파란색 부분 즉, message body에 우리가 프로젝트에서 만든 API의 응답을 JSON 형태로 넣어줄겁니다.
나중에 Swagger를 통해서 API를 테스트 해줄때 위와같이 Response Body에 저희가 만든 API 응답이 JSON 형태로 들어가 있는 것을 확인할 수 있어요!
📌 API 응답 형식은 어떻게 만들까요?
Example 1.
{
"status": "error",
"data": null,
"message": "id: 'student01' 업데이트 실패"
}
Example 2.
{
"code": "S000",
"message": "OK",
"data":{
"id":1,
"name":"test"
}
}
Example 3.
위와 같이 다양한 종류의 형식을 사용할 수 있습니다 ! 정해진 것은 없고 프로젝트마다 다른 형식을 사용할 수 있어요.
저는 동아리 워크북을 참고하여 아래와 같은 형식을 사용해보도록 할게요 😮
{
"isSuccess" : Boolean 타입
"code" : String
"message" : String
"result" : {응답으로 필요한 또 다른 json}
}
📌 코드로 구현해보기.
다음과 같은 의존성을 추가해서 dummy 프로젝트를 만들어주세요.
- Spring Web
- Lombok
연습이니 디렉토리는 자유롭게 설정해주세요!
✅ ApiResponse 클래스 만들기
먼저 ApiResponse라는 클래스를 만들어주고 result 즉, 또다른 결과를 다양한 형태를 받아주기 위해서 제네릭으로 받아줄겁니다.
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse<T> {
@JsonProperty("isSuccess")
private final Boolean isSuccess;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T result;
}
위에서 부터 중요한 부분을 설명드리겠습니다.
- @JsonPropertyOrder : ApiResponse라는 클래스를 결국 JSON으로 변환해서 ResponseBody에 넣어줄건데, 변환할 경우 디폴트는 필드의 순서를 지키려고 하지만, 변환될 수 있는 경우가 있기 때문에 순서를 지정해줍니다.
- @JsonProperty : JPA 의 @Column 과 비슷합니다. isSuccess 라는 필드의 이름이 바뀌지 않도록 고정해줍니다.
- JsonInclude(JsonInclude.Include.NON_NULL) : null 값이 올 경우 Json에서 제외하도록 합니다. ( 응답에 돌려줄 내용이 없을수도 있으니까요 )
이제 필드에 대한 부분을 생각해 봅시다.
isSuccess는 당연히 성공이면 true 실패면 false를 반환할 것입니다.
🤔 그럼 code와 message는 어떤것을 넣어줘야할까요? 이 부분은 프로젝트에 따라서 정해주시면 됩니다.
사실상 내가 원하는 메시지랑 코드를 미리 만들어두고 사용하면 되는거죠 ! (Enum으로 하면 야무질 것 같은 느낌이 팍팍듭니다)
커스텀 상태 코드에 대한 부분은 다른 글에서 정리를 해두었습니다.
참고 : Custom 상태 코드 설명 🔗
✅ Custom 상태 코드 (성공) 만들기
@Getter
@AllArgsConstructor
public enum SuccessStatus {
// 일반적인 응답
_OK(HttpStatus.OK, "COMMON200", "성공입니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
}
입맛대로 상수를 커스텀 상태 코드를 만들어주세요
✅ ApiResponse를 만들어주는 static 생성 매서드를 만들자
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse<T> {
@JsonProperty("isSuccess")
private final Boolean isSuccess;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T result;
public static <T> ApiResponse<T> onSuccess(T result){
return new ApiResponse<>(true, SuccessStatus._OK.getCode() , SuccessStatus._OK.getMessage(), result);
}
}
- 성공한 ApiResponse 를 만들 경우 당연히 isSuccess는 true가 들어갑니다.
- code와 message는 SuccessStatus._OK 에서 미리 만들어준 값들을 그대로 넣어줍니다.
- result에 들어갈 것만 제네릭으로 받아서 그대로 넣어주면 끝 !
✅ Controller에서 사용되는 부분 맛보기
@RestController
@RequiredArgsConstructor
@RequestMapping("/test")
public class TestController {
@GetMapping("/execute")
public ApiResponse<TestResponse.TestDTO> test(){
return ApiResponse.onSuccess(TestConverter.toTestDTO());
}
}
예시로 그냥 쓰던것을 가져와보았는데요. 다른 부분들은 볼 필요 없이 return 부분에서 Result에 들어갈 제네릭 인자만 받아서 ApiResponse를 반환해주는 것을 확인할 수 있습니다.
@RestController의 역할
@RestController는 @Controller와 @ResponseBody를 결합한 역할을 합니다.
이 어노테이션이 사용된 컨트롤러 메서드는 자동으로 JSON 응답을 생성하여 반환합니다.
따라서 저희가 만든 ApiResponse가 Json으로 잘 변환되어 Responsebody에 들어가게 됩니다!
✅ 이해하기 쉽게 정리해보자
(간단히 만들어봤는데 디자인이 구려도 이해해주세요 😢)
- ApiResponse에서 만들어둔 OnSuccess 매서드를 Controller에서 호출합니다.
- 자동으로 isSucess, code, message는 채워지고 result만 담아주면 됩니다.
- 담아서 던져준다
Result에 담아주는 것은 필요한 Converter와 DTO를 만들어야하는데 그 내용은 잠깐 미루겠습니다.
성공 메시지를 예시로 들었고 실패인 경우와 인터페이스의 다형성을 적용해 심화된 내용을 이어서 다루겠습니다.
📌 다형성을 추가해보자
순서대로 설명할태니 잘 따라와 주세요 !
우선 성공에 대한 커스텀 상태 메시지를 만들었으니 실패에 대한 것도 만들어 봅시다.
@Getter
@AllArgsConstructor
public enum ErrorStatus {
// COMMON
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
}
저는 다음과 같은 예시를 사용하겠습니다.
public interface BaseCode {
String getCode();
String getMessage();
}
public interface BaseErrorCode {
String getCode();
String getMessage();
}
그 이후에 Interface 두개를 만들겁니다(DTO 에 대한 부분은 우선 넘겨주세요 ) . 이 두개 인터페이스의 목적은 각각 성공과 실패한 경우의 Enum을 implement 하기 위해서 인데요... 그런 과정이 왜 필요할까요❓ 느슨한 결합을 만들어서 다형성을 높이기 위해서 입니다.
inteface 두개를 만들고 각각의 Enum을 implement 해줍시다.
✅ BaseCode를 Implement한 SuccessCode
@Getter
@AllArgsConstructor
public enum SuccessStatus implements BaseCode {
// 일반적인 응답
_OK(HttpStatus.OK, "COMMON200", "성공입니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public String getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
당연히 BaseErrorCode에도 마저 수정을 해주세요 .
✅ 기존 ApiResponse의 OnSuccess 매서드
public static <T> ApiResponse<T> onSuccess(T result){
return new ApiResponse<>(true, SuccessStatus._OK.getCode() , SuccessStatus._OK.getMessage(), result);
}
기존에 만들어 놓은 성공에 관한 정적 펙토리 메서드 onSuccess인데요, 현재 이 메서드는 SuccessStatus의 _Ok만을 이용하여 작동하게 설계가 되어있습니다. inteface를 이용함으로 이 부분을 느슨하게 결합할 수 있습니다.
✅ ApiResponse의 정적 펙토리 매서드에 느슨한 결합을 추가해보자!
public static <T> ApiResponse<T> of(BaseCode code, T result){
return new ApiResponse<>(true, code.getCode() , code.getMessage(), result);
}
이런식으로 BaseCode라는 인터페이스를 입력 인자로 받고 인터페이스의 매서드를 이용해서 ApiResponse의 필드들을 채우도록 설계할 수 있습니다.
이렇게 설계하는 경우 😧
- SuccessStatus 뿐만 아니라 BaseCode를 Implement하는 다른 Enum도 of 메서드의 입력 인자로 사용될 수 있습니다.
- 입력인자를 SuccessStatus가 아닌 다른 것으로 바꾸는 과정도 ApiResponse 클래스를 수정할 필요가 없습니다.
✅ ApiResponse의 실패에 대한 정적 펙토리 매서드
public static <T> ApiResponse<T> onFailure(BaseErrorCode code, T data){
return new ApiResponse<>(false, code.getCode() , code.getMessage(), data);
}
실패한 경우에는 다음과 같은 매서드를 만들 수 있겠네요.
마지막으로 DTO를 추가해서 설계를 마치겠습니다.
📌 DTO를 추가해보자
@Getter
@Builder
public class ReasonDTO {
private HttpStatus httpStatus;
private final boolean isSuccess;
private final String code;
private final String message;
public boolean getIsSuccess(){
return isSuccess;
}
}
@Getter
@Builder
public class ErrorReasonDTO {
private HttpStatus httpStatus;
private final boolean isSuccess;
private final String code;
private final String message;
public boolean getIsSuccess(){
return isSuccess;
}
}
성공인 경우와 실패인 경우에 대해서 각각 사용할 DTO를 두개 만들어줍시다. (Builder 패턴도 달아줍시다)
DTO를 사용하는 경우 외부에 도메인이 드러나지 않는 효과가 있어요 !
//기존의 BaseCode
public interface BaseCode {
String getCode();
String getMessage();
}
// New BaseCode
public interface BaseCode {
ReasonDTO getReason();
ReasonDTO getReasonHttpStatus();
}
BaseCode도 ReasonDTO를 반환하도록 수정해줍시다 😮
@Getter
@AllArgsConstructor
public enum SuccessStatus implements BaseCode {
// 일반적인 응답
_OK(HttpStatus.OK, "COMMON200", "성공입니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
@Override
public ReasonDTO getReason() {
return ReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(true)
.build();
}
@Override
public ReasonDTO getReasonHttpStatus() {
return ReasonDTO.builder()
.message(message)
.code(code)
.isSuccess(true)
.httpStatus(httpStatus)
.build()
;
}
}
HttpStatus를 담아주는 경우와 그렇지 않은 경우 두 가지 매서드를 만들어 주었는데요,
정확히 말하면 이번 게시글에서 정한 ApiResponse는 HttpStatus가 필드에 없기 때문에 SuccessStatus Enum에도 필요가 없고 그것을 DTO에 넣어서 반환할 필요가 없습니다...
하지만 UMC 워크북에서는 HttpStatus를 커스텀 상태 메시지에도 넣어주고 또 그것에 따라 HttpStatus를 반환하는 매서드와 그렇지 않은 매서드를 분리해 놓았습니다.
추측으로는 ApiResponse에서는 사용하지 않지만 에러 처리나 다른 부분에서는 사용될수도 있다고 생각하기 때문에... 만들지 않았을까 싶어요 이 부분은 조금 더 공부하고 알게 되면 게시물을 수정해보겠습니다. 🙇🏻
✅ 최종 ApiResponse 클래스
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse<T> {
@JsonProperty("isSuccess")
private final Boolean isSuccess;
private final String code;
private final String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
private 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);
}
}
📌 잘 만들었는지 Test 해보자 !
✅ Test Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/test")
public class TestController {
@GetMapping("/execute")
public ApiResponse<TestResponse.TestDTO> test(){
return ApiResponse.onSuccess(TestConverter.toTestDTO());
}
}
✅ Test용 DTO
public class TestResponse {
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class TestDTO{
String content;
}
}
✅ Test용 Converter
public class TestConverter {
public static TestResponse.TestDTO toTestDTO() {
return TestResponse.TestDTO.builder()
.content("Test용 Content")
.build();
}
}
이제 어플리케이션을 돌리고 RestController의 url로 들어가 잘 돌아가는지 확인해봅시다.
성공 성공... 😂
reference:
강의 - 모든 개발자를 위한 Http 기본 지식 - 김영한 🔗
UMC 워크북
티스토리 - [Packy] API 공통 응답 포맷 만들기 - 정상 응답인 경우& 예외 처리 응답인 경우🔗
티스토리 - Server ) Api Http status code에 대하여, header와 통일시켜야할까 body에만 담아야 할까? 🔗
벨로그 -스프링 API 공통 응답 처리하기 🔗
'프로젝트' 카테고리의 다른 글
배포의 모든 것 - 2. RDS와 Session Manager (0) | 2025.02.25 |
---|---|
배포의 모든 것 - 1. AWS 시작하기 및 EC2 띄우기 (5) | 2025.01.25 |
Restful API Endpoint를 어떻게 설계해야 할까? (1) | 2025.01.14 |
DDD(Domain Driven Design) 구조 알아보기 (5) | 2025.01.14 |
커스텀 상태 코드(Custom Error Code) (0) | 2025.01.01 |