microservice 20 개. service A 가 service B 의 API 형식 변경. deploy 후 A 의 client 코드가 깨짐. integration test 가 전부 통과 했는데 왜? — A 는 B 의 새 형식 안 보고 test 했고, B 는 A 의 사용 패턴 모름. Contract testing 이 이 gap 을 메꿈. 이 가이드는 consumer-driven contract 의 메커니즘과 Pact 의 실제 작동을 정리한다.
Microservice Integration Test 의 한계
naive 방법:
Service A test:
- service B 실제로 띄움 (또는 그 의존 stub)
- HTTP 호출 → 응답 검증
문제:
1. CI 에서 B 띄우려면 B 의 모든 의존 (B → C → D → DB) 도
2. B 의 새 버전이 deploy 되기 전엔 A 의 test 가 새 형식 검증 X
3. 20 개 service 면 한 test 가 20 service 띄움 → 분 단위 + flaky
4. service 간 의존 cycle 어떻게?
→ "spin up 20 services for one test" 의 비용 + 신뢰성 hellholeContract — 양측의 약속
Contract = 한 service 가 다른 service 의 API 를 어떻게 사용하는지 명세.
예 (Pact):
{
consumer: "service-a",
provider: "service-b",
interactions: [
{
description: "get user by id",
request: {
method: "GET",
path: "/users/42",
headers: {"Accept": "application/json"}
},
response: {
status: 200,
body: {
id: 42,
name: matcher.string("Alice"), // 정확한 값 X, type 만
email: matcher.regex(/.+@.+/)
}
}
}
]
}
→ JSON 으로 저장. git versioned. Pact Broker (또는 git) 로 공유.Consumer-Driven Contract Testing
Step 1 — Consumer (A) 가 contract 생성
test("get user from B", () => {
pact.given("user 42 exists")
.uponReceiving("a GET")
.withRequest({method: "GET", path: "/users/42"})
.willRespondWith({status: 200, body: {id: 42, name: "Alice"}});
// A 의 코드 실행 — Pact mock server 가 contract 대로 응답
const user = await getUserFromB(42);
expect(user.name).toBe("Alice");
});
→ 통과 시 contract JSON 생성 → Pact Broker upload
Step 2 — Provider (B) 가 contract 검증
pact.verifyProvider({
providerName: "service-b",
providerBaseUrl: "http://localhost:8080",
pactBrokerUrl: "...",
stateHandlers: {
"user 42 exists": () => db.users.insert({id: 42, name: "Alice"})
}
});
→ B 가 실제로 도는 상태에서 contract 의 모든 interaction 호출 → 검증.
→ B 의 새 변경이 contract 깨면 → CI fail.
→ 두 단계 모두 통과 = A 와 B 의 통합 보장 (둘 다 띄우지 않고).Consumer-Driven 의 의미
Provider (B) 가 spec 만들고 consumer (A) 가 따르는 게 아님 — consumer (A) 가 "내가 이렇게 쓴다" 선언하고 provider (B) 가 그것을 보장.
이점:
- B 가 "어떤 field 가 실제 사용되나" 알 수 있음
- B 가 unused field 안전하게 제거 (어느 consumer 도 사용 X 확인 후)
- A 가 "필요한 것만" 명세 — overspec 회피
vs Schema-first (OpenAPI):
- OpenAPI: B 가 "내가 이런 API 제공" → A 가 알아서 사용
- Pact: A 가 "내가 이렇게 쓴다" → B 가 만족
- 둘 다 가치 있음 — OpenAPI 는 외부 공개 API, Pact 는 내부 service 간Provider States — given clause
Contract test 의 함정 — "user 42 가 있다" 가 검증 시점에 DB 에 없으면?
해결: provider state (Pact 의 given clause):
consumer test:
pact.given("user 42 exists")
.uponReceiving("get user")
...
provider verification:
stateHandlers: {
"user 42 exists": async () => {
await db.clear();
await db.users.insert({id: 42, name: "Alice"});
},
"user 42 does not exist": async () => {
await db.clear();
}
}
→ provider 가 검증 전에 그 state 만듦. consumer 의 가정과 일치.Matchers — flexible 검증
contract 가 너무 strict 하면 — provider 가 timestamp 등 변경 시 깨짐.
matcher 로 "type/format 만 검증, 값 자유":
response: {
status: 200,
body: {
id: matcher.integer(42), // integer 면 OK (42 는 예시)
name: matcher.string("Alice"), // string 면 OK
created_at: matcher.iso8601DateTime(), // ISO datetime 면 OK
email: matcher.regex(/^.+@.+$/),
items: matcher.eachLike({id: integer(), price: decimal()})
}
}
→ provider 의 정확한 값 변동 (timestamp, id) 허용, 구조는 보장.Pact Broker — 공유 + workflow
자체 host 또는 SaaS (PactFlow).
기능:
- contract JSON 저장 (consumer/provider 별)
- version tagging (main, staging, production)
- "can-i-deploy?" query — "service A v3 가 production 의 B 와 호환?"
- 의존 graph 시각화 (어떤 service 가 어떤 service 의존)
- 변경 webhook (consumer contract 새 버전 → provider CI 자동 trigger)
CI 통합:
- consumer build → contract publish → "ready for verification" tag
- provider build → broker 에서 contract 가져와 verify → "verified" tag
- deploy 전 can-i-deploy 호출 → 양쪽 호환 시만 deployContract vs Integration vs E2E
| 검증 | 방법 | 비용 |
|---|---|---|
| 한 함수 / class | Unit | 저렴 |
| 한 service 의 통합 (DB 포함) | Integration | 중간 |
| service A ↔ service B contract | Contract (Pact) | 저렴 (둘 다 안 띄움) |
| 사용자 관점 전체 flow | E2E | 매우 비쌈 |
흔한 함정
- contract 가 너무 strict — matcher 안 쓰고 정확한 값 비교 → provider 의 작은 변경 시 깨짐. matcher 활용.
- provider state 부재 — "user 42 exists" 같은 전제 없이 verify → DB state 의존 fail.
- contract 의 의존성 graph 무시 — A→B→C 의 contract 있어도 A→C 직접 호출은 검증 안 됨. 모든 dep 별 contract 필요.
- contract = 모든 검증 대체 가정 — contract 는 interface 검증만. 비즈니스 로직 / performance 는 별도.
- broker 없이 운영 — JSON 을 git 으로 직접 공유 가능하지만 version tag · can-i-deploy 같은 workflow 없음.
마무리
Contract testing 의 본질 — "service 둘 다 안 띄우고도 둘 사이 통합 보장". consumer-driven 으로 "실제 사용 패턴만 명세" → false positive 적음.
실용 — microservice 5+ 시 도입 가치. 내부 service 간은 Pact, 외부 API 는 OpenAPI 로 양분. Pact Broker + can-i-deploy 워크플로우가 deploy 안전성의 핵심.