지난주차까지 데이터에 대해 핸들링했다면,
이번 주차는 본격적으로 프로젝트에 관한 설정을 해봅시다.
우선적으로 API 응답의 통일이 필요하겠죠.
또한 에러 핸들링(Server, Client가 잘못할 경우 모두 포하)과 스웨거 세팅이 필요합니다.
API 응답 통일은 왜 필요할까요?
JSON 형태의 API 응답의 형식은 보통 다음과 같습니다.
{
isSuccess : Boolean
code : String
message : String
result : {응답으로 필요한 또 다른 json}
}
응답의 경우 Code
라는 형태의 enum으로 형태를 관리합니다.
성공 응답과 실패 응답을 하나의 enum으로 만들거나, 분리하여 관리할 수도 있습니다.
예시를 들어 다음과 같은 실습을 진행해봅시다:
Step 1. apiPayload
라는 이름을 지닌 패키지를 생성합니다.
해당 패키지 아래에 ApiResponse
클래스, code
파일을 만들어주세요.
그 다음 통일된 API 응답을 위한 ApiResponse
를 만들어봅시다.
package umc.study.apiPayload;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse<T> {
@JsonProperty("isSuccess")
private final Boolean isSuccess; // 성공인지 아닌지 알려주는 필드
private final String code; // HTTP 상태코드+세부적인 응답 상황
private final String message; // code에 추가적으로 우리에게 익숙한 문자로 상황을 알려줌
@JsonInclude(JsonInclude.Include.NON_NULL)
private T result; // 실제로 클라이언트에게 필요한 데이터
// 성공한 경우 응답 생성(onSuccess)
// 응답 내부에 들어갈 code를 아직 생성하지 않아 주석처리하였습니다
// 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);
// }
// 실패한 경우 응답 생성(onFailure)
public static <T> ApiResponse<T> onFailure(String code, String message, T data){
return new ApiResponse<>(true, code, message, data);
}
}
result는 어떤 형태의 값이 올지 모르기에, Generic 으로 만들어줍시다.
api 응답 형태에 대해 알아봅니다.
{
"isSuccess" : true,
"code" : "2000"
"message" : "OK"
"result" :
{
"testString" : "This is test!"
}
}
API 응답에 HTTP 상태코드로 대표되는 상황뿐만 아니라, 특정한 상황에서의 세부적인 상황설명을 더 해주는 것이 'code'와 'message'라고 보면 됩니다.
이 'code'와 'message'의 형식을 이제 만들어봅시다.
status들은 enum입니다.
ErrorStatus
SuccessStatus
BaseCode
BaseErrorCode
ErrorReasonDTO
ReasonDTO
CommonErrorStatus의 골조 코드(스켈레톤 코드)는 다음과 같습니다:
@Getter
@AllArgsConstructor
public enum CommonErrorStatus implements BaseErrorCode {
// 가장 일반적인 응답
_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;
// interface의 어노테이션을 통하여 DTO를 만드는 것을 확인할 수 있다.
@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()
;
}
}
🎯 GET /temp/test
- query String: X
- request body: X
- request Header: X
- response
{ "isSuccess ": true, "code" : "2000", "message" : "OK", "result" : { "testString" : "This is test!" } }
Step 1. DTO를 우선적으로 만들어줍니다.
request body에 담겨오는 값이 없기 때문에, TempResponse만 작성한 화면입니다.
Step 2. converter을 만들어줍니다.
converter 패키지에 TempConverter 클래스를 만듭니다.
어떤 객체가 사용하는 의존 객체를 직접 만들지 않고, 주입받아 사용하는 것입니다.
예를 들어, 위의 사례에서 배터리 일체형 장난감은 배터리가 떨어지면 장난감을 새로 구입해야겠죠?
이때, 장난감은 배터리에 의존하고 있습니다.
일체형 장난감의 코드는 다음과 같습니다:
public class Toy {
private Battery battery;
public Toy() { // 생성자에서 배터리 객체 생성
battery = new Battery();
}
}
해당 코드를 보면 배터리가 떨어지면, 배터리 교체가 불가능하므로 새로운 장난감을 생성해야합니다.
유연성이 굉장히 떨어진다는 것을 알 수 있겠습니다.
반면, 배터리 분리형의 장난감의 경우에는 어떨까요?
public class Toy {
private Battery battery;
public Toy(Battery battery) {
this.battery = battery;
}
public void setBattery(Battery battery) {
this.battery = battery;
}
}
생성자, 혹은 setter을 통해 배터리를 주입할 수 있습니다.
배터리가 다 떨어지더라도, 장난감을 새로 마련할 필요 없이 배터리만 교체하면 되므로 유연성이 보장됩니다.
즉, 스프링은 DI를 통해 모듈 간의 결합도가 낮아지고 유연성이 높아질 수 있습니다.