- Authors

- Name
- Youngju Kim
- @fjvbn20031
개요
현대 소프트웨어 개발에서 프론트엔드와 백엔드는 대부분 별도 팀이 동시에 작업합니다. 백엔드 API가 완성될 때까지 프론트엔드 개발이 블로킹되거나, 외부 서드파티 API에 의존하는 기능을 테스트할 때 실제 서비스에 접근할 수 없는 상황이 자주 발생합니다.
**모의 서버(Mock Server)**는 이런 문제를 해결하는 핵심 도구입니다. API 계약을 먼저 정의하고, 실제 백엔드 없이도 프론트엔드 개발과 테스트를 진행할 수 있게 해줍니다.
1. 모의 서버가 필요한 이유
1.1 프론트엔드-백엔드 병렬 개발
전통적인 개발 방식 (순차):
백엔드 API 완성 → 프론트엔드 개발 시작
└── 지연: 프론트엔드가 백엔드를 기다림
모의 서버를 활용한 방식 (병렬):
API 스펙 정의 → 백엔드 개발 || 프론트엔드 개발
↑ 모의 서버로 블로킹 없음
두 팀이 API 계약(Contract)만 먼저 합의하면 동시에 개발할 수 있습니다.
1.2 외부 API 의존성 제거
외부 결제 API, 지도 서비스, 날씨 API 등 서드파티 서비스는 다음 문제가 있습니다:
- 비용 발생: 호출마다 과금되는 API
- 속도 제한: Rate Limiting으로 테스트 실행이 느려짐
- 서비스 불안정: 외부 서비스 장애로 CI/CD 파이프라인이 실패
- 테스트 데이터 오염: 실제 데이터에 테스트 데이터가 섞임
1.3 에러 케이스 재현
실제 서비스에서 재현하기 어려운 상황을 모의 서버로 쉽게 재현할 수 있습니다:
- 네트워크 타임아웃
- 500 Internal Server Error
- 429 Too Many Requests
- 네트워크 연결 끊김
- 응답 지연 (느린 네트워크 시뮬레이션)
1.4 테스트 환경 안정성
CI/CD 파이프라인에서 외부 의존성 없애기:
실제 DB 없이 → InMemory DB Fake
실제 API 없이 → Mock Server
실제 메시지 큐 없이 → In-process Fake
2. json-server (가장 빠른 시작)
json-server는 JSON 파일 하나로 30초 만에 REST API 서버를 만들 수 있는 도구입니다.
2.1 설치와 기본 사용법
# 전역 설치
npm install -g json-server
# 또는 프로젝트에 설치
npm install --save-dev json-server
2.2 db.json 구조 설계
{
"users": [
{ "id": 1, "name": "홍길동", "email": "hong@example.com", "role": "admin" },
{ "id": 2, "name": "이순신", "email": "lee@example.com", "role": "user" }
],
"posts": [
{ "id": 1, "title": "첫 번째 포스트", "body": "내용입니다", "userId": 1, "published": true },
{ "id": 2, "title": "두 번째 포스트", "body": "내용입니다", "userId": 2, "published": false }
],
"comments": [{ "id": 1, "text": "좋은 글이네요", "postId": 1, "userId": 2 }],
"profile": {
"name": "관리자",
"version": "1.0.0"
}
}
json-server 실행:
json-server --watch db.json --port 3001
자동으로 생성되는 엔드포인트:
GET /users
GET /users/1
POST /users
PUT /users/1
PATCH /users/1
DELETE /users/1
GET /posts?userId=1
GET /posts?published=true
GET /posts?_page=1&_limit=10
GET /posts?_sort=title&_order=asc
2.3 커스텀 라우팅 (routes.json)
{
"/api/*": "/$1",
"/api/v1/users/:id": "/users/:id",
"/blog/:id/comments": "/comments?postId=:id"
}
실행 시 라우팅 파일 적용:
json-server --watch db.json --routes routes.json --port 3001
2.4 미들웨어 추가
// middleware.js
module.exports = (req, res, next) => {
// 인증 헤더 검사
if (req.headers.authorization !== 'Bearer valid-token') {
if (req.method !== 'GET') {
return res.status(401).json({ error: 'Unauthorized' })
}
}
// 응답 지연 시뮬레이션 (500ms)
setTimeout(next, 500)
}
json-server --watch db.json --middlewares middleware.js
2.5 관계형 데이터 모델링
json-server는 _embed와 _expand를 통해 관계형 데이터를 지원합니다:
# 댓글 포함한 포스트 조회
GET /posts/1?_embed=comments
# 작성자 정보 포함
GET /posts?_expand=user
# 중첩 관계
GET /users?_embed=posts
2.6 페이지네이션, 정렬, 필터링
# 페이지네이션 (10개씩, 2페이지)
GET /posts?_page=2&_limit=10
# 정렬 (제목 내림차순)
GET /posts?_sort=title&_order=desc
# 필터링 (published=true인 포스트)
GET /posts?published=true
# 검색 (전체 텍스트)
GET /posts?q=검색어
# 범위 필터
GET /posts?id_gte=5&id_lte=10
2.7 커스텀 서버 스크립트
// server.js - 더 세밀한 제어가 필요할 때
const jsonServer = require('json-server')
const server = jsonServer.create()
const router = jsonServer.router('db.json')
const middlewares = jsonServer.defaults()
server.use(middlewares)
// 커스텀 라우트
server.post('/auth/login', (req, res) => {
const { email, password } = req.body
if (email === 'admin@example.com' && password === 'password') {
res.json({ token: 'fake-jwt-token', userId: 1 })
} else {
res.status(401).json({ error: 'Invalid credentials' })
}
})
server.use(router)
server.listen(3001, () => {
console.log('JSON Server is running on port 3001')
})
3. MSW (Mock Service Worker) - 현대적 방법
MSW는 Service Worker를 활용해 네트워크 레벨에서 요청을 가로채는 현대적인 API 모킹 라이브러리입니다. 브라우저와 Node.js 양쪽에서 동일한 핸들러를 사용할 수 있습니다.
3.1 설치 및 설정
npm install msw --save-dev
# Service Worker 파일 생성 (public 폴더에)
npx msw init public/ --save
3.2 REST API 핸들러 작성
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
// GET 사용자 목록
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: '홍길동', email: 'hong@example.com' },
{ id: 2, name: '이순신', email: 'lee@example.com' },
])
}),
// GET 특정 사용자
http.get('/api/users/:id', ({ params }) => {
const { id } = params
if (id === '999') {
return new HttpResponse(null, { status: 404 })
}
return HttpResponse.json({
id: Number(id),
name: '홍길동',
email: 'hong@example.com',
})
}),
// POST 사용자 생성
http.post('/api/users', async ({ request }) => {
const newUser = await request.json()
return HttpResponse.json({ id: Date.now(), ...newUser }, { status: 201 })
}),
// PUT 사용자 수정
http.put('/api/users/:id', async ({ params, request }) => {
const { id } = params
const updates = await request.json()
return HttpResponse.json({ id: Number(id), ...updates })
}),
// DELETE 사용자 삭제
http.delete('/api/users/:id', ({ params }) => {
return new HttpResponse(null, { status: 204 })
}),
]
3.3 GraphQL 핸들러
import { graphql, HttpResponse } from 'msw'
export const graphqlHandlers = [
graphql.query('GetUser', ({ variables }) => {
const { id } = variables
return HttpResponse.json({
data: {
user: {
id,
name: '홍길동',
email: 'hong@example.com',
posts: [{ id: 1, title: '첫 번째 포스트' }],
},
},
})
}),
graphql.mutation('CreateUser', ({ variables }) => {
const { name, email } = variables
return HttpResponse.json({
data: {
createUser: {
id: String(Date.now()),
name,
email,
},
},
})
}),
]
3.4 동적 응답 (요청 파라미터 활용)
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/products', ({ request }) => {
const url = new URL(request.url)
const category = url.searchParams.get('category')
const page = Number(url.searchParams.get('page') ?? '1')
const limit = Number(url.searchParams.get('limit') ?? '10')
const allProducts = [
{ id: 1, name: '노트북', category: 'electronics', price: 1200000 },
{ id: 2, name: '마우스', category: 'electronics', price: 30000 },
{ id: 3, name: '책상', category: 'furniture', price: 250000 },
]
let filtered = allProducts
if (category) {
filtered = allProducts.filter((p) => p.category === category)
}
const start = (page - 1) * limit
const data = filtered.slice(start, start + limit)
return HttpResponse.json({
data,
total: filtered.length,
page,
limit,
})
}),
]
3.5 에러 응답 시나리오
import { http, HttpResponse } from 'msw'
// 에러 시나리오를 동적으로 제어하는 패턴
let shouldFailNext = false
export const handlers = [
// 에러 시뮬레이션 제어 엔드포인트 (테스트 전용)
http.post('/test/simulate-error', async ({ request }) => {
const { fail } = await request.json()
shouldFailNext = fail
return new HttpResponse(null, { status: 200 })
}),
http.post('/api/payment', async ({ request }) => {
if (shouldFailNext) {
shouldFailNext = false
return HttpResponse.json(
{ error: 'PAYMENT_DECLINED', message: '카드가 거절되었습니다' },
{ status: 402 }
)
}
const { amount } = await request.json()
return HttpResponse.json({
paymentId: `PAY-${Date.now()}`,
amount,
status: 'SUCCESS',
})
}),
// 네트워크 에러 시뮬레이션
http.get('/api/unstable', () => {
if (Math.random() > 0.7) {
return HttpResponse.error()
}
return HttpResponse.json({ data: 'success' })
}),
]
3.6 브라우저 환경 설정
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
// src/main.tsx (React 앱 진입점)
async function enableMocking() {
if (process.env.NODE_ENV !== 'development') {
return
}
const { worker } = await import('./mocks/browser')
return worker.start({
onUnhandledRequest: 'bypass' // 처리되지 않은 요청은 그대로 통과
})
}
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
})
3.7 Node.js 환경 설정 (테스트)
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'
export const server = setupServer(...handlers)
// jest.setup.ts
import { server } from './src/mocks/server'
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
// 테스트에서 핸들러 오버라이드
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
test('API 에러 처리 테스트', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json({ error: 'Server Error' }, { status: 500 })
})
)
const result = await fetchUsers()
expect(result.error).toBe('Server Error')
})
3.8 Next.js에서 MSW 설정
// next.config.js
const nextConfig = {
// Service Worker를 위한 헤더 설정
async headers() {
return [
{
source: '/mockServiceWorker.js',
headers: [{ key: 'Service-Worker-Allowed', value: '/' }],
},
]
},
}
// src/app/providers.tsx (App Router)
'use client'
import { useEffect } from 'react'
export function MSWProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (process.env.NEXT_PUBLIC_API_MOCKING === 'enabled') {
import('../mocks/browser').then(({ worker }) => {
worker.start({ onUnhandledRequest: 'bypass' })
})
}
}, [])
return children
}
4. WireMock - Java/JVM 환경
WireMock은 Java/JVM 생태계에서 가장 널리 사용되는 HTTP 모의 서버입니다.
4.1 WireMock Standalone 서버 실행
# WireMock 다운로드
curl -o wiremock.jar https://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-jre8-standalone/2.35.0/wiremock-jre8-standalone-2.35.0.jar
# 실행
java -jar wiremock.jar --port 8080 --verbose
디렉토리 구조:
wiremock/
├── mappings/ # Stub 정의 JSON 파일
│ ├── users.json
│ └── orders.json
└── __files/ # 응답 파일 (body 파일)
├── users.json
└── order-template.json
4.2 JSON 파일로 Stub 정의
{
"request": {
"method": "GET",
"url": "/api/users"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": [
{ "id": 1, "name": "홍길동", "email": "hong@example.com" },
{ "id": 2, "name": "이순신", "email": "lee@example.com" }
]
}
}
4.3 Request Matching (URL, headers, body)
{
"request": {
"method": "POST",
"url": "/api/orders",
"headers": {
"Content-Type": {
"equalTo": "application/json"
},
"Authorization": {
"matches": "Bearer .+"
}
},
"bodyPatterns": [
{
"matchesJsonPath": "$.items[*].productId"
},
{
"equalToJson": {
"userId": 1
},
"ignoreArrayOrder": true,
"ignoreExtraElements": true
}
]
},
"response": {
"status": 201,
"jsonBody": {
"orderId": "ORDER-001",
"status": "PENDING"
}
}
}
4.4 Response Templating (Handlebars)
{
"request": {
"method": "GET",
"urlPathPattern": "/api/users/([0-9]+)"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "{ \"id\": {{request.pathSegments.[2]}}, \"name\": \"사용자-{{request.pathSegments.[2]}}\", \"timestamp\": \"{{now}}\" }",
"transformers": ["response-template"]
}
}
4.5 Scenarios (상태 기반 모킹)
시나리오를 사용하면 상태에 따라 다른 응답을 반환할 수 있습니다:
{
"scenarioName": "주문 처리 흐름",
"requiredScenarioState": "Started",
"newScenarioState": "결제 완료",
"request": {
"method": "POST",
"url": "/api/orders/pay"
},
"response": {
"status": 200,
"jsonBody": { "status": "PAID" }
}
}
{
"scenarioName": "주문 처리 흐름",
"requiredScenarioState": "결제 완료",
"newScenarioState": "배송 중",
"request": {
"method": "GET",
"url": "/api/orders/ORDER-001"
},
"response": {
"status": 200,
"jsonBody": { "status": "SHIPPING" }
}
}
4.6 Record and Playback
실제 API 응답을 기록하고 재생하는 기능:
# 기록 모드로 실행 (실제 API로 프록시)
java -jar wiremock.jar --proxy-all="https://api.example.com" --record-mappings --port 8080
# 기록된 응답 재생 (기본 모드)
java -jar wiremock.jar --port 8080
5. Prism - OpenAPI 기반 모의 서버
Prism은 OpenAPI(Swagger) 스펙에서 자동으로 모의 서버를 생성하는 도구입니다.
5.1 설치와 실행
npm install -g @stoplight/prism-cli
# OpenAPI 스펙으로 모의 서버 실행
npx @stoplight/prism-cli mock api.yaml --port 4010
5.2 OpenAPI 스펙 예시
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
get:
operationId: getUsers
responses:
'200':
description: 사용자 목록
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
examples:
default:
value:
- id: 1
name: 홍길동
email: hong@example.com
post:
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: 사용자 생성됨
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
required: [id, name, email]
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email
CreateUserRequest:
type: object
required: [name, email]
properties:
name:
type: string
email:
type: string
format: email
5.3 동적 응답 생성
Prism은 스펙에서 예시값이 없으면 스키마를 기반으로 자동으로 값을 생성합니다:
# 동적 생성 모드 (스키마 기반 랜덤 응답)
npx @stoplight/prism-cli mock api.yaml --dynamic
5.4 Validation 모드
# Validation 모드 - 요청/응답을 스펙과 대조 검증
npx @stoplight/prism-cli proxy api.yaml https://api.example.com --validate-request --validate-response
6. Go로 모의 서버 구현
Go 표준 라이브러리만으로 완전한 모의 서버를 구현할 수 있습니다.
6.1 기본 모의 서버 구조
package mockserver
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
)
type MockServer struct {
server *http.Server
mux *http.ServeMux
mu sync.RWMutex
stubs map[string]*Stub
calls []RequestLog
}
type Stub struct {
Method string
Path string
StatusCode int
Body interface{}
Headers map[string]string
Delay time.Duration
}
type RequestLog struct {
Method string
Path string
Headers map[string][]string
Body []byte
Timestamp time.Time
}
func NewMockServer(port string) *MockServer {
ms := &MockServer{
mux: http.NewServeMux(),
stubs: make(map[string]*Stub),
}
ms.server = &http.Server{
Addr: ":" + port,
Handler: ms.mux,
}
ms.mux.HandleFunc("/", ms.handleRequest)
// 테스트 제어 엔드포인트
ms.mux.HandleFunc("/__admin/stubs", ms.handleAdminStubs)
ms.mux.HandleFunc("/__admin/calls", ms.handleAdminCalls)
ms.mux.HandleFunc("/__admin/reset", ms.handleAdminReset)
return ms
}
func (ms *MockServer) Start() {
go func() {
log.Printf("MockServer starting on %s", ms.server.Addr)
if err := ms.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("MockServer failed: %v", err)
}
}()
// 서버 시작 대기
time.Sleep(100 * time.Millisecond)
}
func (ms *MockServer) Stop() {
ms.server.Close()
}
func (ms *MockServer) AddStub(stub *Stub) {
ms.mu.Lock()
defer ms.mu.Unlock()
key := stub.Method + ":" + stub.Path
ms.stubs[key] = stub
}
func (ms *MockServer) handleRequest(w http.ResponseWriter, r *http.Request) {
// 요청 로깅
ms.mu.Lock()
ms.calls = append(ms.calls, RequestLog{
Method: r.Method,
Path: r.URL.Path,
Headers: r.Header,
Timestamp: time.Now(),
})
ms.mu.Unlock()
// Stub 조회
ms.mu.RLock()
key := r.Method + ":" + r.URL.Path
stub, ok := ms.stubs[key]
ms.mu.RUnlock()
if !ok {
http.Error(w, "No stub found for "+r.Method+" "+r.URL.Path, http.StatusNotFound)
return
}
// 응답 지연 시뮬레이션
if stub.Delay > 0 {
time.Sleep(stub.Delay)
}
// 헤더 설정
for k, v := range stub.Headers {
w.Header().Set(k, v)
}
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", "application/json")
}
w.WriteHeader(stub.StatusCode)
if stub.Body != nil {
json.NewEncoder(w).Encode(stub.Body)
}
}
func (ms *MockServer) handleAdminReset(w http.ResponseWriter, r *http.Request) {
ms.mu.Lock()
ms.stubs = make(map[string]*Stub)
ms.calls = nil
ms.mu.Unlock()
w.WriteHeader(http.StatusOK)
}
func (ms *MockServer) handleAdminCalls(w http.ResponseWriter, r *http.Request) {
ms.mu.RLock()
defer ms.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ms.calls)
}
func (ms *MockServer) handleAdminStubs(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
var stub Stub
if err := json.NewDecoder(r.Body).Decode(&stub); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ms.AddStub(&stub)
w.WriteHeader(http.StatusCreated)
return
}
ms.mu.RLock()
defer ms.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ms.stubs)
}
6.2 테스트에서 모의 서버 사용
func TestUserService_GetUser(t *testing.T) {
// 모의 서버 시작
ms := NewMockServer("9999")
ms.Start()
defer ms.Stop()
// Stub 설정
ms.AddStub(&Stub{
Method: "GET",
Path: "/api/users/1",
StatusCode: 200,
Body: map[string]interface{}{
"id": 1,
"name": "홍길동",
"email": "hong@example.com",
},
})
// 테스트 대상 서비스 (모의 서버 URL 주입)
client := NewUserServiceClient("http://localhost:9999")
user, err := client.GetUser(1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "홍길동" {
t.Errorf("expected 홍길동, got %s", user.Name)
}
}
func TestUserService_GetUser_NotFound(t *testing.T) {
ms := NewMockServer("9998")
ms.Start()
defer ms.Stop()
ms.AddStub(&Stub{
Method: "GET",
Path: "/api/users/999",
StatusCode: 404,
Body: map[string]string{"error": "User not found"},
})
client := NewUserServiceClient("http://localhost:9998")
_, err := client.GetUser(999)
if err == nil {
t.Error("expected error, got nil")
}
}
7. TypeScript/Node.js로 모의 서버 구현
7.1 Express.js + TypeScript 모의 서버
// mock-server/index.ts
import express, { Request, Response, NextFunction } from 'express'
import fs from 'fs'
import path from 'path'
interface StubDefinition {
method: string
path: string
statusCode: number
body?: unknown
headers?: Record<string, string>
delay?: number
}
const app = express()
app.use(express.json())
const stubs: Map<string, StubDefinition> = new Map()
const requestLog: Array<{ method: string; path: string; timestamp: string }> = []
// 요청 로깅 미들웨어
app.use((req: Request, res: Response, next: NextFunction) => {
requestLog.push({
method: req.method,
path: req.path,
timestamp: new Date().toISOString(),
})
next()
})
// 관리 API
app.post('/__admin/stubs', (req: Request, res: Response) => {
const stub: StubDefinition = req.body
const key = `${stub.method}:${stub.path}`
stubs.set(key, stub)
res.status(201).json({ message: 'Stub registered', key })
})
app.delete('/__admin/stubs', (req: Request, res: Response) => {
stubs.clear()
requestLog.length = 0
res.status(200).json({ message: 'All stubs cleared' })
})
app.get('/__admin/calls', (req: Request, res: Response) => {
res.json(requestLog)
})
// 동적 응답 핸들러
app.use(async (req: Request, res: Response) => {
const key = `${req.method}:${req.path}`
const stub = stubs.get(key)
if (!stub) {
return res.status(404).json({
error: 'No stub found',
request: { method: req.method, path: req.path },
})
}
// 응답 지연
if (stub.delay && stub.delay > 0) {
await new Promise((resolve) => setTimeout(resolve, stub.delay))
}
// 헤더 설정
if (stub.headers) {
Object.entries(stub.headers).forEach(([key, value]) => {
res.setHeader(key, value)
})
}
res.status(stub.statusCode).json(stub.body)
})
const PORT = process.env.MOCK_PORT ?? '3001'
app.listen(PORT, () => {
console.log(`Mock server running on port ${PORT}`)
})
export { app }
7.2 파일 기반 응답 라우팅
// 응답 파일을 디렉토리 구조로 관리
// responses/
// GET/
// api/
// users/
// index.json -> GET /api/users
// 1.json -> GET /api/users/1
// POST/
// api/
// users/
// index.json -> POST /api/users (응답 템플릿)
app.use((req: Request, res: Response) => {
const responsePath = path.join(__dirname, 'responses', req.method, req.path, 'index.json')
if (fs.existsSync(responsePath)) {
const responseData = JSON.parse(fs.readFileSync(responsePath, 'utf-8'))
res.json(responseData)
} else {
res.status(404).json({ error: 'No response file found' })
}
})
7.3 미들웨어로 지연, 에러 주입
// chaos-middleware.ts - 혼돈 엔지니어링을 위한 미들웨어
interface ChaosConfig {
errorRate: number // 0.0 ~ 1.0
minDelay: number // ms
maxDelay: number // ms
}
let chaosConfig: ChaosConfig = {
errorRate: 0,
minDelay: 0,
maxDelay: 0,
}
export const chaosMiddleware = async (req: Request, res: Response, next: NextFunction) => {
// 응답 지연
const delay = chaosConfig.minDelay + Math.random() * (chaosConfig.maxDelay - chaosConfig.minDelay)
if (delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
// 무작위 에러 주입
if (Math.random() < chaosConfig.errorRate) {
return res.status(500).json({
error: 'Chaos monkey strikes!',
timestamp: new Date().toISOString(),
})
}
next()
}
// 설정 변경 API
app.post('/__admin/chaos', (req: Request, res: Response) => {
chaosConfig = { ...chaosConfig, ...req.body }
res.json({ message: 'Chaos config updated', config: chaosConfig })
})
8. Docker Compose로 모의 서버 환경 구성
8.1 docker-compose.yml 예시
version: '3.8'
services:
# json-server 모의 서버
json-server:
image: clue/json-server
volumes:
- ./mock-data/db.json:/data/db.json
- ./mock-data/routes.json:/data/routes.json
ports:
- '3001:80'
command: --watch db.json --routes routes.json
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://localhost/users']
interval: 10s
timeout: 5s
retries: 3
# WireMock 서버
wiremock:
image: wiremock/wiremock:latest
ports:
- '8080:8080'
volumes:
- ./wiremock/mappings:/home/wiremock/mappings
- ./wiremock/__files:/home/wiremock/__files
command:
- '--verbose'
- '--global-response-templating'
healthcheck:
test: ['CMD', 'wget', '-qO-', 'http://localhost:8080/__admin/health']
interval: 10s
timeout: 5s
retries: 3
# 실제 애플리케이션 (모의 서버를 의존성으로 사용)
app:
build: .
ports:
- '8000:8000'
environment:
- USER_SERVICE_URL=http://json-server:80
- PAYMENT_SERVICE_URL=http://wiremock:8080
depends_on:
json-server:
condition: service_healthy
wiremock:
condition: service_healthy
# E2E 테스트 실행
e2e-tests:
build:
context: .
dockerfile: Dockerfile.test
environment:
- APP_URL=http://app:8000
depends_on:
- app
command: npm run test:e2e
profiles:
- test
8.2 테스트 실행 스크립트
#!/bin/bash
# run-tests.sh
echo "모의 서버 환경 시작..."
docker-compose up -d json-server wiremock
echo "헬스체크 대기..."
docker-compose exec json-server wget -qO- http://localhost/users
docker-compose exec wiremock wget -qO- http://localhost:8080/__admin/health
echo "애플리케이션 시작..."
docker-compose up -d app
echo "E2E 테스트 실행..."
docker-compose run --rm e2e-tests
echo "정리..."
docker-compose down
8.3 모의 데이터 초기화 스크립트
// scripts/setup-mock-data.ts
const WIREMOCK_URL = 'http://localhost:8080/__admin/mappings'
const stubs = [
{
request: {
method: 'POST',
url: '/api/payment/charge',
},
response: {
status: 200,
headers: { 'Content-Type': 'application/json' },
jsonBody: {
paymentId: 'TEST-PAYMENT-001',
status: 'SUCCESS',
amount: 10000,
},
},
},
{
request: {
method: 'POST',
url: '/api/payment/charge',
headers: {
'X-Simulate-Failure': { equalTo: 'true' },
},
},
response: {
status: 402,
headers: { 'Content-Type': 'application/json' },
jsonBody: {
error: 'PAYMENT_DECLINED',
message: '카드가 거절되었습니다',
},
},
},
]
async function setupMockData() {
for (const stub of stubs) {
const response = await fetch(WIREMOCK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(stub),
})
console.log(`Stub registered: ${response.status}`)
}
}
setupMockData().catch(console.error)
9. 퀴즈
퀴즈 1: MSW(Mock Service Worker)가 다른 모킹 방법과 다른 핵심적인 차이점은 무엇인가요?
정답: MSW는 Service Worker를 활용해 네트워크 레벨에서 요청을 가로채기 때문에, 애플리케이션 코드를 수정하지 않고도 실제 네트워크 요청을 인터셉트할 수 있습니다. 또한 브라우저와 Node.js 양쪽에서 동일한 핸들러를 재사용할 수 있습니다.
설명: axios-mock-adapter나 fetch-mock 같은 라이브러리는 특정 HTTP 클라이언트 라이브러리를 모킹하므로, HTTP 클라이언트가 바뀌면 모킹 코드도 변경해야 합니다. MSW는 실제 HTTP 스택을 통해 요청이 발생하므로 어떤 HTTP 라이브러리를 사용해도 동일하게 동작합니다.
퀴즈 2: json-server의 가장 큰 제약 사항은 무엇이고, 어떻게 극복할 수 있나요?
정답: json-server의 가장 큰 제약 사항은 복잡한 비즈니스 로직 구현이 어렵다는 점입니다. 단순 CRUD 외의 커스텀 응답 로직이 필요할 때 한계가 있습니다.
설명: 이를 극복하려면 json-server를 라이브러리로 사용하여 커스텀 라우터를 추가(server.post('/auth/login', ...))하거나, 미들웨어를 작성하여 복잡한 로직을 처리할 수 있습니다. 더 복잡한 시나리오가 필요하다면 WireMock이나 직접 구현한 모의 서버로 전환하는 것이 좋습니다.
퀴즈 3: WireMock의 Scenarios 기능은 어떤 상황에서 유용한가요?
정답: Scenarios는 상태에 따라 다른 응답을 반환해야 하는 워크플로우 테스트에 유용합니다. 예를 들어 주문 상태(결제 대기 → 결제 완료 → 배송 중 → 배송 완료)나 재시도 로직(첫 번째 요청은 실패, 두 번째는 성공) 테스트에 적합합니다.
설명: 일반적인 Stub은 항상 동일한 응답을 반환하지만, Scenario를 사용하면 요청이 들어올 때마다 상태가 전이되며 각 상태에 맞는 응답을 반환합니다. 이는 멱등성이 없는 API나 상태 기반 워크플로우를 테스트할 때 필수적입니다.
퀴즈 4: Prism을 사용할 때의 주요 장점은 무엇인가요?
정답: Prism의 주요 장점은 OpenAPI 스펙이 있으면 별도의 모킹 코드 없이 자동으로 모의 서버를 생성한다는 점입니다. 또한 Validation 모드에서 실제 API 요청/응답을 스펙과 대조하여 계약 준수 여부를 자동으로 검증할 수 있습니다.
설명: API 스펙 우선 개발(API-First Design) 방식에서 특히 유용합니다. 백엔드 구현 전에 API 스펙을 먼저 정의하면, 프론트엔드는 Prism으로 즉시 개발을 시작할 수 있고, 백엔드가 완성된 후에는 Validation 모드로 스펙 준수 여부를 검증할 수 있습니다.
퀴즈 5: 모의 서버를 사용할 때 "계약 테스트(Contract Testing)"와 결합해야 하는 이유는 무엇인가요?
정답: 모의 서버는 실제 API와 동기화되지 않을 위험이 있습니다. 계약 테스트를 결합하면 모의 서버가 실제 API와 일치하는지 자동으로 검증할 수 있어 "모의 서버와 실제 API가 달라서 발생하는 버그"를 방지할 수 있습니다.
설명: 모의 서버를 사용해 개발했는데 실제 API가 변경되면, 개발 환경에서는 테스트가 통과하지만 실제 환경에서는 실패하는 문제가 발생할 수 있습니다. Pact와 같은 계약 테스트 도구를 사용하면 Consumer(프론트엔드)가 기대하는 API 계약을 Provider(백엔드)가 준수하는지 자동으로 검증할 수 있습니다.