Java 프로젝트를 끝낸지 약 한달, Django 처럼 그동안 작성해 활용했던 것들을 정리해보는 시간을 가져볼려고 한다. 그동안 바빠 블로그를 잊었었다.
이 방법만이 있는 것이 아니겠지만, 내가 생각했던것을 정리해보고자 작성한다.
이유
기본적으로 API가 Return하는 데이터를 최대한 통일하고 싶었다. 오류와 메세지, 그리고 데이터가 (최대한) 같이 오는 방식으로 진행할려고 했다.
JSON 구조는 다음과 같이 설계했다
{ "code": "ok", // or "fail" (4xx), or "error"(5xx) "message": "String", "data": {} // Object }
data
필드에 실제 값이 들어가고, 이외의 값은 클라이언트를 위한 힌트였다.Json Serialize 라이브러리로는
jackson
을 사용했다.구현
Basic Class
해당 내용 구현을 위하여 간단한 Class를 작성했다.
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; @Data public class ResultWrapper<T> { // Status 문자열들 private static final String STATUS_OK = "ok"; private static final String STATUS_FAIL = "fail"; private static final String STATUS_ERROR = "error"; private String status; // 빈 값이면 Json에 포함하지 않음. @JsonInclude(JsonInclude.Include.NON_NULL) public String message; @JsonInclude(JsonInclude.Include.NON_NULL) protected T data; // HTTP Status Code, 추후 기술 @JsonIgnore // Json에 포함하지 않음 public int httpStatusCode; public ResultWrapper(String status, String message, T data) { this.status = status; this.message = message; this.data = data; } public ResultWrapper(int httpStatusCode, String status, String message, T data) { this.httpStatusCode = httpStatusCode; this.status = status; this.message = message; this.data = data; } }
Methods
추가적으로 해당 구현을 쉽게 반환하게, 템플릿 형식의 메소드를 작성했다.
public class ResultWrapper<T> { // ... public static <T> ResultWrapper<T> ok(String message, T data) { return new ResultWrapper<>(STATUS_OK, message, data); } public static <T> ResultWrapper<T> fail(String message, T data) { return new ResultWrapper<>(400, STATUS_FAIL, message, data); } public static <T> ResultWrapper<T> error(String message, T data) { return new ResultWrapper<>(500, STATUS_ERROR, message, data); } public static <T> ResultWrapper<T> fail(int httpStatusCode, String message, T data) { return new ResultWrapper<>(httpStatusCode, STATUS_FAIL, message, data); } public static <T> ResultWrapper<T> error(int httpStatusCode, String message, T data) { return new ResultWrapper<>(httpStatusCode, STATUS_ERROR, message, data); } public static <T> ResultWrapper<T> of(int httpStatusCode, String status, String message, T data) { return new ResultWrapper<>(httpStatusCode, status, message, data); } }
Controller
HTTP Status Code를 반환되는 값 안에 저장하여 전송하기 때문에, 해당 부분을 Spring에서 컨트롤 할 수 있도록 추가적인 부분을 작성하였다.
이 부분이 가장 어려웠는데, 다행히 Spring이 Jackson 데이터를 처리할때 쓸 수 있는 Filter가 있었다.
AbstractMappingJacksonResponseBodyAdvice
였다.import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.AbstractMappingJacksonResponseBodyAdvice; import javax.servlet.http.HttpServletResponse; @RestControllerAdvice // Annotation이 자동으로 등록한다. public class ResultWrapperJsonControllerAdvice extends AbstractMappingJacksonResponseBodyAdvice { @Override protected void beforeBodyWriteInternal( MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { try { // bodyContainer 에서 getValue를 가져온다. (예외 발생시 ResultWrapper가 아님) ResultWrapper<?> rw = (ResultWrapper<?>) bodyContainer.getValue(); // status code를 가져온다. int statusCode = rw.getHttpStatusCode(); // 만약 statusCode가 0(기본값)이 아니면 해당 값으로 Response의 Status를 바꾼다. if (statusCode != 0) { HttpServletResponse resp = ((ServletServerHttpResponse) response).getServletResponse(); resp.setStatus(statusCode); } } catch (Exception e) { // ResultWrapper가 아니거나 기타 오류, 무시한다. } } }
사용
이제 해당 Class를 사용하여 값을 반환하면 된다.
import com.example.json.ResultWrapper import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/") public class Index { @GetMapping({"", "/"}) public ResultWrapper<Void> index() { return ResultWrapper.ok("Hello, World!", null); } // {"status": "ok", "message": "Hello, World!"} @GetMapping("/fail") public ResultWrapper<String> index() { return ResultWrapper.error(404, "Error", "Not Found"); } // {"status": "fail", "message": "Error", "data": "Not Found"} }