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: 통합 에러 코드1body.code: 통합 에러 코드2body.data.code: 개별 에러 코드
2안. httpStatusCode + body.code 만 사용하는 경우
httpStatusCode: 통합 에러 코드body.code: 개별 에러 코드 (tip: 성공/실패 상관 없이 항상 존재해야 클라이언트 처리가 손쉬움)
3안. => X. body.code + body.data.code 만 사용하는 경우httpStatusCode는 생략 불가
[!warning] httpStatusCode는 무조건 성공/실패 구분에 사용해야 한다. 만약 생략하고 모든 응답을 200 처리하게 되면,
- pinpoint 같은 로깅 라이브러리들은 서버에서 500 응답이 나가는 것을 기반으로 실패를 체크하기 때문에, 이런 라이브러리들과 호환이 되지 않는다. (그 외에도 CDN, LB, API GW 등과 호환 이슈)
- 성공 뿐만 아니라 오류 응답도 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번이나 수행했다.
- 외부 API 응답 자체가 정상인지 (200 OK)
- 외부 API의 Response.code 가 정상인지
- 외부 API의 Response.data.code(authLevel) 이 정상인지
This post is licensed under CC BY 4.0 by the author.