"제휴사 개발 서버가 또 다운됐어요.", "이 케이스는 테스트 데이터가 없어서 QA를 못하고 있어요.", "테스트 중인데 유효한 응답이 안 와요."
외부 기관, 특히 금융사나 파트너사와 연동하여 서비스를 개발하는 백엔드 개발자라면 누구나 한 번쯤 겪어봤을 법한 상황입니다. 다양한 제휴사의 상품을 중개하고 연동하여 고객에게 더 나은 가치를 제공하는 서비스에서, 이러한 외부 시스템의 불안정성은 개발 및 QA 효율을 저하시키고, 심지어 서비스 출시 일정에까지 영향을 미치는 골칫거리가 되곤 합니다.
이번 글에서는 신규 제휴사 연동 과정에서 반복적으로 발생하는 테스트의 어려움을 해결하기 위해, 사용자, 제휴사, 기능 단위로 정밀하게 동작을 제어할 수 있는 Mock 서버를 어떻게 설계하고 구현했는지 그 경험을 공유하고자 합니다.
신규 제휴사 연동 프로젝트를 진행하면서 다음과 같은 문제들이 반복적으로 발생했습니다.
이러한 문제들을 해결하기 위해, 우리는 외부 제휴사 시스템에 의존하지 않고도 다양한 시나리오를 안정적으로 테스트할 수 있는 자체 Mock 서버를 구축하기로 결정했습니다.
용어 정리:
- 제휴사: 은행, 저축은행, 보험사 등 외부 상품 및 서비스를 제공하는 파트너 기관
- 가심사 조회: 고객 정보를 바탕으로 대출 금리, 한도 등 상품 가입 가능 여부를 미리 확인하는 단계
- 대출 신청/실행: 가심사 결과를 바탕으로 실제 대출을 신청하고, 제휴사가 최종 심사 후 자금을 집행하는 단계
- 퍼널(Funnel): 사용자가 특정 목표(예: 대출 신청 완료)에 도달하기까지 거치는 일련의 과정 (예: 상품 목록 조회 → 가심사 → 대출 신청)
단순히 고정된 응답을 반환하는 것을 넘어, 실제 운영 환경과 유사한 다양한 상황을 시뮬레이션할 수 있는 정교한 Mock 서버를 목표로 다음과 같은 요구사항을 정의했습니다.
기능적 요구사항:
비기능적 요구사항:
요구사항을 바탕으로, Mock 서버를 다음과 같은 주요 구성요소로 나누어 설계하고 구현했습니다.
Mock 서버의 동작을 제어하기 위한 정보(예: 테스트할 사용자 ID, 특정 시나리오를 트리거하기 위한 값 등)를 비즈니스 로직에 영향을 주지 않고 전달하기 위해, 커스텀 헤더를 사용하는 방식을 채택했습니다.
이 헤더 주입 로직은 테스트 환경에서만 동작하도록 @Profile("dev")
또는 유사한 설정을 통해 관리하며, 실제 제휴사와 통신하는 HTTP 클라이언트(RestTemplate, WebClient 등) 레벨에서 적용됩니다.
ClientHttpRequestInterceptor
를 구현하여, 모든 요청이 보내지기 전에 원하는 커스텀 헤더를 동적으로 추가합니다.class MockServerHeaderInjectionInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(
HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// ThreadLocal 등에서 현재 테스트 사용자 정보를 가져와 헤더에 주입
String currentUserId = MockUserContext.getUserId();
if (currentUserId != null) {
request.getHeaders().add("X-MOCK-USER-ID", currentUserId);
}
return execution.execute(request, body);
}
}
ExchangeFilterFunction
을 활용하여 요청을 보내기 전에 헤더를 추가하는 필터를 등록합니다.private ExchangeFilterFunction addMockServerHeaderFilter() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
// ThreadLocal 등에서 현재 테스트 사용자 정보를 가져옴
String currentUserId = MockUserContext.getUserId();
if (currentUserId == null) {
return Mono.just(clientRequest);
}
ClientRequest newRequest = ClientRequest.from(clientRequest)
.headers(headers -> headers.add("X-MOCK-USER-ID", currentUserId))
.build();
return Mono.just(newRequest);
});
}
Mock 서버의 모든 설정값은 코드에 하드코딩하지 않고, 데이터베이스 테이블에서 관리하여 런타임에 동적으로 변경할 수 있도록 설계했습니다. 이를 통해 재배포 없이도 테스트 시나리오를 유연하게 변경할 수 있습니다.
partner_metadata
: 제휴사별 메타데이터 (응답 동기/비동기 여부, 네트워크 설정, 암호화 정보 등)partner_response
: 제휴사 및 퍼널별 Mock 응답 데이터 템플릿 (JSON 레이아웃, 예약어 등)user_status
: 사용자별 Mock 설정 (특정 사용자에 대해 Mock 응답 사용 여부, 응답 지연 시간, 사용할 응답 템플릿 지정 등)이 설정 정보들을 조합하여, "A 사용자가 B 제휴사의 C 퍼널을 요청하면, 5초 후에 D 응답 템플릿을 사용해 응답하라"와 같은 정밀한 제어가 가능해집니다.
실제 금융 서비스에서는 가심사 요청 후, 제휴사가 내부 심사를 거쳐 몇 초 또는 몇 분 뒤에 콜백(Callback) API를 통해 결과를 알려주는 비동기 방식이 많습니다. 이러한 현실적인 시나리오를 테스트하기 위해 예약 기능을 구현했습니다.
user_status
나 partner_metadata
테이블의 설정값을 따릅니다.@Scheduled
어노테이션을 사용하여 주기적인 스케줄러(예: 1분마다 실행)를 실행합니다.ThreadPoolTaskScheduler
와 같은 비동기 태스크 실행기를 통해 실제 콜백 API 요청을 비동기적으로 전송합니다.🤔 꼬리 질문:
@Scheduled
를 사용한 주기적인 폴링(Polling) 방식 외에, 예약된 작업을 더 효율적으로 처리할 수 있는 다른 방법에는 어떤 것들이 있을까요? (예:DelayQueue
, 스케줄링 라이브러리 - Quartz, 외부 메시지 큐의 지연 메시지 기능 등) 각 방식의 장단점은 무엇일까요?
Mock 응답은 실제 제휴사 응답과 동일한 포맷을 가져야 하며, 상품명, 금리, 한도 등의 값은 테스트 시나리오에 맞게 동적으로 생성되어야 합니다. 이를 위해 예약어(Placeholder) 기반 템플릿 치환 방식을 사용했습니다.
{{amount}}
, {{productId}}
와 같은 예약어를 포함한 JSON 템플릿으로 미리 partner_response
테이블에 저장합니다.{
"amount": "{{amount}}",
"period": "{{period}}",
"interestRate": "{{interestRate}}",
"productId": "{{productId}}",
"id": "{{id}}"
}
123456789
)를 매핑하는 로직을 추가하여, 최종적으로 실제 시스템과 정합성이 맞는 Mock 데이터를 생성합니다.Mock 서버는 다음과 같은 두 가지 상황에서 실제 제휴사 서버로 요청을 그대로 전달하는 릴레이 서버 역할을 수행해야 합니다.
개발자나 QA 담당자가 DB에 직접 접속하여 설정을 변경하는 것은 번거롭고 위험할 수 있습니다. 우리는 팀 내에서 사용하는 메신저봇(예: Slack Bot)에 Mock 서버 설정 및 확인 기능을 연동하여 사용 편의성을 높였습니다.
이 Mock 서버가 단일 서비스에만 종속되지 않고, 회사 내 다른 제휴사 연동 서비스에서도 손쉽게 활용될 수 있도록 모듈화된 아키텍처를 채택했습니다.
mock-server-api
: 각 서비스로부터 요청을 수신하는 공통 API 모듈mock-server-base
: 콜백, 치환 기능 등 모든 서비스에서 공통으로 사용하는 기반 모듈mock-server-serviceA
: A 서비스의 특화된 비즈니스 로직을 담은 모듈mock-server-serviceB
: B 서비스의 특화된 비즈니스 로직을 담은 모듈application-{service}.properties
파일에 정의하고, mock-server-base
의 메인 application.properties
에서 이들을 spring.config.import
옵션을 통해 통합 로딩하도록 구성하여, 설정의 분리와 통합 관리를 동시에 달성했습니다.새로운 Mock 서버 시스템을 도입함으로써, 우리는 외부 시스템의 불안정성으로부터 완전히 독립된, 안정적이고 예측 가능한 테스트 환경을 구축할 수 있었습니다.
외부 기관과의 연동이 많은 서비스를 개발하는 조직이라면 저희와 유사한 어려움을 겪고 있을 것이라 생각합니다. Mock 서버와 릴레이 기능을 결합한 정교한 테스트 환경을 구축함으로써, 개발 및 테스트 단계의 효율성과 신뢰성을 모두 확보할 수 있었습니다. 이 글이 유사한 문제를 고민하고 있는 개발자분들께 실질적인 아이디어와 도움이 되었으면 좋겠습니다. 감사합니다.