외부 API·시스템 연계 · API Integration

실패했는데 HTTP 200을 반환하면 안 되는 이유

Well 좌표 충돌로 서버 처리가 실패했는데도 HTTP 200과 빈 본문이 반환되던 경로를 추적하고, HTTP 500과 표준 errorCode·errorMessage 계약으로 바꾼 과정입니다.

REST APIHTTP StatusError ContractSpring BootIntegration

문제의 출발점

이 글은 CIWER·WaveChecker 제품 전체가 아니라, LIMS REST API에서 담당한 오류 응답 후속 안정화 범위를 다룹니다. 실제 요청값과 내부 식별정보는 제거했습니다.

서버 로그에는 오류가 있는데 호출자는 성공으로 봤다

분석 처리와 확인 사이에 동일한 Well 좌표가 충돌하는 조건이 발생하면 서버에서는 예외가 기록됐다. 그런데 API 응답은 HTTP 200이었고 본문은 비어 있었다.

호출 시스템 관점에서 이 응답은 해석할 정보가 없다.

  • HTTP 상태는 성공이라고 말한다.
  • 응답 본문에는 성공 결과도 실패 사유도 없다.
  • 재시도해야 하는지, 다음 단계로 진행해야 하는지 판단할 수 없다.

내부 로그에 오류가 정확히 남아 있어도 시스템 간 계약이 실패를 표현하지 못하면, 호출자는 성공 흐름을 계속 실행하거나 별도의 시간 초과와 데이터 불일치로 문제를 뒤늦게 발견한다.

오류를 세 개의 신호로 나눴다

REST API의 실패는 한 가지 값으로 끝나지 않는다.

오류를 세 개의 신호로 나눴다 데이터 표
신호역할이 사례의 문제
HTTP Status요청 처리의 성공·실패를 전송 계층에서 구분실패인데 200 반환
오류 본문호출자가 기록·분기할 기계 판독 정보 제공빈 본문
서버 트랜잭션·상태데이터가 실제로 반영됐는지 보장응답만 보고 판단하기 어려움

errorMessage만 본문에 추가하면서 상태를 200으로 유지하면 HTTP 클라이언트, 모니터링과 재시도 정책은 여전히 성공으로 분류할 수 있다. 반대로 500만 반환하고 본문을 비우면 호출자는 실패를 알 수는 있지만 어떤 업무 조건이 문제였는지 구분하기 어렵다.

그래서 상태와 본문을 함께 바꿔야 했다.

빈 성공 응답이 만들어진 경로를 좁혔다

확인할 지점은 Well 중복을 검출하는 Query 하나가 아니었다. 예외가 발생한 뒤 실제 HTTP 응답으로 변환되기까지의 흐름을 따라갔다.

아래 코드는 문제 해결 구조를 설명하기 위한 pseudocode이며 실제 클래스·테이블·설정 이름과는 무관합니다.

Well 좌표 검증
  → Service 예외 발생
  → 공통 예외 처리
  → HTTP Status와 오류 본문 생성
  → 호출 시스템 응답 수신

서버의 공통 예외 처리 로그에는 오류가 남았지만 최종 응답 경로에서는 상태가 성공으로 유지되고 본문이 비어 있었다. 즉, 원인을 “중복 데이터가 생겼다”에서 끝내지 않고 검출된 실패가 API 계약으로 전달되지 않는 문제로 분리했다.

수정도 Service와 Mapper 경계를 함께 확인했다. 좌표 충돌 여부를 정확히 판정하는 것과, 판정 결과를 명시적 예외로 전달하는 것, 그 예외를 표준 HTTP 응답으로 매핑하는 것은 서로 다른 책임이기 때문이다.

선택한 오류 계약

기존 연계 계약에서 이 조건은 서버가 요청 처리를 완료하지 못한 실패로 다뤘다. 개선 후 호출 시스템이 일관되게 판정할 수 있도록 다음 형태로 통합했다.

아래 코드는 문제 해결 구조를 설명하기 위한 pseudocode이며 실제 클래스·테이블·설정 이름과는 무관합니다.

HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
  "errorCode": "GENERALIZED_ERROR_CODE",
  "errorMessage": "요청을 처리할 수 없습니다."
}

실제 오류 코드와 메시지는 내부 업무 정보를 드러내지 않도록 예시에서 일반화했다. 중요한 규칙은 다음 세 가지다.

  • 성공한 요청만 2xx로 반환
  • 처리 실패는 기존 연계 규칙에 맞는 비2xx 상태로 반환
  • 호출자가 저장·분기할 수 있는 errorCode와 사람이 확인할 errorMessage를 함께 제공

응답을 만드는 위치도 각 Service의 catch 블록에 흩어놓지 않고 공통 예외 처리 경계로 모았다. Service는 실패를 숨기지 않고 전달하고, 예외 처리기는 전송 계약으로 변환한다.

HTTP 상태만 바꿔서는 충분하지 않다

500 응답은 실패 전달을 바로잡지만 데이터 정합성을 자동으로 보장하지 않는다. 예외가 발생하기 전에 일부 데이터가 저장됐다면 HTTP 계약과 별개로 트랜잭션 Rollback을 확인해야 한다.

따라서 검증에서는 다음을 함께 봤다.

  • Well 좌표 충돌이 실제 실패 조건으로 판정되는지
  • 응답 상태가 200이 아니라 500인지
  • 본문에 표준 errorCodeerrorMessage가 존재하는지
  • 정상 요청의 기존 성공 응답이 바뀌지 않는지
  • 실패 경로에서 호출자가 빈 성공으로 오인할 응답이 남지 않는지

사용자 검증에서는 2개 고유 시나리오를 확인했다. 내부·사용자 단계가 섞인 테스트 표의 총 5행은 내부 4행 Y와 사용자 1행 N을 포함하므로, 5개 고유 시나리오나 5건 성공으로 집계하지 않았다. Service·Mapper 변경 이력과 최종 완료 상태는 테스트 행 수와 별도로 대조했다.

호출자 관점에서 검증했다

서버 단위 테스트에서 예외가 발생하는지만 확인하면 “예외를 잘 던졌다”는 사실까지만 알 수 있다. 이번 문제는 예외 발생 이후의 HTTP 응답이 잘못됐기 때문에, 최종 검증 기준도 호출자가 받는 값이어야 했다.

아래 코드는 문제 해결 구조를 설명하기 위한 pseudocode이며 실제 클래스·테이블·설정 이름과는 무관합니다.

검증 전
서버: ERROR 로그
호출자: 200 + 빈 본문 → 성공인지 실패인지 판정 불가

검증 후
서버: 오류 기록
호출자: 500 + errorCode + errorMessage → 실패 분기 가능

API 제공자 로그와 호출자 응답을 같은 테스트 케이스에서 비교해야 중간 계층이 상태나 본문을 다시 바꾸는 문제를 놓치지 않는다.

운영 계약으로서의 오류 응답

오류 응답은 개발 편의를 위한 부가 정보가 아니다. 연계 시스템이 다음 행동을 결정하는 운영 계약이다.

  • 성공이면 다음 분석·저장 단계로 진행
  • 실패면 해당 요청을 보류하고 원인을 기록
  • 재시도 가능한 조건이면 정해진 정책에 따라 재요청
  • 사람이 확인해야 하면 오류 코드와 메시지를 운영 화면이나 로그에 남김

200과 빈 본문은 이 모든 분기를 막는다. 서버 안에서 예외가 발생했다는 사실보다 더 중요한 것은, 실패가 네트워크 경계를 넘어 호출자에게 손실 없이 전달되는가이다.

이번 개선의 결과는 전체 API 오류 체계를 새로 만들었다는 것이 아니다. Well 좌표 충돌이라는 확인된 운영 예외에서 HTTP 200·빈 응답이 반환되던 경로를 HTTP 500과 표준 오류 본문으로 바꾸고, 호출 시스템이 실패를 식별할 수 있게 한 범위다. 작은 상태 코드 변경처럼 보여도 실제로는 서비스, 예외 처리, Mapper와 호출자 계약을 함께 맞춰야 하는 연계 안정화 작업이었다.

TECHNICAL SERIES

CIWER 연계 구조와 운영 안정화

일회성 프로세스를 상시 기동 API로 바꾸고 장시간 작업과 오류 응답 계약을 안정화한 과정입니다.

시리즈 전체 보기

현재 3/3

  1. 1Connection Pool을 적용해도 빨라지지 않은 이유
  2. 226시간 Blast 작업에서 24시간 DB 세션이 만료된 이유
  3. 3실패했는데 HTTP 200을 반환하면 안 되는 이유