Skip to content

필사 모드: 백엔드 성능 엔지니어링 완전 가이드 2025: 프로파일링, 부하 테스트, 병목 분석, 최적화

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

목차

1. 성능 엔지니어링 마인드셋

1.1 측정 먼저, 최적화는 나중에

성능 엔지니어링의 황금률은 "추측하지 말고, 측정하라"입니다. 직감에 의한 최적화는 대부분 잘못된 곳에 시간을 낭비합니다.

**성능 최적화의 3단계**:

1. **측정(Measure)**: 현재 성능을 정량적으로 측정

2. **분석(Analyze)**: 병목 지점을 정확히 식별

3. **최적화(Optimize)**: 가장 영향력 큰 병목부터 해결

1.2 암달의 법칙(Amdahl's Law)

시스템 전체 성능 향상은 개선 가능한 부분의 비율에 의해 제한됩니다.

전체 속도 향상 = 1 / ((1 - P) + P / S)

P = 개선 가능한 부분의 비율

S = 해당 부분의 속도 향상 배수

예시: 전체의 20%를 차지하는 코드를 10배 빠르게 만들면

= 1 / ((1 - 0.2) + 0.2 / 10)

= 1 / (0.8 + 0.02)

= 1.22배 (22% 향상)

반면, 전체의 80%를 차지하는 코드를 2배 빠르게 만들면

= 1 / ((1 - 0.8) + 0.8 / 2)

= 1 / (0.2 + 0.4)

= 1.67배 (67% 향상)

핵심: 작은 부분을 극적으로 개선하는 것보다, 큰 부분을 적당히 개선하는 것이 효과적입니다.

1.3 성능 예산(Performance Budget)

성능 예산 정의 예시

performance_budget:

api_endpoints:

p50_latency_ms: 50

p95_latency_ms: 200

p99_latency_ms: 500

max_latency_ms: 2000

error_rate_percent: 0.1

throughput_rps: 1000

database:

query_p95_ms: 50

query_p99_ms: 200

connection_pool_utilization: 70

slow_query_threshold_ms: 100

external_services:

p95_latency_ms: 300

timeout_ms: 5000

retry_count: 3

circuit_breaker_threshold: 50

2. 프로파일링

2.1 CPU 프로파일링과 Flame Graph

Flame Graph는 CPU 시간이 어디에 소비되는지를 시각적으로 보여주는 강력한 도구입니다.

**Node.js CPU 프로파일링**:

// Node.js - 내장 프로파일러 사용

// 실행: node --prof app.js

// 분석: node --prof-process isolate-*.log > profile.txt

// 또는 v8-profiler-next 사용

const v8Profiler = require('v8-profiler-next');

function startProfiling(durationMs = 30000) {

const title = `cpu-profile-${Date.now()}`;

v8Profiler.startProfiling(title, true);

setTimeout(() => {

const profile = v8Profiler.stopProfiling(title);

profile.export((error, result) => {

if (!error) {

require('fs').writeFileSync(

`./profiles/${title}.cpuprofile`,

result

);

}

profile.delete();

});

}, durationMs);

}

// 미들웨어로 특정 요청 프로파일링

function profilingMiddleware(req, res, next) {

if (req.headers['x-profile'] !== 'true') {

return next();

}

const title = `req-${req.method}-${req.path}-${Date.now()}`;

v8Profiler.startProfiling(title, true);

const originalEnd = res.end;

res.end = function (...args) {

const profile = v8Profiler.stopProfiling(title);

profile.export((error, result) => {

if (!error) {

require('fs').writeFileSync(

`./profiles/${title}.cpuprofile`,

result

);

}

profile.delete();

});

originalEnd.apply(res, args);

};

next();

}

**Go CPU 프로파일링**:

package main

"net/http"

_ "net/http/pprof"

"runtime"

)

func main() {

// pprof 엔드포인트 활성화

go func() {

http.ListenAndServe("localhost:6060", nil)

}()

// CPU 프로파일 수집: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

// Flame Graph 생성: go tool pprof -http=:8080 profile.pb.gz

// 또는 프로그래밍 방식으로

// runtime.SetCPUProfileRate(100)

// pprof.StartCPUProfile(f)

// defer pprof.StopCPUProfile()

runtime.SetBlockProfileRate(1)

runtime.SetMutexProfileFraction(1)

// 애플리케이션 로직

startServer()

}

**Python CPU 프로파일링**:

from pyinstrument import Profiler

cProfile 사용

def profile_with_cprofile(func):

def wrapper(*args, **kwargs):

profiler = cProfile.Profile()

profiler.enable()

result = func(*args, **kwargs)

profiler.disable()

stats = pstats.Stats(profiler)

stats.sort_stats('cumulative')

stats.print_stats(20) # 상위 20개 함수

return result

return wrapper

pyinstrument 사용 (더 읽기 쉬운 출력)

def profile_with_pyinstrument(func):

def wrapper(*args, **kwargs):

profiler = Profiler()

profiler.start()

result = func(*args, **kwargs)

profiler.stop()

print(profiler.output_text(unicode=True))

return result

return wrapper

Django 미들웨어

class ProfilingMiddleware:

def __init__(self, get_response):

self.get_response = get_response

def __call__(self, request):

if request.META.get('HTTP_X_PROFILE') == 'true':

profiler = Profiler()

profiler.start()

response = self.get_response(request)

profiler.stop()

response['X-Profile-Duration'] = str(profiler.last_session.duration)

HTML 프로파일 결과를 파일로 저장

profiler.open_in_browser()

return response

return self.get_response(request)

2.2 메모리 프로파일링

// Node.js 힙 스냅샷

const v8 = require('v8');

const fs = require('fs');

function takeHeapSnapshot() {

const snapshotStream = v8.writeHeapSnapshot();

console.log(`Heap snapshot written to: ${snapshotStream}`);

return snapshotStream;

}

// 메모리 사용량 모니터링

function monitorMemory(intervalMs = 5000) {

setInterval(() => {

const usage = process.memoryUsage();

console.log({

rss_mb: Math.round(usage.rss / 1024 / 1024),

heapTotal_mb: Math.round(usage.heapTotal / 1024 / 1024),

heapUsed_mb: Math.round(usage.heapUsed / 1024 / 1024),

external_mb: Math.round(usage.external / 1024 / 1024),

arrayBuffers_mb: Math.round(usage.arrayBuffers / 1024 / 1024)

});

}, intervalMs);

}

// 메모리 누수 감지 패턴

class MemoryLeakDetector {

constructor(options = {}) {

this.samples = [];

this.maxSamples = options.maxSamples || 60;

this.threshold = options.thresholdMB || 50;

}

sample() {

const usage = process.memoryUsage();

this.samples.push({

timestamp: Date.now(),

heapUsed: usage.heapUsed

});

if (this.samples.length > this.maxSamples) {

this.samples.shift();

}

return this.detectLeak();

}

detectLeak() {

if (this.samples.length < 10) return null;

const first = this.samples[0].heapUsed;

const last = this.samples[this.samples.length - 1].heapUsed;

const diffMB = (last - first) / 1024 / 1024;

// 지속적인 메모리 증가 패턴 감지

let increasing = 0;

for (let i = 1; i < this.samples.length; i++) {

if (this.samples[i].heapUsed > this.samples[i - 1].heapUsed) {

increasing++;

}

}

const increaseRatio = increasing / (this.samples.length - 1);

if (diffMB > this.threshold && increaseRatio > 0.7) {

return {

suspected: true,

growthMB: diffMB.toFixed(2),

increaseRatio: increaseRatio.toFixed(2),

duration: this.samples[this.samples.length - 1].timestamp - this.samples[0].timestamp

};

}

return null;

}

}

2.3 I/O 프로파일링

Python - I/O 프로파일링

from contextlib import contextmanager

logger = logging.getLogger('io_profiler')

class IOProfiler:

"""I/O 작업 시간 측정 데코레이터 및 컨텍스트 매니저"""

_stats = {}

@classmethod

def track(cls, operation_name):

def decorator(func):

@functools.wraps(func)

async def async_wrapper(*args, **kwargs):

start = time.perf_counter()

try:

result = await func(*args, **kwargs)

duration = time.perf_counter() - start

cls._record(operation_name, duration, success=True)

return result

except Exception as e:

duration = time.perf_counter() - start

cls._record(operation_name, duration, success=False)

raise

@functools.wraps(func)

def sync_wrapper(*args, **kwargs):

start = time.perf_counter()

try:

result = func(*args, **kwargs)

duration = time.perf_counter() - start

cls._record(operation_name, duration, success=True)

return result

except Exception as e:

duration = time.perf_counter() - start

cls._record(operation_name, duration, success=False)

raise

if asyncio.iscoroutinefunction(func):

return async_wrapper

return sync_wrapper

return decorator

@classmethod

def _record(cls, name, duration, success):

if name not in cls._stats:

cls._stats[name] = {

'count': 0, 'total_time': 0,

'min_time': float('inf'), 'max_time': 0,

'errors': 0

}

stats = cls._stats[name]

stats['count'] += 1

stats['total_time'] += duration

stats['min_time'] = min(stats['min_time'], duration)

stats['max_time'] = max(stats['max_time'], duration)

if not success:

stats['errors'] += 1

@classmethod

def report(cls):

for name, stats in sorted(cls._stats.items()):

avg = stats['total_time'] / stats['count'] if stats['count'] else 0

logger.info(

f"{name}: count={stats['count']}, "

f"avg={avg*1000:.1f}ms, "

f"min={stats['min_time']*1000:.1f}ms, "

f"max={stats['max_time']*1000:.1f}ms, "

f"errors={stats['errors']}"

)

사용 예시

class UserRepository:

@IOProfiler.track('db.users.find_by_id')

async def find_by_id(self, user_id):

return await self.db.users.find_one({"_id": user_id})

@IOProfiler.track('db.users.search')

async def search(self, query, limit=20):

return await self.db.users.find(query).limit(limit).to_list(limit)

class ExternalAPIClient:

@IOProfiler.track('api.payment.charge')

async def charge(self, amount, token):

async with self.session.post('/charge', json={"amount": amount, "token": token}) as resp:

return await resp.json()

3. 부하 테스트(Load Testing)

3.1 부하 테스트 도구 비교

| 도구 | 언어 | 프로토콜 | 강점 | 약점 |

|------|------|----------|------|------|

| k6 | JavaScript | HTTP, WebSocket, gRPC | 개발자 친화적, CI/CD 통합 | 브라우저 테스트 제한적 |

| Artillery | JavaScript | HTTP, WebSocket, Socket.io | 설정 기반, 확장성 | 복잡한 시나리오 어려움 |

| Locust | Python | HTTP | Python 스크립트, 분산 | 프로토콜 제한적 |

| Gatling | Scala/Java | HTTP, WebSocket | 상세 리포트, JVM 성능 | 학습 곡선 |

| JMeter | Java | 다양함 | GUI, 다양한 프로토콜 | 리소스 소비 큼, 구식 |

3.2 k6 스크립트 예시

// 커스텀 메트릭

const errorRate = new Rate('errors');

const apiDuration = new Trend('api_duration', true);

const requestCount = new Counter('requests');

// 테스트 옵션

export const options = {

scenarios: {

// 시나리오 1: 일반 부하 테스트

normal_load: {

executor: 'ramping-vus',

startVUs: 0,

stages: [

{ duration: '2m', target: 50 }, // 2분간 50 VU까지 증가

{ duration: '5m', target: 50 }, // 5분간 50 VU 유지

{ duration: '2m', target: 100 }, // 2분간 100 VU까지 증가

{ duration: '5m', target: 100 }, // 5분간 100 VU 유지

{ duration: '2m', target: 0 }, // 2분간 0으로 감소

],

},

// 시나리오 2: 스파이크 테스트

spike_test: {

executor: 'ramping-vus',

startVUs: 0,

startTime: '16m',

stages: [

{ duration: '10s', target: 500 }, // 급격한 스파이크

{ duration: '1m', target: 500 }, // 유지

{ duration: '10s', target: 0 }, // 급격한 감소

],

},

},

thresholds: {

http_req_duration: ['p(95)<200', 'p(99)<500'],

errors: ['rate<0.01'],

http_req_failed: ['rate<0.01'],

},

};

const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';

export default function () {

const authToken = login();

group('API Operations', () => {

group('List Products', () => {

const res = http.get(`${BASE_URL}/api/products?page=1&limit=20`, {

headers: { Authorization: `Bearer ${authToken}` },

tags: { name: 'GET /api/products' },

});

check(res, {

'status is 200': (r) => r.status === 200,

'response time OK': (r) => r.timings.duration < 200,

'has products': (r) => JSON.parse(r.body).data.length > 0,

});

errorRate.add(res.status !== 200);

apiDuration.add(res.timings.duration);

requestCount.add(1);

});

group('Get Product Detail', () => {

const productId = Math.floor(Math.random() * 1000) + 1;

const res = http.get(`${BASE_URL}/api/products/${productId}`, {

headers: { Authorization: `Bearer ${authToken}` },

tags: { name: 'GET /api/products/:id' },

});

check(res, {

'status is 200': (r) => r.status === 200,

'has product data': (r) => {

const body = JSON.parse(r.body);

return body.data && body.data.id;

},

});

errorRate.add(res.status !== 200);

apiDuration.add(res.timings.duration);

});

group('Create Order', () => {

const payload = JSON.stringify({

productId: Math.floor(Math.random() * 1000) + 1,

quantity: Math.floor(Math.random() * 5) + 1,

shippingAddress: '123 Test Street',

});

const res = http.post(`${BASE_URL}/api/orders`, payload, {

headers: {

Authorization: `Bearer ${authToken}`,

'Content-Type': 'application/json',

},

tags: { name: 'POST /api/orders' },

});

check(res, {

'order created': (r) => r.status === 201,

'has order id': (r) => JSON.parse(r.body).data.orderId,

});

errorRate.add(res.status !== 201);

apiDuration.add(res.timings.duration);

});

});

sleep(Math.random() * 3 + 1); // 1-4초 사이 대기

}

function login() {

const res = http.post(`${BASE_URL}/api/auth/login`, JSON.stringify({

email: `user${__VU}@test.com`,

password: 'testpassword',

}), {

headers: { 'Content-Type': 'application/json' },

tags: { name: 'POST /api/auth/login' },

});

return res.status === 200 ? JSON.parse(res.body).token : '';

}

3.3 Artillery 설정 예시

artillery-config.yml

config:

target: "http://localhost:3000"

phases:

- duration: 120

arrivalRate: 10

name: "Warm up"

- duration: 300

arrivalRate: 50

name: "Normal load"

- duration: 120

arrivalRate: 100

name: "Peak load"

defaults:

headers:

Content-Type: "application/json"

plugins:

expect: {}

metrics-by-endpoint: {}

ensure:

thresholds:

- http.response_time.p95: 200

- http.response_time.p99: 500

scenarios:

- name: "User browsing flow"

weight: 70

flow:

- post:

url: "/api/auth/login"

json:

email: "user@test.com"

password: "password123"

capture:

- json: "$.token"

as: "authToken"

expect:

- statusCode: 200

- get:

url: "/api/products?page=1&limit=20"

headers:

Authorization: "Bearer {{ authToken }}"

expect:

- statusCode: 200

- hasProperty: "data"

- think: 2

- get:

url: "/api/products/{{ $randomNumber(1, 1000) }}"

headers:

Authorization: "Bearer {{ authToken }}"

expect:

- statusCode: 200

- name: "Order creation flow"

weight: 30

flow:

- post:

url: "/api/auth/login"

json:

email: "buyer@test.com"

password: "password123"

capture:

- json: "$.token"

as: "authToken"

- post:

url: "/api/orders"

headers:

Authorization: "Bearer {{ authToken }}"

json:

productId: "{{ $randomNumber(1, 100) }}"

quantity: "{{ $randomNumber(1, 5) }}"

expect:

- statusCode: 201

3.4 부하 테스트 유형

| 유형 | 목적 | VU 패턴 | 기간 |

|------|------|---------|------|

| Smoke | 기본 동작 확인 | 1-5 | 1-5분 |

| Load | 예상 트래픽 처리 확인 | 예상치 | 15-60분 |

| Stress | 한계점 탐색 | 예상치 초과 | 30-60분 |

| Spike | 급격한 트래픽 대응 | 갑작스런 급증 | 5-10분 |

| Soak | 장시간 안정성 확인 | 일정 수준 유지 | 2-24시간 |

| Breakpoint | 시스템 파괴점 탐색 | 지속적 증가 | 가변적 |

4. 핵심 성능 메트릭

4.1 RED Method

RED Method 모니터링 구현

from prometheus_client import Counter, Histogram, Gauge

Rate: 초당 요청 수

request_count = Counter(

'http_requests_total',

'Total HTTP requests',

['method', 'endpoint', 'status']

)

Errors: 에러 비율

error_count = Counter(

'http_errors_total',

'Total HTTP errors',

['method', 'endpoint', 'error_type']

)

Duration: 응답 시간 분포

request_duration = Histogram(

'http_request_duration_seconds',

'HTTP request duration',

['method', 'endpoint'],

buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]

)

미들웨어 구현

class REDMetricsMiddleware:

def __init__(self, app):

self.app = app

async def __call__(self, scope, receive, send):

if scope['type'] != 'http':

return await self.app(scope, receive, send)

method = scope.get('method', 'UNKNOWN')

path = scope.get('path', '/')

status_code = 500

start = time.perf_counter()

try:

응답 상태 코드 캡처

async def send_wrapper(message):

nonlocal status_code

if message['type'] == 'http.response.start':

status_code = message['status']

await send(message)

await self.app(scope, receive, send_wrapper)

except Exception as e:

error_count.labels(method=method, endpoint=path, error_type=type(e).__name__).inc()

raise

finally:

duration = time.perf_counter() - start

request_count.labels(method=method, endpoint=path, status=str(status_code)).inc()

request_duration.labels(method=method, endpoint=path).observe(duration)

if status_code >= 400:

error_count.labels(method=method, endpoint=path, error_type=f'http_{status_code}').inc()

4.2 지연시간 백분위(Percentiles)

평균(Mean) p50 p95 p99 p99.9 Max

사용자 영향도 낮음 중간 높음 높음 매우높음 극단적

p50 (중앙값): 50%의 요청이 이 시간 이내에 완료

p95: 95%의 요청이 이 시간 이내에 완료 (20개 중 1개가 이보다 느림)

p99: 99%의 요청이 이 시간 이내에 완료 (100개 중 1개가 이보다 느림)

왜 평균은 위험한가?

- 평균 50ms여도 p99가 5000ms일 수 있음

- 매 100번째 요청마다 사용자가 5초를 대기

- 헤비 유저일수록 높은 백분위에 노출될 확률 증가

5. 일반적인 병목 지점

5.1 데이터베이스 병목

**N+1 쿼리 문제**:

BAD: N+1 쿼리 - 주문 100개면 101번 쿼리 실행

orders = Order.objects.all()[:100]

for order in orders:

각 주문마다 별도 쿼리로 사용자 정보 조회

print(f"Order {order.id} by {order.user.name}")

GOOD: Eager loading - 2번의 쿼리로 해결

orders = Order.objects.select_related('user').all()[:100]

for order in orders:

print(f"Order {order.id} by {order.user.name}")

GOOD: Prefetch (M:N 관계)

orders = Order.objects.prefetch_related('items__product').all()[:100]

for order in orders:

for item in order.items.all():

print(f" - {item.product.name}")

// Node.js + Prisma - N+1 해결

// BAD: N+1

const orders = await prisma.order.findMany({ take: 100 });

for (const order of orders) {

const user = await prisma.user.findUnique({

where: { id: order.userId }

});

}

// GOOD: Include (Join)

const orders = await prisma.order.findMany({

take: 100,

include: {

user: true,

items: {

include: { product: true }

}

}

});

// GOOD: DataLoader 패턴

const DataLoader = require('dataloader');

const userLoader = new DataLoader(async (userIds) => {

const users = await prisma.user.findMany({

where: { id: { in: [...userIds] } }

});

const userMap = new Map(users.map(u => [u.id, u]));

return userIds.map(id => userMap.get(id));

});

// 여러 번 호출해도 자동으로 배치 처리

const user1 = await userLoader.load(1);

const user2 = await userLoader.load(2);

5.2 인덱스 부재와 전체 테이블 스캔

-- 느린 쿼리 탐지 (PostgreSQL)

SELECT

query,

calls,

mean_exec_time,

total_exec_time,

rows

FROM pg_stat_statements

ORDER BY mean_exec_time DESC

LIMIT 20;

-- 실행 계획 분석

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)

SELECT o.*, u.name

FROM orders o

JOIN users u ON o.user_id = u.id

WHERE o.status = 'pending'

AND o.created_at > NOW() - INTERVAL '7 days'

ORDER BY o.created_at DESC

LIMIT 50;

-- 복합 인덱스 생성 (쿼리 패턴에 맞게)

CREATE INDEX CONCURRENTLY idx_orders_status_created

ON orders (status, created_at DESC)

WHERE status IN ('pending', 'processing');

-- 인덱스 사용률 확인

SELECT

schemaname,

tablename,

indexname,

idx_scan,

idx_tup_read,

idx_tup_fetch

FROM pg_stat_user_indexes

ORDER BY idx_scan ASC;

5.3 커넥션 풀 고갈

// HikariCP 최적 설정 (Java/Spring Boot)

// application.yml

/*

spring:

datasource:

hikari:

maximum-pool-size: 20

minimum-idle: 5

idle-timeout: 300000

max-lifetime: 600000

connection-timeout: 30000

leak-detection-threshold: 60000

pool-name: "MainPool"

*/

// 커넥션 풀 모니터링

public class ConnectionPoolMonitor {

private final HikariDataSource dataSource;

public PoolStats getStats() {

HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();

return new PoolStats(

poolBean.getTotalConnections(),

poolBean.getActiveConnections(),

poolBean.getIdleConnections(),

poolBean.getThreadsAwaitingConnection()

);

}

public void logWarningIfNeeded() {

PoolStats stats = getStats();

double utilization = (double) stats.active / stats.total;

if (utilization > 0.8) {

log.warn("Connection pool utilization HIGH: {}% ({}/{})",

Math.round(utilization * 100),

stats.active, stats.total);

}

if (stats.waiting > 0) {

log.error("Threads waiting for connection: {}", stats.waiting);

}

}

}

PgBouncer 설정 (PostgreSQL 커넥션 풀러)

pgbouncer.ini

"""

[databases]

mydb = host=127.0.0.1 port=5432 dbname=mydb

[pgbouncer]

listen_port = 6432

listen_addr = 0.0.0.0

auth_type = md5

auth_file = /etc/pgbouncer/userlist.txt

pool_mode = transaction

default_pool_size = 25

min_pool_size = 5

reserve_pool_size = 5

reserve_pool_timeout = 3

max_client_conn = 1000

max_db_connections = 50

server_idle_timeout = 600

server_lifetime = 3600

client_idle_timeout = 0

log_connections = 1

log_disconnections = 1

log_pooler_errors = 1

stats_period = 60

"""

5.4 잠금 경합(Lock Contention)

// Go - 잠금 경합 프로파일링

package main

"runtime"

"sync"

"time"

)

// BAD: 글로벌 뮤텍스로 전체 맵 잠금

type BadCache struct {

mu sync.Mutex

items map[string]interface{}

}

func (c *BadCache) Get(key string) interface{} {

c.mu.Lock()

defer c.mu.Unlock()

return c.items[key]

}

// GOOD: 샤딩으로 잠금 경합 분산

type ShardedCache struct {

shards [256]shard

shardMask uint8

}

type shard struct {

mu sync.RWMutex

items map[string]interface{}

}

func NewShardedCache() *ShardedCache {

c := &ShardedCache{shardMask: 255}

for i := range c.shards {

c.shards[i].items = make(map[string]interface{})

}

return c

}

func (c *ShardedCache) getShard(key string) *shard {

hash := fnv32(key)

return &c.shards[hash&uint32(c.shardMask)]

}

func (c *ShardedCache) Get(key string) (interface{}, bool) {

s := c.getShard(key)

s.mu.RLock()

defer s.mu.RUnlock()

val, ok := s.items[key]

return val, ok

}

func (c *ShardedCache) Set(key string, value interface{}) {

s := c.getShard(key)

s.mu.Lock()

defer s.mu.Unlock()

s.items[key] = value

}

func fnv32(key string) uint32 {

hash := uint32(2166136261)

for i := 0; i < len(key); i++ {

hash *= 16777619

hash ^= uint32(key[i])

}

return hash

}

6. 데이터베이스 최적화

6.1 쿼리 최적화 전략

-- 1. 서브쿼리를 JOIN으로 변환

-- BAD

SELECT * FROM orders

WHERE user_id IN (SELECT id FROM users WHERE status = 'active');

-- GOOD

SELECT o.* FROM orders o

INNER JOIN users u ON o.user_id = u.id

WHERE u.status = 'active';

-- 2. EXISTS vs IN (대량 데이터)

-- GOOD: EXISTS (서브쿼리 결과가 큰 경우)

SELECT * FROM orders o

WHERE EXISTS (

SELECT 1 FROM users u

WHERE u.id = o.user_id AND u.status = 'active'

);

-- 3. 페이지네이션 최적화

-- BAD: OFFSET 기반 (깊은 페이지에서 느림)

SELECT * FROM products ORDER BY id LIMIT 20 OFFSET 10000;

-- GOOD: 커서 기반 (일정한 성능)

SELECT * FROM products

WHERE id > 10000

ORDER BY id

LIMIT 20;

-- 4. 집계 쿼리 최적화

-- BAD: COUNT(*)를 자주 호출

SELECT COUNT(*) FROM orders WHERE status = 'pending';

-- GOOD: 대략적인 카운트 사용 (PostgreSQL)

SELECT reltuples::bigint AS estimate

FROM pg_class WHERE relname = 'orders';

-- 5. 파티셔닝

CREATE TABLE orders (

id BIGSERIAL,

user_id BIGINT NOT NULL,

status VARCHAR(20) NOT NULL,

created_at TIMESTAMP NOT NULL,

total_amount DECIMAL(10,2)

) PARTITION BY RANGE (created_at);

CREATE TABLE orders_2025_q1 PARTITION OF orders

FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');

CREATE TABLE orders_2025_q2 PARTITION OF orders

FOR VALUES FROM ('2025-04-01') TO ('2025-07-01');

6.2 읽기 복제본(Read Replica)

SQLAlchemy - 읽기/쓰기 분리

from sqlalchemy import create_engine

from sqlalchemy.orm import sessionmaker

class DatabaseRouter:

def __init__(self):

self.writer = create_engine(

'postgresql://writer:pass@primary:5432/mydb',

pool_size=10,

max_overflow=20

)

self.readers = [

create_engine(

f'postgresql://reader:pass@replica{i}:5432/mydb',

pool_size=10,

max_overflow=20

)

for i in range(1, 4) # 3개의 읽기 복제본

]

self._reader_index = 0

def get_writer_session(self):

Session = sessionmaker(bind=self.writer)

return Session()

def get_reader_session(self):

라운드 로빈으로 읽기 복제본 선택

reader = self.readers[self._reader_index % len(self.readers)]

self._reader_index += 1

Session = sessionmaker(bind=reader)

return Session()

사용 예시

db = DatabaseRouter()

쓰기 작업

with db.get_writer_session() as session:

new_order = Order(user_id=1, total=99.99)

session.add(new_order)

session.commit()

읽기 작업 (복제본 사용)

with db.get_reader_session() as session:

orders = session.query(Order).filter_by(status='pending').all()

7. 캐싱 전략

7.1 Cache-Aside 패턴

from functools import wraps

redis_client = redis.Redis(host='localhost', port=6379, db=0)

class CacheAside:

"""Cache-Aside (Lazy Loading) 패턴 구현"""

@staticmethod

def cached(key_prefix, ttl_seconds=300):

def decorator(func):

@wraps(func)

async def wrapper(*args, **kwargs):

캐시 키 생성

cache_key = f"{key_prefix}:{':'.join(str(a) for a in args)}"

1. 캐시에서 조회

cached = redis_client.get(cache_key)

if cached:

return json.loads(cached)

2. 캐시 미스 - DB에서 조회

result = await func(*args, **kwargs)

3. 결과를 캐시에 저장

if result is not None:

redis_client.setex(

cache_key,

ttl_seconds,

json.dumps(result, default=str)

)

return result

return wrapper

return decorator

@staticmethod

def invalidate(key_pattern):

"""패턴 기반 캐시 무효화"""

keys = redis_client.keys(key_pattern)

if keys:

redis_client.delete(*keys)

사용 예시

class ProductService:

@CacheAside.cached('product', ttl_seconds=600)

async def get_product(self, product_id):

return await self.db.products.find_one({"_id": product_id})

@CacheAside.cached('product:list', ttl_seconds=120)

async def list_products(self, category, page):

return await self.db.products.find(

{"category": category}

).skip((page - 1) * 20).limit(20).to_list(20)

async def update_product(self, product_id, data):

await self.db.products.update_one(

{"_id": product_id},

{"$set": data}

)

관련 캐시 무효화

CacheAside.invalidate(f'product:{product_id}')

CacheAside.invalidate('product:list:*')

7.2 Write-Through와 Write-Behind

class WriteThrough:

"""Write-Through: 캐시와 DB를 동시에 업데이트"""

async def update(self, key, value, ttl=300):

1. DB에 쓰기

await self.db.update(key, value)

2. 캐시 업데이트 (DB 쓰기 성공 후)

redis_client.setex(f"wt:{key}", ttl, json.dumps(value, default=str))

async def get(self, key):

캐시에서 조회 (항상 최신 데이터)

cached = redis_client.get(f"wt:{key}")

if cached:

return json.loads(cached)

캐시 미스 시 DB 조회 후 캐시 저장

value = await self.db.get(key)

if value:

redis_client.setex(f"wt:{key}", 300, json.dumps(value, default=str))

return value

class WriteBehind:

"""Write-Behind (Write-Back): 캐시에 먼저 쓰고, 비동기로 DB에 반영"""

def __init__(self):

self.write_queue = asyncio.Queue()

self.batch_size = 100

self.flush_interval = 5 # 초

async def update(self, key, value, ttl=300):

1. 캐시에 즉시 쓰기 (빠른 응답)

redis_client.setex(f"wb:{key}", ttl, json.dumps(value, default=str))

2. 큐에 추가 (비동기 DB 쓰기)

await self.write_queue.put((key, value))

async def flush_worker(self):

"""백그라운드 워커: 큐에서 꺼내서 DB에 배치 쓰기"""

while True:

batch = []

try:

while len(batch) < self.batch_size:

item = await asyncio.wait_for(

self.write_queue.get(),

timeout=self.flush_interval

)

batch.append(item)

except asyncio.TimeoutError:

pass

if batch:

try:

await self.db.bulk_update(batch)

except Exception as e:

실패 시 재시도 큐에 추가

for item in batch:

await self.write_queue.put(item)

await asyncio.sleep(1)

7.3 TTL 전략과 캐시 무효화

다층 TTL 전략

class TieredTTLCache:

TTL_CONFIG = {

자주 변경되는 데이터

'user:session': 1800, # 30분

'cart:items': 900, # 15분

주기적으로 변경되는 데이터

'product:detail': 3600, # 1시간

'product:list': 600, # 10분

'search:results': 300, # 5분

거의 변경되지 않는 데이터

'category:list': 86400, # 24시간

'config:settings': 86400, # 24시간

'static:content': 604800, # 7일

}

@classmethod

def get_ttl(cls, key_type):

return cls.TTL_CONFIG.get(key_type, 300) # 기본 5분

@staticmethod

def stale_while_revalidate(key, ttl, stale_ttl):

"""Stale-While-Revalidate 패턴"""

cached = redis_client.get(key)

if cached:

data = json.loads(cached)

if data['_cached_at'] + ttl > time.time():

return data['value'], False # 신선한 데이터

if data['_cached_at'] + stale_ttl > time.time():

return data['value'], True # 부실하지만 사용 가능

return None, True # 캐시 미스

캐시 워밍(Pre-warming)

class CacheWarmer:

async def warm_popular_products(self):

"""인기 상품 캐시 사전 로딩"""

popular = await self.db.products.find(

{"popular": True}

).limit(100).to_list(100)

pipe = redis_client.pipeline()

for product in popular:

key = f"product:{product['_id']}"

pipe.setex(key, 3600, json.dumps(product, default=str))

pipe.execute()

8. 비동기 처리

8.1 메시지 큐 기반 비동기 처리

Celery를 사용한 비동기 태스크 처리

from celery import Celery, chain, group, chord

app = Celery('tasks', broker='redis://localhost:6379/0')

설정

app.conf.update(

task_serializer='json',

accept_content=['json'],

result_serializer='json',

timezone='UTC',

task_acks_late=True,

worker_prefetch_multiplier=1,

task_reject_on_worker_lost=True,

task_routes={

'tasks.send_email': {'queue': 'email'},

'tasks.process_image': {'queue': 'image'},

'tasks.generate_report': {'queue': 'report'},

}

)

@app.task(bind=True, max_retries=3, default_retry_delay=60)

def send_email(self, to, subject, body):

try:

email_service.send(to=to, subject=subject, body=body)

except Exception as exc:

self.retry(exc=exc)

@app.task(bind=True, max_retries=3)

def process_order(self, order_id):

"""주문 처리 파이프라인"""

try:

order = Order.objects.get(id=order_id)

체인으로 순차 처리

workflow = chain(

validate_inventory.s(order_id),

process_payment.s(order_id),

send_confirmation_email.s(order_id),

update_analytics.s(order_id)

)

workflow.apply_async()

except Exception as exc:

self.retry(exc=exc, countdown=30)

@app.task

def validate_inventory(result, order_id):

재고 확인

order = Order.objects.get(id=order_id)

for item in order.items.all():

if item.product.stock < item.quantity:

raise InsufficientStockError(item.product.name)

return True

@app.task

def bulk_process_orders(order_ids):

"""병렬 배치 처리"""

job = group(process_order.s(oid) for oid in order_ids)

result = job.apply_async()

return result

8.2 이벤트 드리븐 아키텍처

// Node.js - EventEmitter 기반 비동기 처리

const EventEmitter = require('events');

class OrderEventBus extends EventEmitter {

constructor() {

super();

this.setMaxListeners(20);

}

}

const orderBus = new OrderEventBus();

// 이벤트 핸들러 등록 (관심사 분리)

orderBus.on('order.created', async (order) => {

// 재고 업데이트

await inventoryService.decrementStock(order.items);

});

orderBus.on('order.created', async (order) => {

// 확인 이메일 발송

await emailService.sendOrderConfirmation(order);

});

orderBus.on('order.created', async (order) => {

// 분석 데이터 업데이트

await analyticsService.trackOrder(order);

});

orderBus.on('order.created', async (order) => {

// 추천 시스템 업데이트

await recommendationService.recordPurchase(order.userId, order.items);

});

// 주문 생성 시 이벤트 발행

class OrderService {

async createOrder(orderData) {

const order = await this.orderRepo.create(orderData);

// 동기적으로 필수 작업만 수행

// 나머지는 이벤트로 비동기 처리

orderBus.emit('order.created', order);

return order; // 빠르게 응답

}

}

9. 배치 최적화

9.1 벌크 인서트

SQLAlchemy 벌크 인서트 비교

BAD: 하나씩 삽입 (N번의 INSERT)

def insert_one_by_one(session, records):

start = time.time()

for record in records:

session.add(MyModel(**record))

session.commit()

print(f"One by one: {time.time() - start:.2f}s")

GOOD: 벌크 삽입 (1번의 INSERT)

def bulk_insert(session, records):

start = time.time()

session.bulk_insert_mappings(MyModel, records)

session.commit()

print(f"Bulk insert: {time.time() - start:.2f}s")

BETTER: execute_values (PostgreSQL, psycopg2)

def execute_values_insert(conn, records):

start = time.time()

from psycopg2.extras import execute_values

cursor = conn.cursor()

execute_values(

cursor,

"INSERT INTO my_table (col1, col2, col3) VALUES %s",

[(r['col1'], r['col2'], r['col3']) for r in records],

page_size=1000

)

conn.commit()

print(f"execute_values: {time.time() - start:.2f}s")

성능 비교 (10,000건 기준)

One by one: 12.5s

Bulk insert: 0.8s

execute_values: 0.3s

9.2 배치 API 호출

// 외부 API 배치 호출 최적화

class BatchAPIClient {

constructor(options = {}) {

this.batchSize = options.batchSize || 50;

this.concurrency = options.concurrency || 5;

this.retryAttempts = options.retryAttempts || 3;

this.delayBetweenBatches = options.delayMs || 100;

}

async processBatch(items, processFn) {

const results = [];

const errors = [];

// 아이템을 배치로 분할

const batches = [];

for (let i = 0; i < items.length; i += this.batchSize) {

batches.push(items.slice(i, i + this.batchSize));

}

// 동시성 제한하여 배치 처리

for (let i = 0; i < batches.length; i += this.concurrency) {

const concurrentBatches = batches.slice(i, i + this.concurrency);

const batchResults = await Promise.allSettled(

concurrentBatches.map(batch => this.processWithRetry(batch, processFn))

);

for (const result of batchResults) {

if (result.status === 'fulfilled') {

results.push(...result.value);

} else {

errors.push(result.reason);

}

}

// 배치 간 딜레이 (Rate limiting 방지)

if (i + this.concurrency < batches.length) {

await new Promise(r => setTimeout(r, this.delayBetweenBatches));

}

}

return { results, errors, total: items.length, processed: results.length };

}

async processWithRetry(batch, processFn, attempt = 1) {

try {

return await processFn(batch);

} catch (error) {

if (attempt < this.retryAttempts) {

const delay = Math.pow(2, attempt) * 1000; // 지수 백오프

await new Promise(r => setTimeout(r, delay));

return this.processWithRetry(batch, processFn, attempt + 1);

}

throw error;

}

}

}

// 사용 예시

const client = new BatchAPIClient({ batchSize: 100, concurrency: 3 });

const result = await client.processBatch(userIds, async (batch) => {

const response = await fetch('/api/users/batch', {

method: 'POST',

body: JSON.stringify({ ids: batch }),

headers: { 'Content-Type': 'application/json' }

});

return response.json();

});

10. HTTP 최적화

10.1 압축과 프로토콜 최적화

// Express.js 압축 설정

const compression = require('compression');

app.use(compression({

filter: (req, res) => {

if (req.headers['x-no-compression']) return false;

return compression.filter(req, res);

},

level: 6, // 압축 레벨 (1-9, 6이 균형점)

threshold: 1024, // 1KB 이상만 압축

memLevel: 8, // 메모리 사용량 (1-9)

}));

// HTTP/2 서버 설정

const http2 = require('http2');

const fs = require('fs');

const server = http2.createSecureServer({

key: fs.readFileSync('server.key'),

cert: fs.readFileSync('server.crt'),

allowHTTP1: true,

});

server.on('stream', (stream, headers) => {

const path = headers[':path'];

// Server Push

if (path === '/index.html') {

stream.pushStream({ ':path': '/styles.css' }, (err, pushStream) => {

if (!err) {

pushStream.respond({ ':status': 200, 'content-type': 'text/css' });

pushStream.end(fs.readFileSync('styles.css'));

}

});

}

stream.respond({

':status': 200,

'content-type': 'text/html',

});

stream.end(fs.readFileSync(`.${path}`));

});

10.2 Keep-Alive와 연결 재사용

Python requests - 세션 재사용

from requests.adapters import HTTPAdapter

from urllib3.util.retry import Retry

BAD: 매번 새 연결

def fetch_bad(urls):

results = []

for url in urls:

response = requests.get(url) # 매번 TCP 핸드셰이크

results.append(response.json())

return results

GOOD: 세션 재사용 (Keep-Alive)

def fetch_good(urls):

session = requests.Session()

재시도 설정

retry_strategy = Retry(

total=3,

backoff_factor=0.5,

status_forcelist=[500, 502, 503, 504]

)

adapter = HTTPAdapter(

max_retries=retry_strategy,

pool_connections=10,

pool_maxsize=20,

pool_block=False

)

session.mount("http://", adapter)

session.mount("https://", adapter)

results = []

for url in urls:

response = session.get(url) # 연결 재사용

results.append(response.json())

session.close()

return results

11. 애플리케이션 레벨 최적화

11.1 효율적인 직렬화

// JSON vs MessagePack vs Protobuf 비교

const msgpack = require('msgpack-lite');

// 테스트 데이터

const data = {

users: Array.from({ length: 1000 }, (_, i) => ({

id: i,

name: `User ${i}`,

email: `user${i}@example.com`,

age: 20 + (i % 50),

active: i % 3 !== 0,

tags: ['tag1', 'tag2', 'tag3'],

metadata: { loginCount: i * 10, lastLogin: new Date().toISOString() }

}))

};

// JSON

console.time('json-serialize');

const jsonStr = JSON.stringify(data);

console.timeEnd('json-serialize');

console.log(`JSON size: ${Buffer.byteLength(jsonStr)} bytes`);

// MessagePack

console.time('msgpack-serialize');

const msgpackBuf = msgpack.encode(data);

console.timeEnd('msgpack-serialize');

console.log(`MessagePack size: ${msgpackBuf.length} bytes`);

// 일반적인 결과:

// JSON size: ~120KB, serialize: ~3ms

// MessagePack size: ~85KB, serialize: ~2ms (약 30% 작음)

11.2 Object Pooling

// Apache Commons Pool2 기반 객체 풀

public class ExpensiveObjectPool {

private final GenericObjectPool<ExpensiveObject> pool;

public ExpensiveObjectPool() {

GenericObjectPoolConfig<ExpensiveObject> config = new GenericObjectPoolConfig<>();

config.setMaxTotal(50);

config.setMaxIdle(20);

config.setMinIdle(5);

config.setTestOnBorrow(true);

config.setTestWhileIdle(true);

config.setTimeBetweenEvictionRunsMillis(30000);

pool = new GenericObjectPool<>(new ExpensiveObjectFactory(), config);

}

public ExpensiveObject borrow() throws Exception {

return pool.borrowObject();

}

public void returnObject(ExpensiveObject obj) {

pool.returnObject(obj);

}

static class ExpensiveObjectFactory extends BasePooledObjectFactory<ExpensiveObject> {

@Override

public ExpensiveObject create() {

return new ExpensiveObject(); // 비용이 큰 초기화

}

@Override

public PooledObject<ExpensiveObject> wrap(ExpensiveObject obj) {

return new DefaultPooledObject<>(obj);

}

@Override

public void passivateObject(PooledObject<ExpensiveObject> pooledObj) {

pooledObj.getObject().reset(); // 풀에 반환 시 상태 초기화

}

@Override

public boolean validateObject(PooledObject<ExpensiveObject> pooledObj) {

return pooledObj.getObject().isValid();

}

}

}

12. 프로덕션 모니터링

12.1 SLO 기반 알림 설정

Prometheus 알림 규칙

groups:

- name: slo-alerts

rules:

p99 지연시간 SLO 위반

- alert: HighP99Latency

expr: |

histogram_quantile(0.99,

rate(http_request_duration_seconds_bucket[5m])

) > 0.5

for: 5m

labels:

severity: warning

annotations:

summary: "p99 latency exceeds 500ms"

description: "p99 latency is at {{ $value }}s for 5 minutes"

에러율 SLO 위반

- alert: HighErrorRate

expr: |

sum(rate(http_requests_total{status=~"5.."}[5m]))

/

sum(rate(http_requests_total[5m])) > 0.01

for: 3m

labels:

severity: critical

annotations:

summary: "Error rate exceeds 1%"

처리량 급감

- alert: ThroughputDrop

expr: |

sum(rate(http_requests_total[5m]))

< 0.5 * sum(rate(http_requests_total[5m] offset 1h))

for: 5m

labels:

severity: warning

annotations:

summary: "Throughput dropped over 50% compared to 1h ago"

커넥션 풀 고갈 임박

- alert: ConnectionPoolExhaustion

expr: |

hikaricp_connections_active

/ hikaricp_connections_max > 0.85

for: 2m

labels:

severity: warning

annotations:

summary: "Connection pool utilization above 85%"

GC 일시정지 시간 증가

- alert: HighGCPauseTime

expr: |

rate(jvm_gc_pause_seconds_sum[5m])

/ rate(jvm_gc_pause_seconds_count[5m]) > 0.1

for: 5m

labels:

severity: warning

annotations:

summary: "Average GC pause time exceeds 100ms"

12.2 대시보드 구성

Grafana 대시보드 JSON 생성 (Python)

class PerformanceDashboard:

def generate_panels(self):

return {

"dashboard": {

"title": "Backend Performance",

"panels": [

RED 메트릭

self._throughput_panel(),

self._error_rate_panel(),

self._latency_panel(),

리소스 사용량

self._cpu_panel(),

self._memory_panel(),

self._gc_panel(),

데이터베이스

self._db_query_panel(),

self._connection_pool_panel(),

self._slow_queries_panel(),

캐시

self._cache_hit_rate_panel(),

self._cache_latency_panel(),

외부 서비스

self._external_api_panel(),

]

}

}

def _latency_panel(self):

return {

"title": "API Latency Percentiles",

"type": "timeseries",

"targets": [

{

"expr": 'histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))',

"legendFormat": "p50"

},

{

"expr": 'histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))',

"legendFormat": "p95"

},

{

"expr": 'histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))',

"legendFormat": "p99"

}

]

}

13. 실전 퀴즈

암달의 법칙은 시스템 전체 성능 향상이 **개선 가능한 부분의 비율**에 의해 제한된다는 것을 보여줍니다.

**핵심 시사점**:

- 전체 실행 시간의 작은 부분(예: 5%)을 아무리 빠르게 만들어도 전체 성능 향상은 미미합니다

- 전체 실행 시간의 큰 부분(예: 80%)을 2배만 빠르게 해도 상당한 성능 향상을 얻습니다

- 따라서 **프로파일링으로 가장 큰 병목을 먼저 식별**한 후, 그 부분을 집중적으로 개선해야 합니다

**실무 적용**:

1. 프로파일링(Flame Graph)으로 실행 시간 분포 파악

2. 가장 큰 비율을 차지하는 병목부터 순서대로 최적화

3. 각 최적화 후 다시 측정하여 새로운 병목 확인

4. 성능 예산(Performance Budget) 내에 들어오면 최적화 중단

**N+1 문제**: 부모 엔티티 N개를 조회한 후, 각 부모의 자식 엔티티를 개별 쿼리로 조회하여 총 N+1번의 쿼리가 실행되는 문제입니다. 100개의 주문을 조회하면 101번의 DB 쿼리가 발생합니다.

**해결 방법**:

1. **Eager Loading (select_related/include)**: JOIN을 사용하여 부모와 자식을 한 번의 쿼리로 조회. 1:1, N:1 관계에 효과적

2. **Prefetch (prefetch_related)**: 별도 쿼리로 자식 엔티티를 일괄 조회 후 메모리에서 매핑. 1:N, M:N 관계에 효과적. IN 절 사용

3. **DataLoader 패턴**: 여러 개별 요청을 자동으로 배치하여 하나의 쿼리로 실행. GraphQL에서 특히 유용. Facebook이 개발한 패턴

**Cache-Aside (Lazy Loading)**:

- 읽기 시 캐시 확인, 미스 시 DB 조회 후 캐시 저장

- 애플리케이션이 캐시를 직접 관리

- 첫 번째 요청은 항상 캐시 미스 (Cold Start)

- **적합**: 읽기가 많고, 모든 데이터를 캐싱할 필요가 없는 경우

**Write-Through**:

- 쓰기 시 캐시와 DB를 동시에 업데이트

- 캐시가 항상 최신 상태

- 쓰기 지연시간 증가 (두 곳에 쓰기)

- **적합**: 데이터 일관성이 중요하고, 읽기가 쓰기보다 훨씬 많은 경우

Write-Behind는 캐시에 먼저 쓰고 DB에는 비동기로 반영하여 쓰기 성능을 극대화하지만, 데이터 손실 리스크가 있습니다.

1. **Smoke Test**: 최소 부하(1-5 VU)로 시스템 기본 동작을 확인. 배포 후 기본 검증

2. **Load Test**: 예상 트래픽 수준에서 성능 확인. SLO 충족 여부 검증

3. **Stress Test**: 예상 트래픽을 초과하여 시스템 한계점 탐색. 용량 계획에 활용

4. **Spike Test**: 갑작스러운 트래픽 급증(예: 이벤트)에 대한 시스템 반응 확인. 오토스케일링 검증

5. **Soak Test**: 장시간(수 시간~하루) 일정 부하를 유지하여 메모리 누수, 커넥션 고갈 등 점진적 문제 발견

6. **Breakpoint Test**: 부하를 지속적으로 증가시켜 시스템이 완전히 실패하는 지점 탐색. 절대적 한계 파악

**핵심 파라미터**:

- **maximum-pool-size**: 최대 커넥션 수 (과하면 DB 부하, 부족하면 대기)

- **minimum-idle**: 유휴 커넥션 최소 수 (Cold Start 방지)

- **connection-timeout**: 커넥션 획득 대기 시간

- **idle-timeout**: 유휴 커넥션 반환 시간

- **max-lifetime**: 커넥션 최대 수명 (DB 방화벽 타임아웃보다 짧게)

**적절한 풀 크기 결정**:

- HikariCP 공식: `connections = ((core_count * 2) + effective_spindle_count)`

- SSD의 경우: `connections = core_count * 2 + 1` 정도

- 일반적으로 10-20이면 충분한 경우가 많음

- 너무 큰 풀은 오히려 DB의 컨텍스트 스위칭 비용을 증가시킴

- 모니터링 기반으로 조정: 사용률이 80%를 넘으면 증가 검토, 대기 스레드가 발생하면 즉시 증가

참고 자료

1. [Google SRE Book - Performance Engineering](https://sre.google/sre-book/table-of-contents/)

2. [k6 Documentation](https://k6.io/docs/)

3. [Artillery Documentation](https://www.artillery.io/docs)

4. [Brendan Gregg - Systems Performance](https://www.brendangregg.com/systems-performance-2nd-edition-book.html)

5. [Flame Graphs](https://www.brendangregg.com/flamegraphs.html)

6. [HikariCP - About Pool Sizing](https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing)

7. [Redis Best Practices](https://redis.io/docs/management/optimization/)

8. [PostgreSQL Performance Tips](https://www.postgresql.org/docs/current/performance-tips.html)

9. [Node.js Diagnostics Guide](https://nodejs.org/en/docs/guides/diagnostics)

10. [Go pprof Documentation](https://pkg.go.dev/net/http/pprof)

11. [Python cProfile Documentation](https://docs.python.org/3/library/profile.html)

12. [Prometheus Monitoring](https://prometheus.io/docs/practices/alerting/)

13. [DataLoader Pattern](https://github.com/graphql/dataloader)

현재 단락 (1/1352)

성능 엔지니어링의 황금률은 "추측하지 말고, 측정하라"입니다. 직감에 의한 최적화는 대부분 잘못된 곳에 시간을 낭비합니다.

작성 글자: 0원문 글자: 34,990작성 단락: 0/1352