REST API를 개발하다 보면 외부 API를 호출하고 그에 대한 예외를 처리해야 하는 상황이 자주 발생한다.
외부 API에서 내려주는 HTTP 상태 코드와 에러 메시지를 내 서비스의 예외 처리 방식과 일관되게 매핑해야 한다.
이 글에서는 외부 API 예외를 내 서비스에서 어떻게 처리하면 좋을지, 그리고 확장 가능한 예외 처리 구조를 어떻게 설계할지 정리해본다.
📒 외부 API 예외 응답의 형태
일반적으로 외부 API는 자체적으로 정의한 에러 코드, 에러 메시지를 반환한다.
국회 api 예시를 보자.
자체적으로 정의한 에러코드, 에러메세지를 반환하고 있다.
http 상태코드는 api 마다 다른데, 국회 api는 에러여도 200 ok로 온다.
📒 내 서비스에서의 예외 처리 방식
private void validatePageAndSize(final int page, final int size) {
if (page < 0) {
throw new CustomException(PageableErrorCode.INVALID_PAGE);
}
if (size < 1) {
throw new CustomException(PageableErrorCode.INVALID_SIZE);
}
}
// PageableErrorCode
INVALID_SIZE(HttpStatus.BAD_REQUEST, "PGE002", "size 값은 0 이상이어야 합니다"),
CustomException과 ErrorCode를 정의해 사용한다. ErrorCode는 enum으로 http 상태코드, 내가 정의한 에러코드, 에러메세지로 구성된다.
// 커스텀 예외 처리
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
e.printStackTrace();
return handleExceptionInternal(e);
}
private ResponseEntity<ErrorResponse> handleExceptionInternal(CustomException e) {
return ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(makeErrorResponse(e.getErrorCode()));
}
private ErrorResponse makeErrorResponse(ErrorCode errorCode) {
return ErrorResponse.builder().code(errorCode.getCode()).message(errorCode.getMessage()).build();
}
@RestControllerAdvice 에서 전역적으로 처리한다. 응답은 ErrorResponse 객체로 통일해 일관되게 응답한다.
✅ 외부 API 예외를 내 서비스와 매핑하기
@RequiredArgsConstructor
@Getter
public enum CongressApiErrorCode implements ErrorCode {
MISSING_REQUIRED_VALUE(HttpStatus.BAD_REQUEST, "300", "필수 값이 누락되어 있습니다. 요청인자를 참고 하십시오."),
INVALID_CAP_KEY(HttpStatus.UNAUTHORIZED, "290", "인증키가 유효하지 않습니다. 인증키가 없는 경우, 홈페이지에서 인증키를 신청하십시오."),
TRAFFIC_LIMIT_EXCEEDED(HttpStatus.FORBIDDEN, "337", "일별 트래픽 제한을 넘은 호출입니다. 오늘은 더이상 호출할 수 없습니다."),
SERVICE_NOT_FOUND(HttpStatus.NOT_FOUND, "310", "해당하는 서비스를 찾을 수 없습니다. 요청인자 중 SERVICE를 확인하십시오."),
INVALID_REQUEST_TYPE(HttpStatus.BAD_REQUEST, "333", "요청위치 값의 타입이 유효하지 않습니다. 요청위치 값은 정수를 입력하세요."),
MAX_REQUEST_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "336", "데이터요청은 한번에 최대 1,000건을 넘을 수 없습니다."),
SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "서버 오류입니다. 지속적으로 발생시 홈페이지로 문의(Q&A) 바랍니다."),
DATABASE_CONNECTION_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "600",
"데이터베이스 연결 오류입니다. 지속적으로 발생시 홈페이지로 문의(Q&A) 바랍니다."),
SQL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "601", "SQL 문장 오류 입니다. 지속적으로 발생시 홈페이지로 문의(Q&A) 바랍니다."),
CERTIFICATE_REVOKED(HttpStatus.UNAUTHORIZED, "990", "인증서가 폐기되었습니다. 홈페이지에서 인증키를 확인하십시오."),
CAP_KEY_RESTRICTED(HttpStatus.FORBIDDEN, "300", "관리자에 의해 인증키 사용이 제한되었습니다."),
NO_DATA_FOUND(HttpStatus.NOT_FOUND, "200", "해당하는 데이터가 없습니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
public static CongressApiErrorCode from(String code) {
for (CongressApiErrorCode congressApiErrorCode : values()) {
if (congressApiErrorCode.getCode().equals(code)) {
return congressApiErrorCode;
}
}
throw new CustomException(CommonErrorCode.INVALID_ERROR_CODE);
}
}
외부 api 에서 제공하는 문서를 참고해 에러코드를 만들어주자. (chatgpt가 잘 만들어준다 😘)
국회 api를 예로 들었는데, 국회 api 요청 후 응답을 파싱해 에러 응답이라면 정적팩터리 메서드 from 으로 CongressApiErrorCode 로 변환해주자.
✅ 외부 API 예외처리
private void handleApiError(final JsonNode rootNode) {
String errorCode = congressApiClient.getFieldValue(rootNode, "RESULT", "CODE").split("-")[1];
throw new ApiException(CongressApiErrorCode.from(errorCode));
}
외부 api 예외는 ApiException 을 던져준다.
현재로써는 CustomException 을 써도 무관하지만, 외부 api 예외처리에 대해 요구사항이 변경될 수 있다.
예를 들어 외부 API에 재요청하는 등 API별로 다른 대응이 필요할 수 있다.
따라서 확장성 및 유지보수성을 고려해 ApiException 클래스를 만들어 사용했다.
// 외부 api 예외 처리
@ExceptionHandler(ApiException.class)
public ResponseEntity<ApiErrorResponse> handleApiException(ApiException e) {
e.printStackTrace();
return handleExceptionInternal(e);
}
private ResponseEntity<ApiErrorResponse> handleExceptionInternal(ApiException e) {
return ResponseEntity.status(e.getErrorCode().getHttpStatus()).body(ApiErrorResponse.from(e.getErrorCode()));
}
역시 @RestControllerAdvice 에서 전역적으로 처리하며 응답은 ApiException, ErrorResponse 객체로 통일해 일관되게 응답한다.
외부 api 예외 처리를 변경하고 싶다면 해당 부분만 변경해주면 된다. 😁
👌 결론
✅ 외부 API 에러 응답을 내 서비스의 ErrorCode와 매핑하여 일관되게 예외 처리
✅ 내부 서비스 예외 → CustomException + ErrorResponse
✅ 외부 API 예외 → ApiException + ApiErrorResponse
이렇게 하면 외부 API 예외와 내부 서비스 예외를 명확하게 구분하고, 확장 가능하면서도 유지보수가 쉬운 예외 처리 구조를 만들 수 있다!
'스프링' 카테고리의 다른 글
Pageable 커스텀 예외처리 (0) | 2025.06.07 |
---|---|
트랜잭션 내부에서 외부 api 호출하지 말자 (0) | 2025.02.03 |
spring 요청 Validation 검증, 예외 처리 (0) | 2025.01.16 |
테스트 시 @Value 사용 (0) | 2024.11.04 |
dto를 사용해야 하는 이유 (0) | 2024.09.01 |