데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      실제처럼, 빠르게, 안정적으로: 에이닷 통합 테스트 환경 구축기 (WireMock + Docker + GitLab CI)

      지윤 25.08.26
      1,526 5 1
      DEVOTEE 요약
      본 블로그는 외부 API 연동으로 인해 발생하는 통합 테스트 실패 문제를 해결하기 위해 WireMock, Docker, GitLab CI를 활용해 독립적이고 신뢰할 수 있는 테스트 환경을 구축한 과정을 설명합니다. 기존에는 외부 서버 상태나 스펙 변경으로 인해 테스트 실패가 잦았지만, WireMock을 도입하여 외부 API를 Mocking하고, Docker로 통합 테스트 환경을 컨테이너화하여 GitLab CI에서 자동화 테스트를 실행하도록 설정했습니다. 이를 통해 테스트에 대한 신뢰감을 높이고, 반복 작업을 줄이며, 보다 안정적이고 생산적인 개발 환경을 구축하는 데 성공했습니다.
      DEVOTEE 추천 블로그

      빈번하게 실패하는 통합 테스트의 고통

      우리 팀에서는 Prompt Engineering Platform을 만들고 있습니다.

      Prompt Engineering Platform은 다양한 외부 Model Provider와 연동을 하고 있는데, 처음에는 Provider 서버를 직접 연동해서 테스트하였습니다.하지만, 이건 녹록지 않더라고요. 실제 연동하는 서버가 동작하지 않기도 했고, 어떤 날은 Azure 쪽에서 이상하게 타임아웃이 나거나 버전 업데이트 때문에 스펙이 바뀌어버리기도 했습니다.🤯

      그래서 서버 내 Mock API를 만들어 연동하기도 하였지만, 이것 또한 번거로웠습니다. API에 변경 사항이 있을 때마다 Mock API도 함께 변경해 주며 꾸준히 관리해 주어야 했거든요.


      외부 서버의 상태에 따라서 테스트는 종종 실패했고, 실패 원인이 우리 코드 때문인지 외부 서비스 때문인지 알 수가 없었습니다. 이런 상황이 반복되다 보니, 테스트는 신뢰할 수 없는 존재가 되어버렸죠. “테스트가 실패하는데! 이게 내 코드 문제일까, 아니면 또 외부 서버의 문제인 걸까?” 하는 불필요한 검증에 시간을 낭비하는 경우가 많았습니다.


      그러던 중 팀원분이 WireMock을 소개해 주셨고, 이를 적용해 빠르고, 독립적이며, 반복 가능한 FIRST Unit Test에 한 발짝 다가갈 결심을 하게 됩니다.

      FIRST 테스트를 위하여 선택한 도구는 바로 WireMock, Docker, 그리고 GitLab CI입니다. 외부 API를 Mock하면서 독립적으로 테스트할 수 있는 환경을 만들었고, 이를 자동화된 파이프라인 안에 적용하였습니다.


      이 글에서는 WireMock과 Docker, GitLab CI를 어떤 방식으로 활용했는지에 대해 차근차근 이야기하려 합니다.

      안 그래도 바쁜데, 불필요한 테스트 실패로 시간을 빼앗기고 있는 개발자분들께 이 글이 작은 도움이 되길 바랍니다 🙏


      테스트 환경 아키텍처 개요

      As-Is

      앞서 언급했듯, 기존에는 외부 서버를 실제로 연동했기 때문에 에러 응답 테스트가 까다로웠고, 외부 서버에 이슈가 발생하면 테스트도 실패하는 문제가 있었습니다.

      또한, 테스트 시 DB와 Redis를 직접 로컬에서 실행하는 것도 번거로웠습니다.

      아래는 기존의 전체 아키텍처 구성도입니다.

      image.png



      To-Be

      외부 서버 연동을 WireMock으로 대체하고, WireMock, DB, Redis를 모두 Docker로 실행하여 로컬뿐 아니라 GitLab CI 환경에서도 통합 테스트를 수행할 수 있도록 구성하였습니다.

      이에 대한 자세한 내용은 아래에서 상세히 다루겠습니다.

      image.png



      결과적으로 프로젝트의 전체 소스 구조는 아래와 같으며, 여기서 Docker + WireMock + GitLab CI 구성을 한눈에 파악할 수 있습니다.

      docker 폴더에는 컨테이너로 실행할 WireMock 관련 파일이 위치하며, 프로젝트 루트에는 GitLab CI 설정을 위한 .gitlab-ci.yml파일이 있습니다.

      📁 project-root/
      ├──📁 docker/
      │   ├──📁 wiremock/
      │   │   ├──📁 __files/
      │   │   ├──📁 mappings/
      │   │   └──🐳 Dockerfile
      │   └──🐳 docker-compose.yml
      ├──📁 src/
      └──🦊 .gitlab-ci.yml

      또한, 위에 표기하진 않았지만, 통합 테스트를 위해 MySQL과 Redis도 WireMock과 함께 Docker로 실행하고 있습니다.

      이와 같이 테스트에 필요한 서비스들을 자유롭게 추가하면 됩니다. 😊

      WireMock으로 API Stub 만들기

      WireMock이란?

      API mock testing을 위한 오픈소스 툴로, 안정적인 테스트 및 개발 환경을 만들고, 불안정한 외부(3rd party) 서비스와 독립적으로 테스트할 수 있으며, 아직 구현되지 않은 API를 시뮬레이션하는 데 도움을 줍니다.

      WireMock을 이용해 외부 서버의 API를 어떻게 mocking할 수 있는지 살펴보겠습니다.

      특정 요청이 들어왔을 때 미리 정해둔 응답을 반환하는 "stub"을 mappings 디렉터리 내에 구성하여 줍니다.

      그리고 아래와 같이 Intellij에서 WireMock을 간단하게 실행해볼 수 있습니다.

      Docker로 실행하는 방법은 바로 다음 챕터인 "Docker 컨테이너에서 WireMock 구동하기"에서 다루겠습니다.

      WireMock을 사용하면, 외부 서버와 직접 연동하지 않고 mappings 하위에 정의된 응답을 내려주어 테스트 환경을 외부 시스템과 완전히 분리하여 독립적으로 유지할 수 있습니다.


      예를 들어, 아래 stub은 POST /chat/completions 요청에 대해 JSON 정상 응답을 반환합니다.

      {
        "request": {
          "method": "POST",
          "urlPath": "/chat/completions"
        },
        "response": {
          "status": 200,
          "headers": {
            "Content-Type": "application/json"
          },
          "jsonBody": {
            "model": "gpt-5",
            "text": "오늘은 날씨가 좋아요!"
          }
        }
      }

      다음은 일정 시간 지연 응답을 테스트할 수 있는 Stub 예시입니다. 

      /api/v1/delay 경로로 POST 요청이 오면 1000ms(1초) 후에 200 OK 응답을 반환합니다.

      {
        "request": {
          "method": "POST",
          "urlPath": "/api/v1/delay"
        },
        "response": {
          "status": 200,
          "headers": {
            "Content-Type": "application/json"
          },
          "fixedDelayMilliseconds": 1000
        }
      }

      상황에 따라서는, 같은 URL Path에 대해 서로 다른 응답을 받아야 하는 경우도 있습니다 (e.g., 정상 응답과 에러 응답)

      우리 팀은 다양한 에러 상황을 분류하는 테스트가 필요했는데, 이때는 요청 body에 특정 키워드가 포함되어 있는지를 조건으로 stub을 정의했습니다.

      아래 예시는 "rate_limit_exceeded"나 "internal_error"가 body에 포함된 경우 각각 429, 500 에러를 반환하도록 설정한 stub입니다:

      {
        "mappings": [
          {
            "priority": 1,
            "request": {
              "method": "POST",
              "urlPath": "/chat/completions",
              "bodyPatterns": [
                {
                  "contains": "rate_limit_exceeded",
                  "ignoreExtraElements": true  # 추가 요소(필드/값)가 있어도 무시하고 비교(지정한 JSON 부분만 맞으면 매칭으로 인정)
                }
              ]
            },
            "response": {
              "status": 429,
              "headers": {
                "Content-Type": "application/json"
              },
              "jsonBody": {
                "error": {
                  "message": "You have exceeded allocated quota and must wait before making new requests",
                }
              }
            }
          },
          {
            "priority": 1,
            "request": {
              "method": "POST",
              "urlPath": "/v1/chat/completions",
              "bodyPatterns": [
                {
                  "contains": "internal_error",
                  "ignoreExtraElements": true   # 추가 요소(필드/값)가 있어도 무시하고 비교(지정한 JSON 부분만 맞으면 매칭으로 인정) 
                }
              ]
            },
            "response": {
              "status": 500,
              "headers": {
                "Content-Type": "application/json"
              },
              "jsonBody": {
                "error": {
                  "message": "Unexpected internal gateway error",
                }
              }
            }
          }
        ]
      }

      #### Tips💡- WireMock 관련 소소한 팁

      • 응답 본문 분리

        • JSON 매핑 파일 안에 응답 내용을 직접 넣을 수도 있지만, 응답이 길어질 경우 __files/ 디렉터리에 별도 파일로 저장하는 것을 권장합니다.

        • 이렇게 하면 매핑 파일은 요청·응답 구조만 간결하게 유지되고, 응답 본문은 재사용하기 쉬워집니다.

      • Priority 활용

        • 동일한 URL Path라도 요청 조건에 따라 다른 응답을 줄 수 있습니다.

        • priority 값이 낮을수록 먼저 매칭되며, 주로 에러 케이스 등 특수 상황에 낮은 값을 부여합니다.

        • 기본(default) 응답은 priority를 높게 설정해 마지막에 매칭되도록 합니다.

      Docker 컨테이너에서 WireMock 구동하기

      Docker 컨테이너에서 WireMock을 구동하는 가장 간단한 방법은 Docker Hub에 공식적으로 배포된 wiremock/wiremock 이미지를 사용하는 것입니다. 이 이미지를 사용하면 별도의 설정 없이 손쉽게 WireMock 서버를 실행할 수 있습니다.

      앞서 만들었던 프로젝트 구조 하위의 project-root/docker/wiremock/Dockerfile을 아래 코드와 같이 만들어 주었습니다. 여기서, 도커 이미지에 wiremock 디렉토리의 mappings 파일을 복사해 주었는데, mappings는 앞서 만든 API Stub들 입니다.

      ARG VERSION=3.12.1
      FROM wiremock/wiremock:${VERSION}
      COPY wiremock/mappings /home/wiremock/mappings
      • ARG VERSION=3.12.1 → WireMock 버전을 변수로 정의해, 빌드 시 원하는 버전으로 바꿀 수 있게 합니다.

      • FROM wiremock/wiremock:${VERSION} → 공식 WireMock 이미지를 지정한 버전으로 가져옵니다.

      • COPY wiremock/mappings /home/wiremock/mappings → 작성한 stub 매핑 파일들을 컨테이너에 복사해 WireMock이 자동 로딩하도록 합니다.

      그리고, 이렇게 만들어준 docker 컨테이너를 한번에 구성하고 실행할 수 있게 만들기 위해 docker-compose.yml 파일도 정의하여 주었습니다.

      services:
        wiremock:
          container_name: wiremock
          image: {containerRegistryUrl}/wiremock:3.12.1-25.06.1
          ports:
            - "8080:8080"
          restart: always
          healthcheck:
            test: curl -f http://localhost:8080/healthcheck || exit 1
            interval: 5s
            timeout: 3s
            retries: 10
      • container_name: wiremock → 컨테이너 이름을 wiremock으로 고정합니다.

      • image: {containerRegistryUrl}/wiremock:3.12.1-25.06.1 → GitLab Container Registry를 사용하고 있어서 해당 주소를 이용하였습니다.

      • ports: "8080:8080" → 로컬 8080 포트를 컨테이너 8080 포트와 연결합니다.

      • restart: always → 컨테이너가 중단되면 자동으로 재시작하는 옵션입니다.

      • healthcheck → http://localhost:8080/healthcheck 엔드포인트를 주기적으로 확인해 WireMock이 정상 구동 중인지 체크합니다. WireMock 컨테이너가 실행 중이라고 해서 바로 API를 받을 준비가 된 건 아니기 때문에, 실제로 wiremock이 실행이 되고 요청을 처리할 준비가 되었는지 체크하는 단계입니다.

        • test: 컨테이너 상태를 확인하기 위해 실행하는 명령을 정의

      이렇게 만든 docker-compose 파일을 이용하여 실행을 하여 주면 아래와 같이 실행여부를 확인할 수 있습니다. 짜잔... ! 🌟

      $ docker-compose up -d
      
      ██     ██ ██ ██████  ███████ ███    ███  ██████   ██████ ██   ██
      ██     ██ ██ ██   ██ ██      ████  ████ ██    ██ ██      ██  ██
      ██  █  ██ ██ ██████  █████   ██ ████ ██ ██    ██ ██      █████
      ██ ███ ██ ██ ██   ██ ██      ██  ██  ██ ██    ██ ██      ██  ██
       ███ ███  ██ ██   ██ ███████ ██      ██  ██████   ██████ ██   ██
      
      ----------------------------------------------------------------
      |               Cloud: https://wiremock.io/cloud               |
      |                                                              |
      |               Slack: https://slack.wiremock.org              |
      ----------------------------------------------------------------
      
      version:                      3.12.1
      port:                         8080

      API를 호출하여 보면, 앞서 만든 stub의 규칙에 따라서 결과를 반환해주는 것을 확인할 수 있습니다.

      앞서 만든 stub에서 body에 model_server_error가 들어오면, 오류를 반환하게 정의하였었는데요. 그 응답도 확인 할 수가 있네요~ !😀

      curl --location 'localhost:8080/v1/chat/completions' \
      --header 'Content-Type: application/json' \
      --data '{
         "data":"model_server_error"
      }'
      
      {"error":{"message":"Model responded with a server error","type":"internal_errors","code":"model_server_error"}}

      GitLab CI 파이프라인에 WireMock 서비스 추가하기

      우리의 목적은 여기서 끝난 게 아니었습니다. 배포 시 수행하는 테스트에서도 WireMock을 이용하여 자동화된 테스트를 안정적으로 실행하여야 했는데요. 

      GitLab CI 환경에서 테스트함으로써, 개발 플로우는 다음과 같이 개선되었습니다.

      image.png

      gitlab-ci를 통해서 테스트를 실행하기 위해, application-test.yml, .gitlab-ci.yml에 아래와 같이 코드를 추가하여 주었습니다.

      test:
        stage: test
        image: $COMMON_REGISTRY/java:11-glibc-25.04
        services:   
          - name: $COMMON_REGISTRY/wiremock:3.12.1-25.06.1
            alias: wiremock # 파이프라인 안에서 어떤 이름(hostname)으로 접근할지 지정
            variables:
              HEALTHCHECK_TCP_PORT: "8080"
        environment:
          name: testing
        tags:
          - test
        rules:
          - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

      여기서 중요한 부분은 services인데, job에서 함께 실행되는 컨테이너를 정의하는 기능입니다.  alias는 호출시 사용할 이름(예: wiremock)으로 지정해줍니다.

      다음으로, application-test를 살펴보겠습니다.

      # ===============================
      # = SERVERS PROPERTIES
      # ===============================
      servers:
        datas:
          - name: OPENAI
            scheme: http
            host: wiremock
            port: 8080
          - name: AZURE
            scheme: http
            host: wiremock
            port: 8080

      (경고) 여기서 host는 상위의 gitlab-ci에서 지정한 alias인 wiremock을 지정해주어야 합니다.

      통합 테스트 환경 실제로 적용해보니...

      테스트에 대한 신뢰감

      WireMock + Docker + GitLab CI로 통합 테스트 환경을 만들고 나서, 가장 먼저 달라진 건 테스트에 대한 신뢰감이었습니다. 예전에는 CI가 깨지면 무엇이 문제인지 이유를 찾느라 시간을 허비하곤 했는데, 지금은 외부 환경이 흔들려도 우리가 만든 테스트는 안정적으로 돌아간다는 안정감을 얻을 수 있었습니다.


      로컬에서도 마이크로 서비스 환경을 간편하게

      마이크로 서비스 환경에서는 여러 서버가 함께 동작해야 기능이 정상 작동하는 경우가 종종 있습니다. 하지만 로컬에서 이를 테스트하려면 필요한 서버를 전부 실행해야 하는데, 이 과정이 꽤 번거롭습니다. 이럴 때 WireMock으로 해당 서버들을 Mocking하면, 다른 서버를 띄우지 않고도 독립적으로 테스트를 진행할 수 있습니다.


      생산성 향상

      WireMock을 도입하고 나서는 반복 작업에서 많이 해방됐습니다. 굳이 Mock API를 한 땀 한 땀 만들어주지 않아도, 필요한 응답 패턴만 정의해두면 WireMock이 서버처럼 알아서 응답하기 때문에 테스트 시나리오를 짜는 데 집중할 수 있었습니다.


      돌이켜보면, 이 작업은 단순히 테스트 환경을 꾸미는 게 아니라 팀 전체의 개발 사이클을 바꾸는 경험이었습니다. 테스트 실패에 흔들리지 않고, 안정적으로 기능을 개발하고, 빠르게 배포할 수 있는 기반을 마련했기 때문입니다.

      앞으로도 새로운 API 프로바이더가 붙거나 요구사항이 바뀌더라도, 이 환경을 계속 확장하고 다듬어가면서 “실제처럼, 빠르게, 안정적으로” 에이닷을 완성도 있게 만드는 목표를 유지해 나가려고 합니다.



      참고

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      지윤 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기