RESTful API를 기반으로 설계된 서버는 요청을 받으면 이 요청에 대한 적절한 응답값을 내려야 한다. 요청 - 응답의 원리는 클라이언트와 서버가 상호작용하고 소통을 하는 중요한 역할을 담당한다. 그러기에 API Result는 프로젝트를 초기에 설계 할때 반드시 짚고 넘어가야 될 중요한 지점이다.
질문 🧑🏻💻) 공통으로 Api Result를 설계 하는것은 타당한가 MSA 환경은 쉽게 말해 서로다른 여러 서버가 존재 할 수 있다는 말이다. 즉 클라이언트 입장에서 서로 다른 여러 서버와 소통해야 한다. 소통을 해야 하는데 응답 값이 각 서버마다 다르다고 한다면 Client는 서로 다른 응답 값에 대해 관리하고 대응해야 한다. 이 말은 관리 포인트가 많아진다는 얘기다. 필자는 이러한 상황을 적합하지 않다고 판단했다. 통합된 응답 값이 관리 포인트를 낮추고 일관되게 만들어 준다. 그럼으로 공통으로 API Result를 설계하는 것은 적합한 행위다.
질문 🧑🏻💻) 더 나아가 MSA 환경에서 Common을 구현하는 것은 적합한가 독립적으로 개발하고 배포하는 것을 목표로 하는 전통 MSA를 지향하는 관점에서 사실 Common은 적합하지 않을 수 있다. 결합도를 높이기 때문이다. 그럼에도 불구하고 필자는 MSA 환경에서 Common을 만들고 이것을 MSA 심장부라 부른다. 비지니스 로직을 구현하기 위한 보조기능들에 대해 첨예하게 관리가 가능하고, 팀 규칙으로서 일관된 프로그래밍을 가능하게 하기 때문이다. 즉, 최종적으로 Common은 규율과 원칙의 기준이 되고 더 철저한 유지보수를 가능하게 한다. 결합도를 높이는 단점이 있지만 Common을 잘 이해하고 관리한다면 장점이 더 탁월하기에 Common을 구현하는 것은 적합하다 생각한다. MSA에도 정답은 없다. 각 팀의 상황, 각 비지니스 상황을 이해하고 그에 맞는 전략과 기준을 세워 프로젝트를 구축하는게 훌륭한 개발자의 덕목이지 않을까?
API Success
- httpStatusCode: 2xx
- httpBody:
{ result: { "code": String, "reason": String, "Message": String }, body: { <T> } }
API Fail
- httpStatusCod : 4xx, 5xx
- httpBody:
{ result: { "code": String, "reason": String, "frontMessage": String }, body: { } }
필자의 MSA 프로젝트에서 내리고 싶은 API Result는 결론적으로 위와 같다.
구현해보자.
- Result
Api Result의 meta-data의 정보를 담는 class이다. 추가로 필자는 clientUseMessage라는 field를 추가했다. 서버에서 각 상황에 필요한 client use message를 내려주기 위함이다.- ErrorCode
서버 개발을 하다보면 각 도메인, 각 상황에 따른 예외 상황이 존재한다. 이 예외 상황에 대한 정보를 추상화한 interface이다. ErrorCode interface의 추상화 함수는 getter에 대한 함수이고, 실질적으로 이를 구현하는 enum의 field 역할을 한다. ErrorCode는 Result에 예외 상황에 대한 정보를 전달하는 역할을 담당한다. Result.java 파일의 public static ERROR() 함수를 참고하면 된다.- Api
Result의 정보와 Body 정보를 담는 역할을 한다. public static OK(), public static ERROR() 함수를 통해 성공에 대한 Api Result, 예외 상황에 대한 Api Result를 전달 할 수 있도록 설계했다.- ApiException
Exception을 발생할때 ExceptionHandler에게 전달할 정보를 가지는 interface이다. 이 interface의 추상화 함수도 실질적으로 구현체의 getter에 대한 함수이며, 구현체의 field 역할을 한다.
아래의 코드를 천천히 살펴보길 바란다.
@Getter
@NoArgsConstructor
public class Result {
private static final String SUCCESS_CODE = "lucycato";
private static final String SUCCESS_REASON = "success";
private String code;
private String reason;
private String clientUseMessage;
public static Result OK() {
Result result = new Result();
result.code = Result.SUCCESS_CODE;
result.reason = Result.SUCCESS_REASON;
result.clientUseMessage = "";
return result;
}
public static Result ERROR(ErrorCode errorCode) {
Result result = new Result();
result.code = errorCode.getCode();
result.reason = errorCode.getReason();
result.clientUseMessage = errorCode.getClientUseMessage();
return result;
}
public static Result ERROR(ErrorCode errorCode, String reason) {
Result result = new Result();
result.code = errorCode.getCode();
result.reason = reason;
result.clientUseMessage = errorCode.getClientUseMessage();
return result;
}
}
public interface ErrorCode {
Integer getHttpCode();
String getCode();
String getReason();
String getFrontMessage();
}
@Getter
@NoArgsConstructor
public class Api {
private Result result;
private T body;
public static <T> Api<T> OK(T body) {
Api<T> api = new Api<>();
api.result = Result.OK();
api.body = body;
return api;
}
public static Api<Object> ERROR(Result result) {
Api<Object> api = new Api<>();
api.result = result;
api.body = new Object();
return api;
}
public static Api<Object> ERROR(ErrorCode errorCode) {
Api<Object> api = new Api<>();
api.result = Result.ERROR(errorCode);
api.body = new Object();
return api;
}
public static Api<Object> ERROR(ErrorCode errorCode, String reason) {
Api<Object> api = new Api<>();
api.result = Result.ERROR(errorCode, reason);
api.body = new Object();
return api;
}
}
public interface ApiException {
Integer getHttpCode();
Result getResult();
}
클라이언트와 잘 소통하는 것은 서버의 중요한 덕목이다. 이번 글에서는 우리의 소통 약속을 정하고 어떻게 하면 클라이언트와 잘 소통하는지에 대해 이야기 했다. 다음 시간에는 spring-web-mvc, spirng-webflux 두개의 큰 관심사에 대해 경계를 잘 나누어 어떻게 module로 분리하면 좋을지에 대해서 이야기 하는 시간 가지도록 하겠다.
질문 🧑🏻💻) 예외 상황에서는 클라이언트에게 body 값을 내려주지 않는다. 이렇게 설계를 하는것은 올바른가 예외 상황에서 값을 내려준다고 하면 어떠한 상황이 예외상황에서 body를 내려주는것이 적합한 것인가
- Api.java 파일의 ERROR 함수를 보면 body를 무조건 Object로 내리는 것을 확인할 수 있다. 즉, 지금 설계한 구조는 예외상황이 발생했을때는 body의 값을 받을 수 없다는 얘기이다. 이는 타당한가. 현재 필자는 이 부분에 대해 분석 / 연구를 진행하고 있다. 연구가 끝다면 다시 글을 쓰도록 하겠다.
감사합니다. 😀