소비자 주도 계약을 통한 마이크로서비스 통합 테스트 간소화
Ethan Miller
Product Engineer · Leapcell

소개
빠르게 발전하는 분산 시스템 환경에서 마이크로서비스는 확장 가능하고 탄력적인 애플리케이션을 구축하기 위한 사실상의 아키텍처가 되었습니다.
하지만 이러한 아키텍처 패러다임은 중요한 과제를 제시합니다. 바로 독립적으로 배포된 서비스 간의 원활한 통신 및 호환성을 보장하는 것입니다.
기존의 통합 테스트는 종종 전체 마이크로서비스 클러스터를 설정하고 실행해야 하는데, 이는 시간이 많이 걸리고 리소스를 많이 사용하며 불안정하기 쉽습니다.
이러한 오버헤드는 개발 주기를 크게 늦추고 지속적인 통합 및 배포를 어렵게 만듭니다.
이 글에서는 이 문제를 해결하는 강력한 기법인 소비자 주도 계약 테스트, 특히 Pact.io를 사용하여 심층적으로 살펴봅니다.
이 접근 방식이 팀이 API 호환성을 효율적으로 검증하여 소비자와 제공자가 완전히 실행된 환경 없이도 합의된 계약을 준수하도록 하는 방법을 살펴보겠습니다.
계약의 힘
Pact.io의 구체적인 내용을 살펴보기 전에 관련된 핵심 개념을 명확히 이해해 봅시다.
- 마이크로서비스: 느슨하게 결합된 독립적으로 배포 가능한 서비스들의 모음으로 애플리케이션을 구조화하는 아키텍처 스타일입니다. 각 서비스는 일반적으로 특정 비즈니스 기능에 중점을 둡니다.
- API (애플리케이션 프로그래밍 인터페이스): 서로 다른 소프트웨어 애플리케이션이 서로 통신할 수 있도록 하는 정의된 규칙 집합입니다. 마이크로서비스에서 API는 서비스 간 통신의 주요 수단입니다.
- 통합 테스트: 시스템의 서로 다른 단위 또는 구성 요소 간의 상호 작용을 검증하는 소프트웨어 테스트 유형입니다. 마이크로서비스 맥락에서 이는 종종 다른 서비스가 어떻게 통신하는지 테스트하는 것을 의미합니다.
- 소비자 주도 계약 테스트: API의 소비자가 제공자와의 '계약' 또는 예상되는 상호 작용을 정의하는 테스트 방법론입니다. 그런 다음 제공자는 이 계약을 사용하여 소비자의 기대를 충족하는지 확인합니다. 이렇게 하면 제공자 측의 변경이 명시적인 계약 위반 없이 소비자를 중단시키지 않도록 합니다.
소비자 주도 계약의 기본 원칙은 기존의 주도권을 역전시키는 것입니다.
제공자가 API를 결정하는 대신 소비자가 필요한 것을 명시적으로 진술합니다.
Pact.io는 이 프로세스를 촉진하는 인기 있는 프레임워크입니다.
Pact.io 작동 방식
Pact.io는 간단하지만 강력한 3단계 프로세스로 작동합니다.
- 소비자 테스트 생성: 소비자 서비스는 제공자와의 예상되는 상호 작용을 정의하는 테스트를 작성합니다. 이 테스트는 제공자에 대한 요청을 시뮬레이션하고 예상 응답을 어설션합니다. Pact.io는 이러한 상호 작용을 캡처하고 계약을 나타내는 JSON 문서인 'Pact 파일'을 생성합니다.
- Pact 파일 게시: 생성된 Pact 파일은 종종 Pact Broker라고 하는 중앙 저장소에 게시됩니다. 이 브로커는 모든 계약의 단일 진실 공급원 역할을 합니다.
- 제공자 검증: 제공자 서비스는 Pact Broker에서 관련 계약을 검색하고 실제 API 구현이 이러한 계약을 준수하는지 확인합니다. 이 검증 프로세스는 제공자의 빌드 파이프라인의 일부로 실행됩니다.
간단한 예시를 통해 설명해 보겠습니다.
OrderService(소비자)와 ProductCatalogService(제공자)라는 두 개의 서비스가 있다고 가정해 봅시다.
OrderService는 ProductCatalogService에서 제품 세부 정보를 검색해야 합니다.
소비자 코드 예시 (Pact.js를 사용한 JavaScript)
// order-service/tests/contract/product-catalog.spec.js const { pact, provider } = require('@pact-foundation/pact'); const ProductServiceClient = require('../../src/ProductServiceClient'); // 우리의 소비자 클라이언트 describe('ProductCatalogService Integration', () => { let client; beforeAll(() => { // Pact 모의 서버 설정 return provider.setup(); }); afterEach(() => provider.verify()); afterAll(() => provider.finalize()); it('should be able to get a product by ID', async () => { const expectedBody = { id: 'P123', name: 'Laptop', price: 1200.00 }; await provider.addInteraction({ uponReceiving: 'a request for a product by ID', withRequest: { method: 'GET', path: '/products/P123', headers: { 'Accept': 'application/json' }, }, willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: expectedBody, }, }); client = new ProductServiceClient(provider.mockService.baseUrl); const product = await client.getProductById('P123'); expect(product).toEqual(expectedBody); }); });
이 소비자 테스트에서:
@pact-foundation/pact를 사용하여 모의 제공자를 설정합니다.provider.addInteraction은 계약을 정의합니다. 소비자가/products/P123으로GET요청을 하면 모의 서버는 특정 JSON 본문과 상태로 응답해야 합니다.- 그런 다음
ProductServiceClient는 이 모의 서버에 대한 실제 호출을 수행하고 응답을 어설션합니다. - 테스트 실행 후 Pact.js는
pacts디렉토리에product-catalog-service-order-service.json파일을 생성합니다.
제공자 코드 예시 (Pact JVM을 사용한 Spring Boot)
// product-catalog-service/src/test/java/com/example/ProductCatalogServicePactVerificationTest.java package com.example; import au.com.dius.pact.provider.junit5.HttpTestTarget; import au.com.dius.pact.provider.junit5.PactVerificationContext; import au.com.dius.pact.provider.junit5.PactVerificationProvider; import au.com.dius.pact.provider.junitsupport.Provider; import au.com.dius.pact.provider.junitsupport.loader.PactBroker; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; @Provider("ProductCatalogService") // 우리의 제공자 이름 @PactBroker(host = "localhost", port = "9292") // Pact Broker가 실행되는 곳 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ExtendWith(PactVerificationProvider.class) public class ProductCatalogServicePactVerificationTest { @LocalServerPort private int port; @BeforeEach void setUp(PactVerificationContext context) { context.setTarget(new HttpTestTarget("localhost", port)); } @TestTemplate void pactVerificationTest(PactVerificationContext context) { context.verifyInteraction(); } }
제공자 테스트에서:
@Provider("ProductCatalogService")는 이 서비스를 제공자로 식별합니다.@PactBroker는 계약을 가져올 위치를 지정합니다.@SpringBootTest(webEnvironment = SpringTest.WebEnvironment.RANDOM_PORT)는 무작위 포트에서 실제 Spring Boot 애플리케이션을 시작합니다.setUp메서드는 Pact가 이 실행 중인 인스턴스를 가리키도록 구성합니다.context.verifyInteraction()은 Pact에 브로커에서 계약을 검색하고 계약에 따라 실제 실행 중인ProductCatalogService에 실제 요청을 보내고 응답이 정의된 계약과 일치하는지 확인하도록 지시합니다.
이렇게 하면 OrderService 자체를 실행할 필요 없이 제공자가 소비자의 기대에 대해 테스트될 수 있습니다.
애플리케이션 시나리오 및 이점
소비자 주도 계약 테스트는 여러 시나리오에서 빛을 발합니다.
- 마이크로서비스 생태계: 호환성을 유지하면서 서비스의 독립적인 개발 및 배포를 지원하는 주요 사용 사례입니다.
- API 게이트웨이: 서비스 계약에 따라 게이트웨이가 요청을 올바르게 라우팅하고 변환하는지 확인합니다.
- 타사 통합: 외부 API와의 계약을 정의하여 조기에 중단되는 변경 사항을 포착합니다. (외부 당사자가 검증을 위해 Pact.io를 채택하지 않을 수도 있습니다.)
주요 이점은 다음과 같습니다.
- 중단 변경 사항 조기 감지: 제공자의 빌드 파이프라인에서 계약 위반이 감지되어 프로덕션으로 문제가 전달되는 것을 방지합니다.
- 통합 테스트 복잡성 감소: 호환성 테스트를 위해 전체 마이크로서비스 클러스터를 실행할 필요성을 제거하여 시간과 리소스를 절약합니다.
- 더 빠른 피드백 루프: 개발자는 API 변경에 대한 즉각적인 피드백을 받아 개발을 가속화합니다.
- 명확한 API 통신: 계약은 팀 간의 API 기대치를 명확하게 정의하는 살아있는 문서 역할을 합니다.
- 향상된 신뢰 및 협업: 팀이 호환성이 보장된다는 것을 알면서 자신 있게 변경할 수 있는 협업 환경을 조성합니다.
결론
Pact.io를 사용한 소비자 주도 계약 테스트는 마이크로서비스 아키텍처에서 API 호환성을 관리하기 위한 우아하고 효율적인 솔루션을 제공합니다.
기대치를 정의하는 책임을 소비자에게서 제공자의 빌드 파이프라인에서 이러한 기대치를 확인하는 것으로 전환함으로써 팀은 기존 통합 테스트와 관련된 복잡성과 불안정성을 크게 줄일 수 있습니다.
이 접근 방식은 더 빠른 개발 주기, 더 강력한 배포 및 전반적인 시스템에 대한 더 높은 수준의 자신감으로 이어집니다.
소비자 주도 계약을 채택하여 더 탄력적이고 확장 가능한 마이크로서비스 생태계를 구축하십시오.

