Spring Boot 외부 연동 API 설계에서 가장 자주 무너지는 지점은 코드 자체가 아니라 운영 후 추적 가능성입니다. 외부 시스템과의 연동은 양측 코드가 완전히 정상이어도 네트워크, 직렬화 규약, 배포 시점이 어긋나는 순간 장애로 이어집니다. 그때 가장 먼저 묻게 되는 질문은 단 하나입니다. “그래서 그 시점에 어떤 값이 들어왔고, 어떤 값을 돌려보냈는가?”
이 글은 실제 운영 환경에서 외부 연동 API를 다루며 반복적으로 검증된 두 가지 원칙을 정리했습니다. 첫째는 모든 Request/Response를 빠짐없이 로그로 남기는 것, 둘째는 동시 개발 상황에서 Swagger 기반 Mock을 먼저 합의하는 것입니다.
외부 연동 API 설계에서 마주치는 두 가지 상황
외부 연동은 크게 두 가지 형태로 나뉩니다. 하나는 이미 운영 중인 외부 API와 새로 연동하는 경우이고, 다른 하나는 양측이 동시에 새로 개발하는 경우입니다. 두 경우는 마주치는 문제의 종류가 다르고, 그에 따라 설계 우선순위도 달라져야 합니다.
이미 운영 중인 외부 API와 연동할 때 가장 위험한 것은 외부 측의 조용한 변경입니다. 응답 포맷이 바뀌거나, JSON 키 케이스가 카멜케이스에서 스네이크케이스로 변경되거나, 인증 헤더 규약이 업데이트되는 일이 사전 공지 없이 일어나곤 합니다. 그 결과는 보통 다음과 같이 나타납니다. 파싱 예외로 인한 500 에러가 갑자기 발생하거나, 필수값이 null로 들어와 DB INSERT가 실패하거나, 인증이 풀려 401·403이 쏟아지는 상황입니다. 이때 수신 측에 요청 본문 자체가 남아 있지 않으면, “외부가 바꿨는지, 우리가 잘못 받는지”를 증명할 방법이 없어 책임 공방으로 시간이 흘러갑니다.
동시 개발의 경우에는 일정 미스매치와 명세 어긋남이 가장 큰 위험입니다. 외부 측 API가 지연되면 이쪽 개발 일정이 따라서 밀리고, 임시로 하드코딩해 둔 응답이 그대로 운영에 흘러들어가기도 합니다. 더 자주 발생하는 문제는 막판 통합 테스트 단계에서 양측의 직렬화 규약이 어긋난 것이 그제서야 발견되는 상황입니다. 한쪽은 카멜케이스로 보내고 다른 쪽은 스네이크케이스로 받는 식의 미스매치는 오픈 직전 발견되어 일정 전체를 흔듭니다.
두 경우 모두 결국 본질은 같습니다. 계약(Contract)이 명확하지 않거나, 실제 주고받은 데이터의 흔적이 남지 않을 때 외부 연동 API는 무너집니다. 이 두 가지 약점을 메우는 것이 바로 뒤이어 설명할 로그 원칙과 Swagger Mock 우선 개발 원칙입니다.
원칙 1: Request와 Response는 무조건 로그로 남긴다
외부 연동 API에서 가장 먼저 적용해야 할 원칙은 매우 단순합니다. 들어온 값과 나간 값을 모두 기록합니다. 예외 없이 적용합니다.
운영 중 다음과 같은 문의가 들어오는 상황을 가정해 보겠습니다.
“오늘 새벽 3시쯤 데이터 잘 보냈는데, 그쪽 시스템에 안 들어간 것 같아요. 확인 부탁드립니다.”
이 한 문장 뒤에는 두 가지 가능한 진실이 있습니다. 외부 측이 보냈다고 주장하는 데이터가 실제로 도착하지 않았거나, 도착은 했는데 비즈니스 로직에서 검증 실패로 거절되었거나입니다. 이 두 가지를 분리해서 증명할 수 있는 유일한 도구가 로그입니다.
무엇을 남겨야 하는가
연동 API의 로그에는 최소한 다음 항목이 포함되어야 합니다.
| 항목 | 설명 | 왜 필요한가 |
|---|---|---|
| 호출 시각 | ms 단위 타임스탬프 | 외부 측 로그와 시계열 대조 |
| 클라이언트 IP | 호출 측 식별자 | 인증·방화벽 이슈 판별 |
| HTTP 메서드/경로 | POST /api/data/insert | 어떤 엔드포인트로 들어왔는가 |
| 요청 헤더 | Content-Type, 인증 헤더 등 | 규약 변경 감지 |
| 요청 본문 | 전체 JSON Body | 직렬화·필드 검증의 근거 |
| 응답 코드/본문 | HTTP Status + Body | 우리가 무엇을 돌려보냈는가 |
| 처리 소요시간 | ms 단위 | 성능 저하 추적 |
| 트랜잭션 ID | UUID 또는 traceId | 분산 추적, 한 요청의 전 구간 |
이 중에서도 요청 본문 전체와 응답 본문 전체는 절대 누락해서는 안 됩니다. 컬럼명·타입·인코딩 분쟁이 발생했을 때 객관적 증거가 되어 줍니다.
Spring Boot 로깅 구현 패턴
서블릿의 HttpServletRequest는 본문을 한 번만 읽을 수 있기 때문에, 본문을 캡처하려면 ContentCachingRequestWrapper/ContentCachingResponseWrapper를 사용해야 합니다.
@Component
public class ApiAccessLogFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger("API_ACCESS");
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
ContentCachingRequestWrapper request = new ContentCachingRequestWrapper(req);
ContentCachingResponseWrapper response = new ContentCachingResponseWrapper(res);
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
long start = System.currentTimeMillis();
try {
chain.doFilter(request, response);
} finally {
long elapsed = System.currentTimeMillis() - start;
log.info("[REQ] {} {} body={} headers={}",
request.getMethod(), request.getRequestURI(),
mask(body(request)), headerMap(request));
log.info("[RES] status={} body={} elapsedMs={}",
response.getStatus(), mask(body(response)), elapsed);
response.copyBodyToResponse();
MDC.clear();
}
}
}
핵심은 세 가지입니다. 요청 본문을 안전하게 읽기 위한 래퍼 사용, traceId로 한 요청의 모든 로그를 묶을 수 있는 MDC 활용, 그리고 민감정보 마스킹입니다.
민감정보 마스킹과 로그 크기 관리
로그를 다 남기라고 했지만, 그대로 남기면 안 되는 값이 있습니다. 주민등록번호, 카드번호, 휴대전화번호, 토큰 같은 정보는 정규식 기반으로 마스킹한 후 저장해야 합니다.
또한 로그 저장소의 컬럼 크기를 반드시 확인해야 합니다. 한 번은 외부 연동 본문이 약 4.5MB에 달하는 트래픽이 들어오면서, 본문을 그대로 DB의 VARCHAR 컬럼에 INSERT하던 공통 로그 테이블이 한계(65,534바이트)를 초과해 정작 진짜 장애 로그가 기록되지 못한 사례가 있었습니다. 이런 일을 막으려면 다음 중 한 가지 전략을 선택해야 합니다.
첫째, 본문은 별도 파일 시스템(일자별 로그 파일, S3 등)에 저장하고 DB에는 경로만 남깁니다. 둘째, DB에 남기더라도 CLOB 같은 LOB 자료형을 사용합니다. 셋째, 임계치 초과 시 자동 절단(truncate) 후 “[truncated]” 표시를 남깁니다. 어떤 방식이든, 로그를 남기다가 로그 자체가 장애의 원인이 되는 일은 반드시 피해야 합니다.
원칙 2: 동시 개발이라면 Swagger Mock부터 합의한다
이미 운영 중인 외부 API와 연동할 때는 명세가 고정되어 있어 선택지가 단순합니다. 문제는 양측이 동시에 신규 API를 개발하는 경우입니다. 이 상황에서 가장 흔하게 발생하는 풍경은 다음과 같습니다.
외부 측 개발이 일주일 늦어집니다. 이쪽에서는 호출할 대상이 없으니 단위 테스트조차 돌리기 어렵습니다. 결국 하드코딩된 더미 응답으로 막아두고 통합 테스트는 막판으로 미룹니다. 막판에 외부 측 응답이 도착하면, 한쪽이 카멜케이스로 보내고 한쪽이 스네이크케이스로 받는 것 같은 어긋남이 그제서야 발견됩니다.
이 일정 리스크를 가장 깔끔하게 해소하는 방법이 Swagger(OpenAPI) 기반 Mock 우선 개발입니다.
Swagger Mock 우선 개발의 흐름
흐름은 다음과 같습니다.
먼저, 양측이 함께 OpenAPI 명세를 작성합니다. 요청 스키마, 응답 스키마, 에러 코드, 헤더, 인증 방식까지 합의한 뒤 YAML/JSON 명세 파일로 고정합니다. 이 명세 자체가 양측의 계약(Contract)이 됩니다.
다음으로, 외부에 노출되는 엔드포인트를 미리 만들어 두고 고정된 Mock 응답을 돌려줍니다. 이쪽 API가 실제로 비즈니스 로직을 구현하기 전이라도, 외부 측은 미리 정의된 응답을 받아 자기 쪽 코드를 개발하고 테스트할 수 있습니다. 반대로 이쪽도 외부 측이 제공한 Mock 엔드포인트로 호출 코드를 안전하게 검증할 수 있습니다.
마지막으로, 실제 비즈니스 로직 구현이 끝나면 Mock 응답을 실제 로직으로 점진적으로 교체합니다. 명세는 그대로 유지되므로 호출 측 코드는 손댈 필요가 없습니다.
Spring Boot에서 Mock 응답 노출하는 방법
springdoc-openapi를 사용하면 어노테이션만으로 명세와 Mock 예시를 함께 정의할 수 있습니다.
@RestController
@RequestMapping("/api/v1/orders")
public class OrderApiController {
@Operation(summary = "주문 등록", description = "외부 연동용 주문 등록 API")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공",
content = @Content(examples = @ExampleObject(value = """
{
"result_code": "0000",
"result_msg": "success",
"order_id": "ORD-20260517-0001"
}
"""))),
@ApiResponse(responseCode = "400", description = "필수값 누락",
content = @Content(examples = @ExampleObject(value = """
{
"result_code": "4001",
"result_msg": "customer_name is required"
}
""")))
})
@PostMapping
public OrderResponse register(@RequestBody @Valid OrderRequest request) {
// 1단계: 합의된 고정 Mock 응답을 반환
// 2단계: 실제 비즈니스 로직 구현 후 점진적 교체
return OrderResponse.mocked();
}
}
이렇게 해두면 외부 측은 Swagger UI에서 실제 요청/응답 예시를 그대로 보고, 호출 테스트까지 진행할 수 있습니다. 명세서를 별도 문서로 주고받으며 동기화하느라 낭비되는 시간이 사라집니다.
Mock 우선 개발의 효과
| 비교 항목 | 일반 개발 | Mock 우선 개발 |
|---|---|---|
| 통합 테스트 시점 | 양측 구현 완료 후 | 명세 합의 직후부터 |
| 일정 의존성 | 느린 쪽에 양쪽이 묶임 | 양측 병렬 진행 |
| 규약 어긋남 발견 시점 | 막판 통합 테스트 | 명세 작성 단계 |
| 외부 측 만족도 | 막판 변경 빈번 | 예측 가능성 확보 |
명세 합의에 들이는 며칠이 통합 단계의 일주일을 아껴 줍니다. 결과적으로 양측 모두 win-win인 구조가 됩니다.
두 원칙이 함께 작동할 때의 시너지
로그와 Mock은 별개의 원칙처럼 보이지만, 운영에 들어가면 한 쌍으로 작동합니다.
장애 신고가 들어왔을 때 추적 흐름을 상상해 봅니다. 첫 번째로 traceId로 해당 요청의 전체 로그를 묶어 봅니다. 두 번째로 요청 본문을 펼쳐 외부 측 주장과 실제 도착 값이 같은지 확인합니다. 세 번째로 그 본문을 Swagger 명세와 비교해 어느 필드가 어긋났는지 즉시 판별합니다.
명세, 로그, traceId가 모두 갖춰져 있다면 이 추적은 보통 수 분 안에 결론이 납니다. 반대로 어느 하나라도 빠지면 같은 작업이 수 시간으로 늘어납니다.
외부 연동 API 설계 운영 체크리스트
다음 항목을 신규 외부 연동 API를 오픈하기 전에 한 번씩 점검하시기 바랍니다.
첫째, 요청과 응답이 모두 로그로 남는지 확인합니다. 응답만 남기는 경우가 의외로 많습니다.
둘째, traceId가 발급되어 한 요청의 모든 로그를 묶을 수 있는지 확인합니다.
셋째, 민감정보 마스킹 규칙이 정의되어 있고 정규식이 검증되어 있는지 확인합니다.
넷째, 로그 저장소의 크기 한계를 알고 있고, 한계 초과 시 동작이 정의되어 있는지 확인합니다.
다섯째, 신규 API라면 Swagger 명세를 외부 측과 함께 합의했는지 확인합니다.
여섯째, 명세에 성공 응답뿐 아니라 주요 실패 응답 예시가 포함되어 있는지 확인합니다.
일곱째, DTO 기반 파라미터를 사용하고 있는지 확인합니다. Map 기반 파라미터는 키 규약 변경에 매우 취약합니다.
자주 묻는 질문 (FAQ)
Q1. 모든 요청을 로그로 남기면 디스크가 너무 빠르게 차지 않나요?
대량 트래픽 API라면 정당한 우려입니다. 다음 전략을 조합해 해결합니다. 일정 기간(예: 30일) 이후 자동 압축·삭제 정책을 두고, 정상 응답은 요약만 남기고 실패 응답만 본문 전체를 남기며, 본문은 별도 파일 시스템에 저장하고 DB에는 경로만 남깁니다.
Q2. 외부 측이 Swagger 명세 합의에 협조하지 않으면 어떻게 하나요?
이쪽에서 먼저 OpenAPI YAML을 작성해 보내는 것이 가장 빠른 방법입니다. 문서 형식이 명확하면 상대도 반대 의견을 내기 쉽고, 합의 과정 자체가 짧아집니다. Swagger UI 링크 한 줄을 공유하는 것이 워드 문서 10페이지보다 효과적입니다.
Q3. Mock 응답과 실제 응답이 달라지면 의미가 없지 않나요?
그래서 계약 테스트(Contract Test) 를 함께 운영합니다. springdoc이 생성하는 OpenAPI 스펙을 기준으로 응답이 스키마를 따르는지 자동 검증하는 테스트를 작성하면, 실제 응답이 명세에서 벗어나는 순간 CI가 깨집니다.
Q4. 로그가 너무 많아서 검색이 어렵습니다. 어떻게 정리하나요?
ELK Stack(Elasticsearch + Logstash + Kibana) 또는 Loki 같은 로그 검색 시스템을 도입하시기 바랍니다. traceId를 키로 검색하면 한 요청의 전 구간을 한 번에 추적할 수 있고, 응답 코드별·시간대별 통계도 즉시 확인 가능합니다.
Q5. 이미 운영 중인 레거시 외부 API에 로그를 추가하면 성능에 영향이 가지 않나요?
비동기 로깅(Logback의 AsyncAppender)을 사용하면 본문 로깅으로 인한 성능 영향은 대부분 무시할 수 있는 수준입니다. 단, 본문 크기가 수 MB 단위라면 직렬화 비용이 누적되므로, 크기 임계치 기반 절단 정책을 함께 적용하시기 바랍니다.
마무리
Spring Boot 외부 연동 API 설계의 본질은 화려한 기술이 아니라 추적 가능성과 계약 명확성입니다. 들어온 값과 나간 값을 빠짐없이 기록해 두면, 장애가 발생했을 때 책임 소재와 원인을 빠르게 분리할 수 있습니다. 동시 개발 상황에서 Swagger 기반 Mock을 먼저 합의하면, 양측 일정이 서로의 진척에 묶이지 않고 병렬로 진행됩니다.
이 두 원칙은 거창한 아키텍처가 필요하지 않습니다. 필터 한 개와 어노테이션 몇 줄로 시작할 수 있고, 그 효과는 운영에 들어간 첫 주부터 체감됩니다. 외부 연동 API를 새로 설계하거나 레거시 연동을 개선할 계획이 있다면, 이 두 가지를 첫 번째 작업 항목으로 두시기 바랍니다.
핵심 요약
- 외부 연동 API의 모든 Request/Response는 traceId와 함께 무조건 로그로 남깁니다.
- 민감정보는 마스킹하고, 로그 저장소의 크기 한계를 사전에 검증합니다.
- 동시 개발 상황에서는 Swagger(OpenAPI) 명세와 Mock 응답을 먼저 합의합니다.
- Mock 우선 개발로 양측이 병렬 개발과 통합 테스트를 동시에 진행할 수 있습니다.
- 명세·로그·traceId가 갖춰지면 장애 추적 시간이 수 시간에서 수 분으로 줄어듭니다.
“Spring Boot 외부 연동 API 설계 필수 원칙”에 대한 2개의 생각