이전 포스팅
https://changbroblog.tistory.com/240
위기: "재고가 이상해요..." - 동시성 이슈와의 전쟁
이전 포스팅https://changbroblog.tistory.com/239 시작: 대규모 트래픽을 견디는 티켓 예매 시스템 구축기안녕하세요! 이번 프로젝트는"수만 명의 사용자가 동시에 접속하는 티켓팅 환경에서 어떻게 시스
changbroblog.tistory.com
전 포스팅에서 에서 Redis 분산 락으로 동시성 문제를 해결했습니다. 하지만 트래픽이 계속 늘어난다면 단일 서버(Monolith)로는 CPU와 메모리의 한계에 부딪히게 됩니다.
그래서 이번에는 서버를 3대로 늘리고(Scale-out), 앞단에 Nginx를 두어 트래픽을 분산시키는 구조를 도입했습니다. 그런데 예상치 못한 문제가 발생했습니다.
"로그인을 했는데, 새로고침만 하면 로그아웃이 돼요!"
1. 인프라 확장: Nginx와 3개의 앱 서버
Docker Compose를 이용해 애플리케이션 서버를 app-1, app-2, app-3 3대로 복제하고, Nginx가 라운드 로빈(Round Robin) 방식으로 요청을 분배하도록 구성했습니다.
- Round Robin: 요청 1 -> 서버 1, 요청 2 -> 서버 2, 요청 3 -> 서버 3 ... 순서대로 할당.
events {
worker_connections 1024;
}
http {
upstream ticket-app {
server app-1:8080 max_fails=1 fail_timeout=2s;
server app-2:8080 max_fails=1 fail_timeout=2s;
server app-3:8080 max_fails=1 fail_timeout=2s;
}
server {
listen 80;
location / {
proxy_pass http://ticket-app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Failover 설정
proxy_connect_timeout 1s;
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
}
}
}
2. 문제 상황: 세션 불일치 (Session Inconsistency)
💥 현상

- 사용자가 로그인 시도 -> Nginx가 서버 1로 보냄. -> 서버 1에 세션 생성.
- 로그인 성공 후 메인 페이지로 이동(새로고침) -> Nginx가 서버 2로 보냄.
- 서버 2: "어? 내 메모리엔 네 세션 정보가 없는데? 누구세요?" -> 로그인 페이지로 튕겨냄.
🧐 원인 분석
기존의 세션(Session) 방식은 세션 정보를 각 서버의 내부 메모리(In-Memory)에 저장합니다.
서버가 여러 대가 되면서, 서버끼리 세션 정보를 공유하지 못해 발생하는 전형적인 문제입니다.
3. 해결책 비교: Sticky Session vs Session Clustering
| 방식 | 설명 | 장점 | 단점 |
|---|---|---|---|
| Sticky Session | Nginx가 특정 사용자의 IP나 쿠키를 기억해서, 처음 접속한 서버로만 계속 보내줌. | 구현이 쉽다 (Nginx 설정만 변경). | 특정 서버에 트래픽이 몰릴 수 있음. 서버 다운 시 해당 세션 다 날아감. |
| Session Clustering | 세션 저장소를 외부(Redis, DB)로 분리하여 모든 서버가 공유함. | 서버가 죽어도 세션 유지. 트래픽 분산 자유로움. | 별도 저장소(Redis) 구축 필요. |
저는 고가용성(High Availability)과 유연한 확장성을 위해 Redis Session Clustering 방식을 선택했습니다. 이미 분산 락을 위해 Redis를 사용 중이므로 비용도 들지 않습니다.
4. 해결: Redis Session Clustering 적용
Spring Boot에서는 아주 간단하게 적용할 수 있습니다.spring-session-data-redis 의존성을 추가하고 설정만 조금 바꿔주면, 톰캣의 HttpSession을 가로채서 알아서 Redis에 저장해줍니다.
① build.gradle: 의존성 추가
Spring Session 라이브러리를 추가하여 세션 관리 주체를 Spring에서 Redis로 넘기도록 설정했습니다.
dependencies {
// Redis 사용을 위한 스타터
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// 세션을 Redis에 저장하기 위한 핵심 라이브러리
implementation 'org.springframework.session:spring-session-data-redis'
}
② application.properties: 저장소 지정
단 한 줄의 설정으로 세션 저장소를 Redis로 변경할 수 있습니다.
# 세션 저장소 타입을 redis로 명시 (기본값은 none/memory)
spring.session.store-type=redis
# Redis 접속 정보 (환경변수 활용)
spring.data.redis.host=${SPRING_DATA_REDIS_HOST:localhost}
spring.data.redis.port=${SPRING_DATA_REDIS_PORT:6379}
🛠️ 적용 후 아키텍처
- 서버 1, 2, 3: 로그인 요청이 오면 세션을 생성해서 Redis에 저장.
- 서버 2: 다음 요청이 오면 Redis에서 세션을 조회해서 확인.
🏆 최종 결과

- 서버가 바뀌어도 로그인이 풀리지 않음.

- 운영 중에 서버 1대를 강제로 끄고(Down) 다른 서버로 접속되어도 로그인이 유지됨. (세션 데이터가 Redis에 살아있기 때문)
5. 결론
이번 단계를 통해 서버를 아무리 늘려도(Scale-out) 사용자는 동일한 경험을 할 수 있는 무상태(Stateless) 아키텍처를 구축했습니다. 이제 지금 시스템은 한두 대의 서버가 장애로 죽더라도 서비스가 중단되지 않는 고가용성(High Availability)을 갖추게 되었습니다.
하지만...
서버가 늘어나니 이제 데이터베이스(Postgres)가 비명을 지르기 시작했습니다. 모든 서버가 매번 DB를 직접 조회하고 있기 때문입니다.
다음 포스팅에서는 Redis Caching을 도입하여 DB 부하를 획기적으로 줄이는 방법을 다뤄보겠습니다.
'Project > 티켓 예매 시스템(대규모 트래픽 테스트)' 카테고리의 다른 글
| 위기: "재고가 이상해요..." - 동시성 이슈와의 전쟁 (1) | 2026.02.02 |
|---|---|
| 시작: 대규모 트래픽을 견디는 티켓 예매 시스템 구축기 (1) | 2026.02.02 |