Skip to content

필사 모드: 모의 서버(Mock Server) 구축 완벽 가이드

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

개요

현대 소프트웨어 개발에서 프론트엔드와 백엔드는 대부분 별도 팀이 동시에 작업합니다. 백엔드 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

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 핸들러

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 동적 응답 (요청 파라미터 활용)

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 에러 응답 시나리오

// 에러 시나리오를 동적으로 제어하는 패턴

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

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(

)

})

3.7 Node.js 환경 설정 (테스트)

// src/mocks/server.ts

export const server = setupServer(...handlers)

// jest.setup.ts

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))

afterEach(() => server.resetHandlers())

afterAll(() => server.close())

// 테스트에서 핸들러 오버라이드

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'

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

"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

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. 퀴즈

**정답**: MSW는 **Service Worker를 활용해 네트워크 레벨에서 요청을 가로채기** 때문에, 애플리케이션 코드를 수정하지 않고도 실제 네트워크 요청을 인터셉트할 수 있습니다. 또한 브라우저와 Node.js 양쪽에서 동일한 핸들러를 재사용할 수 있습니다.

**설명**: axios-mock-adapter나 fetch-mock 같은 라이브러리는 특정 HTTP 클라이언트 라이브러리를 모킹하므로, HTTP 클라이언트가 바뀌면 모킹 코드도 변경해야 합니다. MSW는 실제 HTTP 스택을 통해 요청이 발생하므로 어떤 HTTP 라이브러리를 사용해도 동일하게 동작합니다.

**정답**: json-server의 가장 큰 제약 사항은 **복잡한 비즈니스 로직 구현이 어렵다**는 점입니다. 단순 CRUD 외의 커스텀 응답 로직이 필요할 때 한계가 있습니다.

**설명**: 이를 극복하려면 json-server를 라이브러리로 사용하여 커스텀 라우터를 추가(`server.post('/auth/login', ...)`)하거나, 미들웨어를 작성하여 복잡한 로직을 처리할 수 있습니다. 더 복잡한 시나리오가 필요하다면 WireMock이나 직접 구현한 모의 서버로 전환하는 것이 좋습니다.

**정답**: Scenarios는 **상태에 따라 다른 응답을 반환해야 하는 워크플로우 테스트**에 유용합니다. 예를 들어 주문 상태(결제 대기 → 결제 완료 → 배송 중 → 배송 완료)나 재시도 로직(첫 번째 요청은 실패, 두 번째는 성공) 테스트에 적합합니다.

**설명**: 일반적인 Stub은 항상 동일한 응답을 반환하지만, Scenario를 사용하면 요청이 들어올 때마다 상태가 전이되며 각 상태에 맞는 응답을 반환합니다. 이는 멱등성이 없는 API나 상태 기반 워크플로우를 테스트할 때 필수적입니다.

**정답**: Prism의 주요 장점은 **OpenAPI 스펙이 있으면 별도의 모킹 코드 없이 자동으로 모의 서버를 생성**한다는 점입니다. 또한 Validation 모드에서 실제 API 요청/응답을 스펙과 대조하여 계약 준수 여부를 자동으로 검증할 수 있습니다.

**설명**: API 스펙 우선 개발(API-First Design) 방식에서 특히 유용합니다. 백엔드 구현 전에 API 스펙을 먼저 정의하면, 프론트엔드는 Prism으로 즉시 개발을 시작할 수 있고, 백엔드가 완성된 후에는 Validation 모드로 스펙 준수 여부를 검증할 수 있습니다.

**정답**: 모의 서버는 실제 API와 동기화되지 않을 위험이 있습니다. **계약 테스트를 결합하면 모의 서버가 실제 API와 일치하는지 자동으로 검증**할 수 있어 "모의 서버와 실제 API가 달라서 발생하는 버그"를 방지할 수 있습니다.

**설명**: 모의 서버를 사용해 개발했는데 실제 API가 변경되면, 개발 환경에서는 테스트가 통과하지만 실제 환경에서는 실패하는 문제가 발생할 수 있습니다. Pact와 같은 계약 테스트 도구를 사용하면 Consumer(프론트엔드)가 기대하는 API 계약을 Provider(백엔드)가 준수하는지 자동으로 검증할 수 있습니다.

현재 단락 (1/859)

현대 소프트웨어 개발에서 프론트엔드와 백엔드는 대부분 별도 팀이 동시에 작업합니다. 백엔드 API가 완성될 때까지 프론트엔드 개발이 블로킹되거나, 외부 서드파티 API에 의존하는 ...

작성 글자: 0원문 글자: 20,278작성 단락: 0/859