Post

API 응답 코드 계층 구조 설계

1
2
3
4
5
6
7
8
9
10
11
12
      HTTP      |                 |
<---  Status    |  Response.code  | Response.data.code
       Code     |                 |
{
    httpStatusCode: ?,   --> httpStatusCode는 해당 서버로부터 오는 모든 api 응답의 일관된 처리를 위한 error code로서 의미를 가짐
    body: {
        code: ?,         --> body.code는 해당 서버로부터 오는 모든 api 응답의 일관된 처리를 위한 error code로서 의미를 가짐
        data: {
            code: ?      --> body.data.code는 api의 성공/실패 보다는 코드값 자체로서의 의미.
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
// @FE에서 받는 다면
http.get(`/api-1/example`)
    .then(resultCodeHandler(   --> body.code 에러 처리
        data => {
            data.code = ...
        },
        this.failUrl))
    .catch(() => {});          --> httpStatusCode 에러코드 처리
http.post(`/api-2/bar`)
    .then(resultCodeHandler(   --> api는 다르지만, body.code 에러 처리는 일원화하여 처리한다.
    ...
1
2
3
4
5
6
Response<T> 타입
{
    "code" : {CommonCode},    // body.code가 된다.
    "message" : {String},
    "data" : T
}
1
2
3
4
5
6
7
8
9
10
예제) Response<FooResult> 타입
{
    "code" : {CommonCode},    // 서버 전역적으로 사용되는 공통 응답 코드.
    "message" : {String},
    "data" : {                // 해당 API specific한 데이터
        "code": {FooEnum},
        "field1": bla,
        "field2": bla,
    }
}

HTTP status code와 Response.code

  • HTTP status code와 Response.code는 둘 다 ‘해당 서버로부터 오는 모든 api 응답의 일관된 처리를 위한 error code로서 의미’를 가진다.
    • 의미 중복이므로 가능하면 한 쪽으로 통합하는 것이 좋다.
  • 한 쪽으로 통합한다면 어느 쪽으로 통합하는 것이 나은가?
    • HTTP status code를 이용해서 Response<T> layer를 없애는 것이 좋다.
    • http code는 숫자가 제한 되어 있고, 세세하게 커스텀이 불가능하다는 단점이 있지만, 그 보다는 아래 Response<T>의 단점이 더 커보인다.

1. Response<T> 을 반환하게 되면, 받는 쪽에서도 보통 제네릭으로 받아야 하므로 ParameterizedTypeReference를 사용해야 한다.

1
2
3
4
5
6
7
8
9
// Http status code를 사용하는 restful API의 경우
.bodyToMono(BarResponse::class.java)  
.doOnError(WebClientResponseException::class.java) { 
    ...it.responseBodyAsString... 
}

// Response<T>를 쓰는 경우 (항상 200 응답. 그 안에 결과 code가 포함)
.bodyToMono(ptypeReference<FooResponse<List<AssetStatus>>>())
.doOnSuccess { checkSuccess(it) }
  • API 성공 시에는 ptypeReference를 쓰고 별도 check를 해주어야 한다는 것이 약간 번거롭기는 하지만, Response<T>를 써도 큰 문제는 없다.
  • 하지만 API 실패 시에는 응답으로 내려가는 필드가 성공 일 때와는 달라서, Response<T> 방식에 단점이 있다.
    • HTTP status code를 사용하는 restful 방식은 responseBodyAsString로 실패 응답 body를 string으로 간주하기 때문에, 그냥 로그 찍으면 모든 필드가 다 로깅 된다.
    • 반면 Response<T> 방식은, 일단 dto로 받고, dto.isFailure 이면 로그를 찍게 되는데, 이 때 dto 필드에 없는 필드는 로그에 찍히지 않는다.
    • 이를 방지하기 위해서는 일단 Map으로 받아서 실패이면 로그를 찍도록 해야 하는데, 어느쪽이든 조금 애매하다.

HTTP status code를 쓰더라도, 실패 메시지를 body에 주면 된다.

2. 보통은 애초에 실패에 대한 정보를 그렇게 계층적으로 구조화 할 필요가 없어서 불필요한 체크가 늘어난다. (HttpStatus 만으로도 충분하다)

1
2
3
4
5
6
7
ResponseEntity<Response<MyResponse>> responseEntity = ...

if (!responseEntity.getStatusCode().is2xxSuccessful()
    || !ResultStatus.OK.getCode().equals(responseEntity.getBody().getCode())
    || responseEntity.getBody().getData().getAuthLevel() != AuthLevel.ONE) {
    new FooException("오류");
}

예제를 보면 체크를 3번이나 수행했다.

  1. 외부 API 응답 자체가 정상인지 (200 OK)
  2. 외부 API의 Response.code 가 정상인지
  3. 외부 API의 Response.data.code(authLevel) 이 정상인지
  • 물론 어디서 에러가 발생했는지를 구분해서 처리가 달라야 한다면 위와 같이 3번 체크하는 것이 의미가 있을 수 있다.
  • 하지만 이렇게 3단계로 계층화 해서 처리해야 하는 경우는 잘 없다. 보통 내가 관심 있는 것은 “외부 API가 결과 data를 줬냐 안줬냐” 이다. (1,2번 묶음)
    • 보통 그냥 외부 API에서 응답 제대로 주지 않는 [1, 2] 상황은 굳이 구분 할 필요가 없어서 묶어서 체크하는게 낫다. => REST api
  • [2, 3]을 묶어서 처리하는 것은 당연히 불가능하다. authLevel은 꼭 봐야 하니까.

이렇게 여러 체크 중 어디서 실패하든 종단 처리로 가야하는 경우, Optional.filter 사용하는게 더 깔끔할 수 있다.

Response.code와 Response.data.code

Response.data.code는 꼭 필요한 순간들이 있다.
위 예제에서 authLevel은 클라이언트에서 반드시 참조해야 하는 data code 이므로 [2, 3]을 묶어서 처리하는 것은 불가능함을 확인했다.

하지만 Response.data.code가 authLevel 같은 data code가 아니라, 단순한 성공/실패 응답 코드의 의미를 지닐 때에도 Response.data.code를 별도로 두는 것이 나은가?

예를 들어 bin 조회 같은 경우, 응답 케이스를 다음과 같이 세분화 할 수 있는데

  1. 성공 및 결과 반환
  2. 성공이나 해당 bin 찾을 수 없음 (NOT FOUND)
  3. 성공이나 미지원 카드사
  4. 실패로 입력값 에러 (PARAM ERROR)
  5. 외부 에러 (EXTERNAL ERROR)

클라이언트 측에서 1~5 응답에 따른 분기 처리가 필요한 상황이라고 가정할 때, 사용 할 수 있는 방법은 아래와 같다. (즉, 1~5 응답은 반드시 클라이언트에서 구분되어야 한다.)

  1. [1-5] 를 모두 통합 코드 enum에 추가한다. (Response<T> layer가 있는 경우)
    • 해당 API에서만 발생하는 오류(특히 3번)임에도 통합 코드에 넣는 것이 적절하지 않아 보인다.
    • 이런 식으로 각 API에서 모두 통합 코드에 추가하다 보면 통합 코드 표가 너무 비대해진다.
  2. [1-5] 각각 Http status code를 하나씩 부여한다. (Response<T> layer가 없는 경우)
    • 1, 2, 3은 모두 성공인데, 각각 어떤 status code를 내려 줄 것인가?
    • [1=200, 2=204, 3=???…] 애매하다. 이처럼 HTTP status 정의와 서버에서 의도한 응답의 의미는 100% 일치하지 않는 경우가 잦은데, API docs에 “이 API에서 발생하는 xxx status는 이런 의미를 가진다.” 라고 반드시 명시해주어야 한다.
    • docs에 명시 한다고 하더라도, 따로 message를 응답으로 주는 것이 아니기 때문에 어떤 Error message를 띄울지는 클라이언트에서 모두 관리해야 한다.
    • 클라이언트에서 HTTP status에 따른 통합 handler를 사용하고 있다면, 거기에 들어맞지 않을 가능성도 크다.
  3. [1-3]은 Http status 200으로 처리하고, 그 안에서 Response.data.code를 사용해 구분한다.
    • 이런 상황에서 사용했을 때 여러모로 장점이 많다.
    • 세부 응답 코드는 API에 종속적인 코드이므로, 변경이 있을 때 해당 API 이용자 에게만 고지하면 된다. (공통 응답 코드 기조를 바꾸는 것 보다 부담이 적다)
    • 코드를 2개 이상 반환할 수 있다는 장점도 있다.
    • 메시지도 함께 반환 가능하다.

즉 HTTP status(또는 Response.code)의 하위 집합으로 Response.data.code를 사용하는게 도움이 되는 경우가 많다.

기타

중요 ) 응답 코드를 구성할 때는 클라이언트의 관심사가 무엇이냐를 생각해야 한다.

외부에서 받은 응답 코드를 그대로 내려주는 것이 항상 좋은 방법은 아닐 수 있음.
클라이언트가 필요로 하지 않는 정보라면 응답 코드를 적당히 추상화해서 내려주는 것도 필요하다.

즉, 공통 코드 + 세부 코드로 내려줄 수 있는 상황이지만, 공통 코드로 merge 해서 내려주는 것.

이를 판단하는 좋은 기준은, 어떤 클라이언트를 사용하더라도(클라이언트가 n개가 되더라도) 모두 같은 처리를 할 것 같은가?를 생각해보는 것이다.

공통 응답 코드를 사용할 때는, 해당 공통 응답 코드를 깊은 layer에서 반환 할 가능성에 대해서도 생각해야 한다.
그래서 공통 응답 코드는 반드시 의미와 역할이 명확해야 한다. 의미가 명확한 공통 응답 코드라면, 어느 layer에서 반환하든 같은 의미를 지닐 것이므로 클라이언트에서 받았을 때 처리가 달라지지 않아 문제를 예방할 수 있다.

공통 응답 코드 vs 개별 응답 코드

서버에서 제공하는 api 이더라도 각 api 마다 응답 코드도 다르고 의미도 다르다면, 사용하는 쪽에서 일관된 처리 인터페이스를 작성할 수 없다.
그래서 상기한 응답 구조는 해당 서버로부터 오는 모든 api 응답에 대해 일관된 처리를 공유할 수 있도록 일종의공통 응답 코드(CommonResponseCode)가 존재해야 함을 전제하고 있다.

 공통 응답 코드가 있는 경우개별 응답 코드만 사용하는 경우
응답 코드 Enum대부분의 응답은 공통 응답 코드 Enum을 참조하여 해결할 수 있다.각 API 마다 모두 각자의 응답 코드 Enum을 정의해야 한다.
클라이언트 처리클라이언트 코드에서 공통 처리부 CommonHandler를 정의할 수 있다.공통 처리부를 정의할 수 없다. 그러나 어차피 같은 공통 응답 코드(e.g., 404)반환하더라도 API 마다 그 응답의 의미가 조금씩 다르기 때문에 공통 처리부는 크게 의미가 없을 수 있다.

이러한 장단점을 생각해 보면 공통 응답 코드가 존재하는 것이
최상위 응답 코드(http status든, res.code든)는 공통 응답 코드의 의미를 지니게 하는 것이
관리하기 더 나은 경우가 많았다.

This post is licensed under CC BY 4.0 by the author.