제휴사 테스트 서버가 또 말썽인가요? 정밀 제어 가능한 Mock 서버 개발기

이동휘·2025년 6월 16일
0

매일매일 블로그

목록 보기
29/49

"제휴사 개발 서버가 또 다운됐어요.", "이 케이스는 테스트 데이터가 없어서 QA를 못하고 있어요.", "테스트 중인데 유효한 응답이 안 와요."

외부 기관, 특히 금융사나 파트너사와 연동하여 서비스를 개발하는 백엔드 개발자라면 누구나 한 번쯤 겪어봤을 법한 상황입니다. 다양한 제휴사의 상품을 중개하고 연동하여 고객에게 더 나은 가치를 제공하는 서비스에서, 이러한 외부 시스템의 불안정성은 개발 및 QA 효율을 저하시키고, 심지어 서비스 출시 일정에까지 영향을 미치는 골칫거리가 되곤 합니다.

이번 글에서는 신규 제휴사 연동 과정에서 반복적으로 발생하는 테스트의 어려움을 해결하기 위해, 사용자, 제휴사, 기능 단위로 정밀하게 동작을 제어할 수 있는 Mock 서버를 어떻게 설계하고 구현했는지 그 경험을 공유하고자 합니다.


1. 배경: 왜 Mock 서버가 필요했을까?

신규 제휴사 연동 프로젝트를 진행하면서 다음과 같은 문제들이 반복적으로 발생했습니다.

  • 제휴사 개발 서버의 잦은 장애 및 불안정한 응답: 테스트를 진행해야 할 시간에 서버가 다운되거나, 예상치 못한 응답을 반환하여 테스트가 중단되는 경우가 많았습니다.
  • 특정 케이스 테스트의 어려움: 특정 사용자 조건(예: 신용 등급, 기존 대출 유무)이나 특정 기능 플로우(예: 대출 거절, 한도 초과)에 맞는 테스트 데이터를 확보하기가 매우 어려웠습니다.
  • 리드타임 증가 및 운영 피로도: 위와 같은 문제들로 인해 QA 및 신규 기능 개발 효율이 떨어지고, 전체적인 프로젝트 일정이 지연되었습니다.

이러한 문제들을 해결하기 위해, 우리는 외부 제휴사 시스템에 의존하지 않고도 다양한 시나리오를 안정적으로 테스트할 수 있는 자체 Mock 서버를 구축하기로 결정했습니다.

용어 정리:

  • 제휴사: 은행, 저축은행, 보험사 등 외부 상품 및 서비스를 제공하는 파트너 기관
  • 가심사 조회: 고객 정보를 바탕으로 대출 금리, 한도 등 상품 가입 가능 여부를 미리 확인하는 단계
  • 대출 신청/실행: 가심사 결과를 바탕으로 실제 대출을 신청하고, 제휴사가 최종 심사 후 자금을 집행하는 단계
  • 퍼널(Funnel): 사용자가 특정 목표(예: 대출 신청 완료)에 도달하기까지 거치는 일련의 과정 (예: 상품 목록 조회 → 가심사 → 대출 신청)

2. 우리가 원했던 Mock 서버: 요구사항 정의

단순히 고정된 응답을 반환하는 것을 넘어, 실제 운영 환경과 유사한 다양한 상황을 시뮬레이션할 수 있는 정교한 Mock 서버를 목표로 다음과 같은 요구사항을 정의했습니다.

기능적 요구사항:

  • 정밀한 제어: 특정 사용자, 제휴사, 퍼널(기능) 단위로 Mock 서버 사용 여부를 동적으로 설정할 수 있어야 한다.
  • 다양한 응답 방식 지원: 동기(Synchronous) 응답뿐만 아니라, 실제 제휴사처럼 일정 시간 지연 후 응답을 주는 비동기(Asynchronous) 콜백 방식도 지원해야 한다.
  • 릴레이(Relay) 서버 역할: Mock 설정을 사용하지 않는 요청은 실제 제휴사 서버로 그대로 전달(Relay)하는 기능을 수행해야 한다.
  • 쉬운 설정: 개발자나 QA 담당자가 복잡한 코드 수정 없이 쉽게 설정을 변경하고 테스트를 진행할 수 있어야 한다.

비기능적 요구사항:

  • 기존 코드 변경 최소화: Mock 서버 도입으로 인해 기존 비즈니스 로직 코드를 수정하는 일을 최소화해야 한다.
  • 커스텀 헤더 활용: Mock 서버 제어에 필요한 정보(예: 테스트 대상 사용자 ID)는 커스텀 헤더를 통해 전달하여 비즈니스 로직과의 격리를 유지한다.
  • 유연한 확장 구조: 향후 다른 서비스에서도 이 Mock 서버를 손쉽게 연동하고 확장할 수 있는 유연한 구조를 제공한다.

3. 설계 및 구현: 정교한 Mock 서버 만들기

요구사항을 바탕으로, Mock 서버를 다음과 같은 주요 구성요소로 나누어 설계하고 구현했습니다.

1. 커스텀 헤더 주입: 비즈니스 로직과의 격리

Mock 서버의 동작을 제어하기 위한 정보(예: 테스트할 사용자 ID, 특정 시나리오를 트리거하기 위한 값 등)를 비즈니스 로직에 영향을 주지 않고 전달하기 위해, 커스텀 헤더를 사용하는 방식을 채택했습니다.

이 헤더 주입 로직은 테스트 환경에서만 동작하도록 @Profile("dev") 또는 유사한 설정을 통해 관리하며, 실제 제휴사와 통신하는 HTTP 클라이언트(RestTemplate, WebClient 등) 레벨에서 적용됩니다.

  • RestTemplate의 경우: 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);
        }
    }
  • WebClient의 경우: 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);
        });
    }

2. 유연한 설정 관리: 모든 것은 DB 테이블에!

Mock 서버의 모든 설정값은 코드에 하드코딩하지 않고, 데이터베이스 테이블에서 관리하여 런타임에 동적으로 변경할 수 있도록 설계했습니다. 이를 통해 재배포 없이도 테스트 시나리오를 유연하게 변경할 수 있습니다.

  • partner_metadata: 제휴사별 메타데이터 (응답 동기/비동기 여부, 네트워크 설정, 암호화 정보 등)
  • partner_response: 제휴사 및 퍼널별 Mock 응답 데이터 템플릿 (JSON 레이아웃, 예약어 등)
  • user_status: 사용자별 Mock 설정 (특정 사용자에 대해 Mock 응답 사용 여부, 응답 지연 시간, 사용할 응답 템플릿 지정 등)

이 설정 정보들을 조합하여, "A 사용자가 B 제휴사의 C 퍼널을 요청하면, 5초 후에 D 응답 템플릿을 사용해 응답하라"와 같은 정밀한 제어가 가능해집니다.

3. 비동기 응답 시뮬레이션: 예약(Reservation) 기능

실제 금융 서비스에서는 가심사 요청 후, 제휴사가 내부 심사를 거쳐 몇 초 또는 몇 분 뒤에 콜백(Callback) API를 통해 결과를 알려주는 비동기 방식이 많습니다. 이러한 현실적인 시나리오를 테스트하기 위해 예약 기능을 구현했습니다.

  • 1단계: 예약 등록
    • 가심사 조회나 대출 신청과 같은 초기 API가 호출될 때, Mock 서버는 비동기 콜백 응답을 위한 예약 정보를 DB에 저장합니다.
    • 예약 정보에는 "누구에게", "어떤 내용의 콜백을", "몇 초 후에 보낼 것인가" 등의 정보가 포함됩니다. 지연 시간은 user_statuspartner_metadata 테이블의 설정값을 따릅니다.
  • 2단계: 예약 실행
    • Mock 서버 내에서 Spring의 @Scheduled 어노테이션을 사용하여 주기적인 스케줄러(예: 1분마다 실행)를 실행합니다.
    • 스케줄러는 예약 테이블에서 현재 시간 기준으로 실행 시간이 지난 예약 건들을 조회합니다.
    • 조회된 예약 건들은 ThreadPoolTaskScheduler와 같은 비동기 태스크 실행기를 통해 실제 콜백 API 요청을 비동기적으로 전송합니다.

🤔 꼬리 질문: @Scheduled를 사용한 주기적인 폴링(Polling) 방식 외에, 예약된 작업을 더 효율적으로 처리할 수 있는 다른 방법에는 어떤 것들이 있을까요? (예: DelayQueue, 스케줄링 라이브러리 - Quartz, 외부 메시지 큐의 지연 메시지 기능 등) 각 방식의 장단점은 무엇일까요?

4. 현실적인 Mock 데이터 생성: 예약어와 템플릿 기반 치환

Mock 응답은 실제 제휴사 응답과 동일한 포맷을 가져야 하며, 상품명, 금리, 한도 등의 값은 테스트 시나리오에 맞게 동적으로 생성되어야 합니다. 이를 위해 예약어(Placeholder) 기반 템플릿 치환 방식을 사용했습니다.

  1. Mock 템플릿 정의: 각 제휴사/퍼널별 응답 포맷에 따라, 동적으로 변경될 값을 {{amount}}, {{productId}}와 같은 예약어를 포함한 JSON 템플릿으로 미리 partner_response 테이블에 저장합니다.
    {
      "amount": "{{amount}}",
      "period": "{{period}}",
      "interestRate": "{{interestRate}}",
      "productId": "{{productId}}",
      "id": "{{id}}"
    }
  2. 예약어 치환: 응답 생성 시, JSON 객체(JsonNode)를 재귀적으로 순회하며 템플릿 내의 예약어들을 실제 테스트 데이터 값으로 치환합니다.
  3. ID 매핑 처리: "OOO 자동차 담보 대출"과 같은 상품명과 실제 내부 시스템에서 사용하는 고유 ID(예: 123456789)를 매핑하는 로직을 추가하여, 최종적으로 실제 시스템과 정합성이 맞는 Mock 데이터를 생성합니다.

5. 릴레이(Relay) 서버 역할 수행

Mock 서버는 다음과 같은 두 가지 상황에서 실제 제휴사 서버로 요청을 그대로 전달하는 릴레이 서버 역할을 수행해야 합니다.

  1. 사용자가 Mock 설정을 사용하지 않는 경우: 요청 헤더나 DB 설정을 확인하여 특정 사용자가 Mock 대상이 아니라고 판단되면, 받은 요청을 그대로 실제 제휴사 API로 전달하고 그 응답을 다시 클라이언트에게 반환합니다.
  2. 제휴사가 콜백 API를 호출하는 경우: 제휴사가 우리 시스템의 콜백 엔드포인트로 비동기 응답을 보낼 때, API 게이트웨이의 라우팅 규칙을 테스트 환경에서는 Mock 서버로 향하도록 설정합니다. Mock 서버는 이 콜백 요청을 수신한 후, 다시 실제 비즈니스 로직을 처리하는 내부 애플리케이션 서버로 요청을 전달합니다.

6. 쉬운 사용을 위한 메신저봇 연동

개발자나 QA 담당자가 DB에 직접 접속하여 설정을 변경하는 것은 번거롭고 위험할 수 있습니다. 우리는 팀 내에서 사용하는 메신저봇(예: Slack Bot)에 Mock 서버 설정 및 확인 기능을 연동하여 사용 편의성을 높였습니다.

  • 현재 설정 확인: 버튼 클릭 한 번으로 현재 어떤 제휴사의 어떤 퍼널이 Mock 데이터를 사용하고 있는지 쉽게 확인할 수 있습니다.
  • 설정 변경: 특정 제휴사의 특정 퍼널에 대해 어떤 Mock 데이터를 사용할지 버튼이나 명령어를 통해 간편하게 설정할 수 있습니다.

7. 유연한 확장 구조 제공: 다른 서비스에서도 쉽게 연동하기

이 Mock 서버가 단일 서비스에만 종속되지 않고, 회사 내 다른 제휴사 연동 서비스에서도 손쉽게 활용될 수 있도록 모듈화된 아키텍처를 채택했습니다.

  • 모듈 구조:
    • mock-server-api: 각 서비스로부터 요청을 수신하는 공통 API 모듈
    • mock-server-base: 콜백, 치환 기능 등 모든 서비스에서 공통으로 사용하는 기반 모듈
    • mock-server-serviceA: A 서비스의 특화된 비즈니스 로직을 담은 모듈
    • mock-server-serviceB: B 서비스의 특화된 비즈니스 로직을 담은 모듈
  • Multi-DataSource 지원: 각 서비스 모듈이 별도의 데이터 소스(DataSource)를 사용해야 하는 경우를 대비하여, Spring 환경에서 Multi-DataSource를 지원하도록 구성했습니다. 이때, 패키지 구조나 어노테이션 기반 설정의 한계를 극복하기 위해, 각 데이터 소스 설정을 명시적인 Java 클래스 기반(@Configuration)으로 정의하는 방식을 채택하여 유연성과 명확성을 확보했습니다.
  • 프로퍼티 설정 전략: 각 모듈에 필요한 설정은 모듈 내부의 application-{service}.properties 파일에 정의하고, mock-server-base의 메인 application.properties에서 이들을 spring.config.import 옵션을 통해 통합 로딩하도록 구성하여, 설정의 분리와 통합 관리를 동시에 달성했습니다.

4. 개선 결과와 성과: 테스트 효율성과 신뢰성을 모두 잡다!

새로운 Mock 서버 시스템을 도입함으로써, 우리는 외부 시스템의 불안정성으로부터 완전히 독립된, 안정적이고 예측 가능한 테스트 환경을 구축할 수 있었습니다.

  • 리드타임 단축: 외부 플랫폼의 크롤링 주기에 의존하던 기존 방식과 달리, 필요한 시점에 즉시 테스트가 가능해져 QA 및 개발 리드타임을 획기적으로 단축했습니다.
  • 테스트 커버리지 확대: 실제 환경에서 재현하기 어려웠던 다양한 예외 케이스(거절, 한도 초과, 특정 에러 응답 등)를 손쉽게 시뮬레이션할 수 있게 되어, 서비스의 안정성과 신뢰도를 높일 수 있었습니다.
  • 개발 및 운영 효율성 증대: 개발자와 QA 담당자가 직접 필요한 테스트 시나리오를 손쉽게 구성하고 실행할 수 있게 되어, 커뮤니케이션 비용과 대기 시간을 크게 줄였습니다.

마치며

외부 기관과의 연동이 많은 서비스를 개발하는 조직이라면 저희와 유사한 어려움을 겪고 있을 것이라 생각합니다. Mock 서버와 릴레이 기능을 결합한 정교한 테스트 환경을 구축함으로써, 개발 및 테스트 단계의 효율성과 신뢰성을 모두 확보할 수 있었습니다. 이 글이 유사한 문제를 고민하고 있는 개발자분들께 실질적인 아이디어와 도움이 되었으면 좋겠습니다. 감사합니다.

0개의 댓글