로드밸런서 뒤에 서버를 늘렸더니 로그인이 자꾸 풀린 이유
문제 발생
트래픽 증가에 대응해 애플리케이션 서버를 한 대에서 세 대로 늘리고 로드밸런서로 분산시켰는데, 사용자들이 로그인 직후 바로 로그아웃되는 것처럼 인증이 자꾸 풀리는 문제를 겪었습니다.
원인 분석
세션을 각 서버의 메모리에 저장하는 방식(전통적인 in-memory session)에서는, 세션 데이터가 그 세션을 만든 서버에만 존재합니다. 로드밸런서는 기본적으로 각 요청을 여러 서버에 고르게 분산시키므로, 사용자가 로그인할 때는 서버 A가 응답하고 세션을 A의 메모리에 저장했는데, 바로 다음 요청은 로드밸런서가 서버 B로 보낼 수 있습니다. 서버 B는 그 사용자의 세션을 전혀 모르므로 미인증 상태로 취급합니다 — 사용자 입장에서는 로그인이 안 되는 것처럼 보입니다.
해결 방안
두 가지 방향이 있고, 각각 트레이드오프가 있습니다.
- Sticky session(세션 어피니티): 로드밸런서가 같은 사용자의 요청을 항상 같은 서버로 보내도록(쿠키 기반 라우팅 등) 설정합니다.
upstream backend {
ip_hash; # 클라이언트 IP 기준으로 항상 같은 서버로 라우팅
server app1:3000;
server app2:3000;
}장점은 서버 코드를 바꿀 필요가 없다는 것이지만, 단점도 명확합니다 — 특정 서버로 트래픽이 쏠리면 부하 분산이 불균등해질 수 있고, 그 서버가 장애로 내려가면 거기 연결되어 있던 세션이 전부 끊깁니다. 즉 가용성과 확장성을 어느 정도 희생하는 방식입니다.
- 외부 세션 저장소: 세션 데이터를 각 서버의 메모리가 아니라 Redis 같은 공유 저장소에 둡니다.
app.use(session({
store: new RedisStore({ client: redisClient }),
// ...
}));어느 서버가 요청을 받든 같은 Redis에서 세션을 조회하므로, sticky session 없이도 세션이 항상 일관됩니다. 이 방식이 대부분의 경우 더 권장됩니다 — 로드밸런서를 특정 라우팅 방식에 종속시키지 않고, 서버를 자유롭게 늘리거나 줄여도(오토스케일링 포함) 세션이 끊기지 않기 때문입니다.
- Better Auth처럼 세션을 처음부터 DB에 저장하는 방식을 쓰는 아키텍처(이 프로젝트가 바로 이 경우입니다)는 애초에 이 문제 자체가 발생하지 않습니다 — 어느 서버가 요청을 받든 같은 DB의 같은 세션 row를 직접 읽으므로, sticky session이나 별도 세션 저장소 없이도 자연스럽게 일관성이 보장됩니다.
- 정말로 sticky session이 필요한 특수한 상황(예: WebSocket 연결처럼 세션이 아니라 연결 자체가 특정 서버에 묶여야 하는 경우)이 아니라면, 새로 설계하는 시스템은 처음부터 "세션이 어느 서버에서든 동일하게 보이는" 구조(공유 저장소든 DB든)로 시작하는 것이 나중에 서버를 늘릴 때 겪는 이런 문제를 원천적으로 피하는 길입니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.