검색어를 입력하세요.

대규모 트래픽 처리를 위한 로드밸런싱과 DB 분산 전략

간지뽕빨리턴님 2026. 4. 12. 00:04
반응형

 

서비스를 개발하다 보면 처음에는 단일 서버 하나로도 충분합니다. 하지만 사용자가 늘고, 동시 접속이 수백에서 수천 단위를 넘어가기 시작하면 "서버가 버틸 수 있을까?"라는 고민이 현실로 다가옵니다.

실제로 업무를 하면서 규모 있는 서비스를 만들다 보면, 단순히 기능 구현에만 집중할 수 없습니다. 동시 접속자가 몰릴 때 응답 지연이 발생하고, DB 커넥션이 부족해져 장애로 이어지는 상황을 한두 번 겪고 나면 트래픽 분산DB 처리 전략이 아키텍처 설계의 핵심이라는 걸 체감하게 됩니다.

 

이번 글에서는 로드밸런싱의 기본 개념부터 DB 커넥션 관리, 읽기/쓰기 분리, 캐싱 전략까지 실무에서 활용할 수 있는 기술들을 정리해 보겠습니다. 이론에 그치지 않고 왜 이 기술이 필요한지, 그리고 어떤 상황에서 어떤 선택을 해야 하는지 판단 기준을 함께 다루겠습니다. 또한 IIS/ASP.NET Core 환경과 Node.js 환경에서의 실제 구현 방법도 함께 정리하였습니다.


 

1로드밸런싱(Load Balancing)이란

로드밸런싱은 다수의 서버에 클라이언트 요청을 분산하여 특정 서버에 부하가 집중되는 것을 방지하는 기술입니다. 단일 서버 구성에서는 해당 서버가 다운되면 서비스 전체가 중단되지만, 로드밸런서를 도입하면 고가용성(High Availability)수평 확장(Scale-Out)이 가능해집니다.

로드밸런서의 동작 위치

[클라이언트] | v [로드밸런서] -- L4(TCP/UDP) 또는 L7(HTTP/HTTPS) 계층 | +--+--+ v v v [WAS1][WAS2][WAS3] -- 동일 애플리케이션 서버 그룹

L4 로드밸런싱은 IP 주소와 포트 기반으로 분배하며, 처리 속도가 빠르고 단순합니다. AWS NLB, HAProxy(TCP 모드)가 대표적입니다.

L7 로드밸런싱은 HTTP 헤더, URL 경로, 쿠키 등 애플리케이션 계층 정보를 기반으로 분배합니다. URL 패턴별로 다른 서버 그룹에 라우팅 할 수 있으며, AWS ALB, Nginx가 대표적입니다.

주요 분배 알고리즘

알고리즘 설명 적합한 상황
Round Robin 서버에 순서대로 요청을 분배합니다 서버 스펙이 동일할 때
Weighted Round Robin 서버별 가중치를 부여하여 분배합니다 서버 스펙이 다를 때
Least Connections 현재 연결 수가 가장 적은 서버로 분배합니다 요청 처리 시간이 불균등할 때
IP Hash 클라이언트 IP 기반 해시로 고정 서버를 할당합니다 세션 유지가 필요할 때
Least Response Time 응답 시간이 가장 빠른 서버로 분배합니다 서버 성능이 동적으로 변하는 환경

세션 관리 문제

로드밸런싱 환경에서 가장 먼저 부딪히는 문제가 세션 관리입니다. 사용자가 WAS1에서 로그인했는데 다음 요청이 WAS2로 가면 세션이 없어 로그인이 풀리게 됩니다. 해결 방법은 크게 세 가지입니다.

 

Sticky Session — 로드밸런서가 특정 사용자를 동일 서버에 고정하는 방식입니다. 구현이 간단하지만, 특정 서버에 부하가 몰릴 수 있고 해당 서버가 다운되면 세션이 유실됩니다.

 

Session Clustering — WAS 간 세션 데이터를 복제하고 공유하는 방식입니다. Tomcat의 경우 DeltaManager를 활용할 수 있지만, 서버 수가 늘어나면 동기화 오버헤드가 기하급수적으로 증가합니다.

 

External Session Store — Redis나 Memcached 같은 외부 저장소에 세션을 저장하는 방식입니다. 가장 널리 사용되며, WAS가 어디로 라우팅되든 동일한 세션 데이터를 조회할 수 있습니다.

[WAS1] --+ [WAS2] --+--> [Redis Cluster] -- 세션 저장소 [WAS3] --+
참고 - 실무에서는 JWT 토큰 기반 인증으로 아예 서버 측 세션을 없애는 Stateless 구조를 채택하는 경우도 많습니다. 세션 저장소 자체가 불필요해지므로 구조가 단순해집니다. 다만 토큰 탈취 시 즉시 무효화가 어렵다는 트레이드오프가 있어, Refresh Token 전략이나 Token Blacklist 등 보완 설계가 필요합니다.

2IIS + ASP.NET Core 환경에서의 로드밸런싱

Windows Server 기반의 IIS 환경에서는 ARR(Application Request Routing) 모듈을 통해 L7 로드밸런싱을 구성할 수 있습니다. ARR은 IIS의 확장 모듈로, 리버스 프록시 및 로드밸런서 역할을 수행합니다.

IIS ARR 기반 서버 팜 구성

[클라이언트] | v [IIS + ARR] -- 로드밸런서 역할 | +--+--+ v v v [IIS Node1][IIS Node2][IIS Node3] ASP.NET ASP.NET ASP.NET Core App Core App Core App

ARR에서 서버 팜(Server Farm)을 생성하고, 각 백엔드 서버를 등록하면 자동으로 Health Check와 함께 요청을 분배합니다. 분배 알고리즘은 Weighted Round Robin, Least Current Request 등을 IIS 관리자에서 선택할 수 있습니다.

ASP.NET Core 분산 세션 구성 (Redis)

ASP.NET Core에서는 Microsoft.Extensions.Caching.StackExchangeRedis 패키지를 통해 분산 세션을 구성합니다.

// Program.cs builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = "redis-server:6379"; options.InstanceName = "MyApp_"; }); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); app.UseSession();

ASP.NET Core Data Protection Key 공유

로드밸런싱 환경에서 ASP.NET Core를 운영할 때 반드시 설정해야 하는 것이 Data Protection Key 공유입니다. 각 인스턴스가 서로 다른 키를 사용하면 인증 쿠키를 복호화할 수 없어 세션이 풀리게 됩니다.

builder.Services.AddDataProtection() .PersistKeysToStackExchangeRedis( ConnectionMultiplexer.Connect("redis-server:6379"), "DataProtection-Keys" ) .SetApplicationName("MyApp");
주의 — Data Protection Key를 공유하지 않으면 IIS ARR 환경에서 Sticky Session 없이 운영할 때 로그인이 반복적으로 풀리는 현상이 발생합니다. 이 문제는 실무에서 매우 자주 발생하므로 반드시 확인해야 합니다.

IIS 환경에서의 Health Check 설정

ASP.NET Core에는 내장 Health Check 미들웨어가 있으며, ARR의 URL Test 기능과 연동할 수 있습니다.

builder.Services.AddHealthChecks() .AddSqlServer(connectionString, name: "sqldb") .AddRedis("redis-server:6379", name: "redis"); app.MapHealthChecks("/health");

ARR 서버 팜의 Health Test 설정에서 URL을 /health로 지정하면, 장애가 발생한 노드를 자동으로 분배 대상에서 제외합니다.


3Node.js 환경에서의 로드밸런싱

Node.js는 싱글 스레드 이벤트 루프 기반으로 동작하기 때문에, 멀티 코어 CPU를 활용하려면 별도의 분산 처리가 필요합니다. 대표적인 방법은 Cluster 모듈PM2, 그리고 앞단에 Nginx 리버스 프록시를 두는 구성입니다.

Node.js Cluster 모듈

const cluster = require('cluster'); const os = require('os'); const express = require('express'); if (cluster.isPrimary) { const cpuCount = os.cpus().length; console.log(`Primary ${process.pid} — forking ${cpuCount} workers`); for (let i = 0; i < cpuCount; i++) { cluster.fork(); } cluster.on('exit', (worker) => { console.log(`Worker ${worker.process.pid} exited. Restarting...`); cluster.fork(); }); } else { const app = express(); app.get('/', (req, res) => { res.send(`Handled by worker ${process.pid}`); }); app.listen(3000); }

PM2를 활용한 클러스터 모드

PM2는 Node.js 프로세스 매니저로, 클러스터 모드를 간편하게 설정할 수 있습니다.

module.exports = { apps: [{ name: "my-app", script: "./app.js", instances: "max", exec_mode: "cluster", max_memory_restart: "500M", env: { NODE_ENV: "production", PORT: 3000 } }] };
$ pm2 start ecosystem.config.js $ pm2 monit $ pm2 reload my-app

Nginx 리버스 프록시 + Node.js 다중 인스턴스

프로덕션 환경에서는 Nginx를 앞단에 두고 여러 Node.js 인스턴스(또는 서버)로 요청을 분배하는 것이 일반적입니다.

upstream nodejs_backend { least_conn; server 127.0.0.1:3001; server 127.0.0.1:3002; server 127.0.0.1:3003; server 127.0.0.1:3004; } server { listen 80; server_name example.com; location / { proxy_pass http://nodejs_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }

Node.js 분산 세션 관리 (express-session + Redis)

const session = require('express-session'); const RedisStore = require('connect-redis').default; const { createClient } = require('redis'); const redisClient = createClient({ url: 'redis://redis-server:6379' }); await redisClient.connect(); app.use(session({ store: new RedisStore({ client: redisClient }), secret: 'your-secret-key', resave: false, saveUninitialized: false, cookie: { secure: true, maxAge: 1800000 } }));

4DB 커넥션 관리 — Connection Pool의 중요성

트래픽이 증가하면 WAS보다 DB가 먼저 병목이 되는 경우가 많습니다. DB 커넥션은 생성 비용이 높은 자원입니다. TCP 3-way Handshake, 인증, 세션 초기화 과정을 매 요청마다 반복하면 성능이 급격히 저하됩니다.

Connection Pool 기본 구조

[Application Thread 1] --+ [Application Thread 2] --+--> [Connection Pool] --> [DB] [Application Thread 3] --+ (미리 생성된 커넥션 N개를 재사용)

HikariCP 설정 예시 (Spring Boot / Java)

spring: datasource: hikari: minimum-idle: 10 maximum-pool-size: 30 connection-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000 leak-detection-threshold: 5000

ASP.NET Core 커넥션 풀 설정

ASP.NET Core에서 SQL Server를 사용하는 경우, 커넥션 풀은 연결 문자열에서 설정합니다. ADO.NET이 내부적으로 커넥션 풀을 관리합니다.

{ "ConnectionStrings": { "Default": "Server=db-server;Database=MyDb;User Id=sa;Password=***;Min Pool Size=10;Max Pool Size=100;Connection Timeout=15;Connection Lifetime=300;" } }
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer( builder.Configuration.GetConnectionString("Default"), sqlOptions => sqlOptions.EnableRetryOnFailure( maxRetryCount: 3, maxRetryDelay: TimeSpan.FromSeconds(5), errorNumbersToAdd: null ) ) );

Node.js 커넥션 풀 설정 (PostgreSQL)

const { Pool } = require('pg'); const pool = new Pool({ host: 'db-server', port: 5432, database: 'mydb', user: 'admin', password: '***', min: 5, max: 30, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, }); const client = await pool.connect(); try { const result = await client.query( 'SELECT * FROM products WHERE id = $1', [id] ); return result.rows[0]; } finally { client.release(); }
참고 — maximum-pool-size 설정은 단순히 크게 잡는 것이 능사가 아닙니다. DB 서버의 max_connections 값과 애플리케이션 인스턴스 수를 함께 고려해야 합니다. 예를 들어 DB max_connections가 200이고 인스턴스가 5대라면, 인스턴스당 풀 크기는 200 / 5 = 40, 여유분을 감안하면 30~35 수준이 적절합니다.

커넥션 누수(Connection Leak) 방지

실무에서 자주 발생하는 장애 원인 중 하나가 커넥션 누수입니다. 트랜잭션을 열어놓고 예외 발생 시 커넥션을 반환하지 않으면, 풀의 커넥션이 하나씩 고갈되다가 결국 전체 서비스가 멈추게 됩니다.

// [C#] 잘못된 예 — 예외 발생 시 커넥션 미반환 var conn = new SqlConnection(connString); conn.Open(); var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT ..."; cmd.ExecuteReader(); // [C#] 올바른 예 — using 문 사용 using var conn = new SqlConnection(connString); await conn.OpenAsync(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT ..."; await cmd.ExecuteReaderAsync();

5DB 분산 전략 — 읽기/쓰기 분리 (Read Replica)

일반적인 웹 서비스에서 DB 요청의 70~80%는 읽기(SELECT) 작업입니다. 이 읽기 부하를 별도의 복제본(Replica)으로 분리하면 Master DB의 부하를 크게 줄일 수 있습니다.

+--- [쓰기 요청] --> [Master DB] [Application] | | (비동기 복제) +--- [읽기 요청] --> [Replica DB 1] [Replica DB 2]

ASP.NET Core에서의 읽기/쓰기 분리

builder.Services.AddDbContext<WriteDbContext>(options => options.UseSqlServer("Server=master-db;...")); builder.Services.AddDbContext<ReadDbContext>(options => options.UseSqlServer("Server=replica-db;...") .UseQueryTrackingBehavior( QueryTrackingBehavior.NoTracking ));
public class OrderService { private readonly WriteDbContext _writeDb; private readonly ReadDbContext _readDb; // 읽기 -- Replica로 라우팅 public async Task<List<Order>> GetOrders(long userId) => await _readDb.Orders .Where(o => o.UserId == userId) .ToListAsync(); // 쓰기 -- Master로 라우팅 public async Task CreateOrder(Order order) { _writeDb.Orders.Add(order); await _writeDb.SaveChangesAsync(); } }

Node.js에서의 읽기/쓰기 분리 (Sequelize)

const { Sequelize } = require('sequelize'); const sequelize = new Sequelize('mydb', null, null, { replication: { read: [ { host: 'replica-1', username: 'reader', password: '***' }, { host: 'replica-2', username: 'reader', password: '***' } ], write: { host: 'master-db', username: 'writer', password: '***' } }, dialect: 'postgres', pool: { max: 30, min: 5, idle: 10000 } });
주의 — 복제 지연(Replication Lag) — Master에서 Replica로의 데이터 복제는 비동기로 이루어지므로 수 밀리초에서 수 초의 지연이 발생할 수 있습니다. 사용자가 게시글을 작성한 직후 목록을 조회하면 방금 작성한 글이 보이지 않는 문제가 생길 수 있습니다. 쓰기 직후 읽기(Read-after-Write) 시에는 강제로 Master에서 조회하도록 처리해야 합니다.

6캐싱 전략 — DB 부하를 줄이는 가장 효과적인 방법

아무리 DB를 분산해도 동일한 데이터를 반복 조회하는 요청은 캐시로 처리하는 것이 가장 효율적입니다.

캐시 적용 계층

[클라이언트] --> [CDN 캐시] --> [애플리케이션 캐시] --> [DB 캐시] --> [DB] (정적 자원) (Redis / Local) (Query Cache)

ASP.NET Core — IDistributedCache 기반 Look-Aside 패턴

public class ProductService { private readonly IDistributedCache _cache; private readonly ReadDbContext _db; public async Task<Product> GetProduct(long id) { var key = $"product:{id}"; var cached = await _cache.GetStringAsync(key); if (cached != null) return JsonSerializer.Deserialize<Product>(cached); var product = await _db.Products.FindAsync(id); await _cache.SetStringAsync(key, JsonSerializer.Serialize(product), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }); return product; } }

Node.js — ioredis 기반 캐시 유틸리티

const Redis = require('ioredis'); const redis = new Redis('redis://redis-server:6379'); async function getProduct(id) { const key = `product:${id}`; const cached = await redis.get(key); if (cached) return JSON.parse(cached); const product = await db.query( 'SELECT * FROM products WHERE id = $1', [id] ); await redis.setex(key, 1800, JSON.stringify(product.rows[0]) ); return product.rows[0]; }

캐시 무효화(Invalidation) 전략 비교

전략 설명 장점 단점
TTL 기반 일정 시간 후 자동 만료 구현이 단순합니다 만료 전까지 오래된 데이터를 제공할 수 있습니다
Write-Through 데이터 변경 시 캐시도 즉시 갱신 데이터 일관성이 높습니다 쓰기 성능이 저하됩니다
Write-Behind 캐시에 먼저 쓰고 일정 주기로 DB에 반영 쓰기 성능이 우수합니다 데이터 유실 위험이 있습니다
이벤트 기반 데이터 변경 이벤트 발행 시 캐시 갱신 시스템 간 결합도가 낮습니다 이벤트 유실 처리가 필요합니다

7대용량 트래픽 대응 — 실무 체크리스트

실제 서비스에서 대규모 트래픽에 대비할 때 점검해야 할 항목을 계층별로 정리하면 다음과 같습니다.

WAS 계층

  • 로드밸런서 구성 (L4/L7 선택, IIS ARR 또는 Nginx)
  • Auto Scaling 정책 수립 (CPU, 메모리, 요청 수 기준)
  • Stateless 설계 (세션 외부 저장소 또는 JWT)
  • 서킷 브레이커(Circuit Breaker) 적용 — 외부 서비스 장애 전파 방지
  • Rate Limiting / Throttling — 비정상 트래픽 차단

DB 계층

  • Connection Pool 적정 사이즈 산정
  • 읽기/쓰기 분리 (Read Replica)
  • 쿼리 최적화 및 인덱스 점검
  • Slow Query 모니터링
  • 필요 시 샤딩(Sharding) 검토

캐시 계층

  • Redis/Memcached 클러스터 구성
  • 캐시 히트율 모니터링 (목표: 80% 이상)
  • 캐시 스탬피드(Cache Stampede) 방지 — 동시 만료 시 DB 폭주 대비
  • Hot Key 분산 처리

인프라 / 모니터링

  • CDN 적용 (정적 자원)
  • 메시지 큐(Kafka, RabbitMQ) 도입 — 비동기 처리로 피크 부하 완화
  • APM 도구(Datadog, Grafana, Pinpoint 등) 연동
  • 부하 테스트(nGrinder, k6, JMeter) 정기 수행

참고 자료 및 공식 문서

마무리

로드밸런싱과 DB 분산 전략은 서비스가 성장할수록 반드시 마주하게 되는 과제입니다. 처음부터 완벽한 아키텍처를 설계할 필요는 없지만, 각 기술이 어떤 문제를 해결하는지, 그리고 어떤 트레이드오프가 있는지 이해하고 있으면 장애 상황에서 훨씬 빠르게 대응할 수 있습니다.

핵심 정리!

  1. WAS는 Stateless하게 설계하고 로드밸런서로 수평 확장합니다.
  2. DB는 커넥션 풀 관리와 읽기/쓰기 분리로 병목을 줄입니다.
  3. 캐시를 적극 활용하여 불필요한 DB 접근을 줄입니다.

작은 규모의 서비스라도 이런 구조를 머릿속에 그려두시면, 나중에 트래픽이 증가했을 때 당황하지 않고 단계적으로 확장할 수 있을 것입니다.

#로드밸런싱 #LoadBalancing #DB분산처리 #ConnectionPool #ReadReplica #캐싱전략 #대용량트래픽 #서버아키텍처 #ASPNETCore #NodeJS

커피 한 잔의 힘

이 글이 도움이 되셨다면, 커피 한 잔으로 응원해주세요!
여러분의 작은 후원이 더 좋은 콘텐츠를 만드는 큰 힘이 됩니다.