Post

API 응답 코드 계층 구조 설계

API 응답 코드 계층 구조 설계

Decomposition

  • API 응답의 상태 (e.g. 성공, 실패, 특수실패)를 표현하기 위해서는 코드가 필요하다.
  • 이를 표현하기 위해 필드에서 사용하는 코드를 모두 나열하면 아래와 같이 3가지다. ```js HTTP | | <— Status | body.code | body.data.code Code | |

HTTP/1.1 ? –> httpStatusCode { code: ?, –> body.code message: …, data: { code: ? –> body.data.code } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
### 예시
- BE에서 3가지 응답 code를 모두 사용하는 타입이 있다고 가정하면 아래와 같다. 
```json
Response<T> 타입
{
    "code" : {CommonCode},    // 서버 전역적으로 사용되는 공통 응답 코드.
    "message" : {String},
    "data" : T
}

예제) Response<FooResult> 타입
{
    "code" : {CommonCode},    // body.code가 된다.
    "message" : {String},
    "data" : {                // 해당 API specific한 데이터
        "code": {FooEnum},    // body.data.code가 된다.
        "field1": bla,
        "field2": bla,
    }
}
  • Response<FooResult>를 FE에서 받아서 처리하는 방식은 아래와 같다.
    1
    2
    3
    4
    5
    6
    7
    8
    
    // @FE에서 받는 다면
    http.get(`/api-1/example`)
      .then(resultCodeHandler(   --> body.code 처리 (보통 일원화하여 처리한다.)
          data => {
              data.code = ...   --> body.data.code 처리 (보통 API마다 별도 처리)
          },
          this.failUrl))
      .catch(() => {});          --> httpStatusCode 처리 (에러인 경우만)
    

Q. 3가지를 다 써야 할까? 2가지만 써도 될까?

1안. httpStatusCode + body.code + body.data.code 모두 사용하는 경우 (위 예시 케이스)

  • httpStatusCode: 통합 에러 코드1
  • body.code: 통합 에러 코드2
  • body.data.code: 개별 에러 코드

2안. httpStatusCode + body.code 만 사용하는 경우

  • httpStatusCode: 통합 에러 코드
  • body.code: 개별 에러 코드 (tip: 성공/실패 상관 없이 항상 존재해야 클라이언트 처리가 손쉬움)

3안. body.code + body.data.code 만 사용하는 경우 => X. httpStatusCode는 생략 불가

[!warning] httpStatusCode는 무조건 성공/실패 구분에 사용해야 한다. 만약 생략하고 모든 응답을 200 처리하게 되면,

  1. pinpoint 같은 로깅 라이브러리들은 서버에서 500 응답이 나가는 것을 기반으로 실패를 체크하기 때문에, 이런 라이브러리들과 호환이 되지 않는다. (그 외에도 CDN, LB, API GW 등과 호환 이슈)
  2. 성공 뿐만 아니라 오류 응답도 200 OK로 나가면, 원치 않는 오류 응답이 캐싱되어 클라이언트가 오래된 오류 정보를 계속 받게 될 위험이 있다.

즉, HTTP Status Code는 HTTP 프로토콜 수준의, 모든 웹 통신에 공통적으로 적용되는 추상적이고 범용적인 오류 분류. 클라이언트 라이브러리, 네트워크 인프라(게이트웨이, 방화벽)가 이해하는 언어이기 때문에 고정 200으로 나가서는 안된다.

best practice : 2안

  • 2안으로 가도 충분히 통합 에러 코드와 개별 에러 코드를 모두 표현 할 수 있다.
  • 클라이언트는 API-specific 에러 코드를 처리하기 위해 API 마다 에러 코드 표를 따로 관리해야 한다.
  • 클라이언트 측 통합 에러 처리 핸들러는 HTTP Status Code를 기반으로 작성하면 된다.

예시 1) 필수 필드 누락, 형식 오류 케이스에 대한 응답 예시

1
2
3
4
5
HTTP/1.1 400 Bad Request
{
    "code": "REQUIRED_FIELD_MISSING", // API-specific 에러 코드
    "message": "필수 필드 'username'이(가) 누락되었습니다.",
}
1
2
3
4
5
HTTP/1.1 400 Bad Request
{
    "code": "INVALID_EMAIL_FORMAT",   // API-specific 에러 코드
    "message": "이메일 주소 형식이 올바르지 않습니다. 유효한 이메일을 입력해주세요.",
}

예시 2) BIN 조회

1
2
3
4
5
6
HTTP/1.1 200 OK
{
    "code": "OK",
    "message": "성공",
    "data": ...
}
1
2
3
4
5
HTTP/1.1 200 OK
{
    "code": "NOT_SUPPORT",
    "message": "빈 조회는 성공했으나 미지원 카드사",
}
1
2
3
4
5
HTTP/1.1 400 Bad Request
{
    "code": "PARAM_ERROR",
    "message": "입력값 에러",
}
1
2
3
4
5
HTTP/1.1 500 Internal Server Error
{
    "code": "EXTERNAL_ERROR",
    "message": "외부 API 요청 중 에러",
}

2안의 Decomposition

1
2
3
4
5
6
7
8
9
10
       HTTP     |                 |
<---  Status    |    body.code    |
       Code     |                 |

HTTP/1.1 ?       --> 통합 성공/실패 에러 코드
{
    code: ?,     --> API-specific 에러 코드
    message: ...,
    data: {...}
}

2안의 BE 응답 타입

1
2
3
4
5
6
Response<T, E> 타입
{
    "code" : E,             // API-specific 에러 코드 enum
    "message" : {String},
    "data" : T              // 성공  때만 존재
}

body.code에 쓸 enum을 API 마다 따로 정의해야 하는가?

  • 통합 에러 코드 enum 1개 vs API-specific 에러 코드 enum N개
  • 이건 비즈니스 상황에 맞게 처리하면 된다. 통합 에러 코드 enum으로 가고, message만 바꾸는 방식도 흔하다.
  • 둘 다 쓰는 방법도 괜찮다. 일반적인 경우 E에 통합 코드를 넣고, 특수한 코드를 반환해야 하는 경우 E에 API-specific enum을 넣는 방식이다.

왜 1안보다 2안이 더 나은가?

1안으로 가게 되면 통합 에러 코드를 처리하기 위해서 HTTP Status Code와 body.code를 둘 다 봐야 된다는 단점.
이는 아래와 같은 클라이언트 처리 부담을 야기함.

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) 이 정상인지
This post is licensed under CC BY 4.0 by the author.