서버 한 대를 늘리는 것과 여러 대로 쪼개는 것의 차이
서버를 키우는 것부터 시도해야 하는 이유
트래픽이 늘면 반사적으로 "서버를 여러 대로 늘려야죠"라고 말하는 지원자가 많습니다. 틀린 방향은 아니지만 순서가 이상합니다. 실무에서나 인터뷰에서나 먼저 검토할 건 수직 확장(scale up)입니다. CPU와 메모리를 늘린 더 큰 인스턴스 한 대로 문제가 풀린다면, 그게 여러 대로 쪼개는 것보다 훨씬 단순합니다. 서버가 한 대면 데이터 정합성 문제도, 세션 공유 문제도, 로드밸런서도 필요 없습니다.
문제는 수직 확장에 물리적 한계가 있다는 겁니다. 클라우드 인스턴스 스펙은 어느 지점 이상 올라가지 않고, 올라간다 해도 비용이 선형이 아니라 그 이상으로 뜁니다. 그리고 결정적으로, 서버가 한 대인 이상 그 서버가 죽으면 서비스 전체가 죽습니다. 이 단일 장애점(SPOF) 문제는 아무리 스펙을 올려도 해결되지 않습니다. 이 지점에서 수평 확장(scale out)으로 넘어가는 게 타당해집니다.
로드밸런서는 트래픽을 나누는 것 이상의 일을 한다
로드밸런서를 "요청을 여러 서버에 나눠주는 장치" 정도로만 설명하고 넘어가면 절반만 말한 겁니다. 실제로 로드밸런서가 하는 중요한 일은 헬스 체크입니다. 죽은 서버로 트래픽을 계속 보내면 사용자는 타임아웃을 겪습니다. 로드밸런서가 주기적으로 각 서버의 상태를 확인하고, 응답이 없는 서버를 풀에서 빼는 것이 수평 확장이 실제로 안정성을 개선하는 지점입니다. 서버를 열 대로 늘려도 죽은 서버로 계속 요청이 가면 아무 의미가 없습니다.
분배 알고리즘은 상황에 따라 다른 걸 씁니다.
# 라운드로빈: 순서대로 돌아가며 배정. 구현이 단순하지만 서버 성능 차이를 무시한다.
def round_robin(servers, counter):
server = servers[counter % len(servers)]
return server, counter + 1
# 최소 연결: 현재 처리 중인 연결이 가장 적은 서버로 보낸다.
# 요청마다 처리 시간 편차가 큰 서비스(예: 이미지 변환)에 더 적합하다.
def least_connections(servers_with_load):
return min(servers_with_load, key=lambda s: s.active_connections)
# 일관된 해싱: 같은 클라이언트(또는 같은 키)가 항상 같은 서버로 가야 할 때.
# 세션 고정(sticky session)이나 캐시 적중률 유지에 쓴다.
def consistent_hash_pick(key, hash_ring):
h = hash(key)
return hash_ring.find_server_clockwise(h)라운드로빈은 각 요청의 처리 비용이 비슷할 때는 잘 동작하지만, 어떤 요청은 1ms 만에 끝나고 어떤 요청은 500ms가 걸리는 서비스에서는 특정 서버에 무거운 요청이 몰릴 수 있습니다. 이럴 때는 최소 연결 방식이 더 균형 잡힌 분배를 만듭니다. 일관된 해싱은 뒤에 나올 캐싱 챕터와 샤딩 챕터에서 다시 등장하는데, "같은 요청은 같은 곳으로" 라는 요구가 있을 때 로드밸런서 단계부터 캐시·DB 단계까지 반복해서 쓰이는 아이디어이기 때문입니다.
상태를 서버에 두는 순간 수평 확장은 깨진다
수평 확장을 막는 가장 흔한 실수는 서버가 상태를 갖게 두는 겁니다. 로그인 세션을 서버 메모리에 저장하면, 사용자의 두 번째 요청이 로드밸런서에 의해 다른 서버로 가는 순간 로그인이 풀립니다. 이 문제를 스티키 세션(같은 클라이언트를 항상 같은 서버로 보내기)으로 땜질할 수도 있지만, 그러면 그 서버가 죽었을 때 세션이 통째로 날아가고, 애초에 로드밸런서가 트래픽을 균등하게 분배하려는 목적과도 어긋납니다.
정공법은 서버를 무상태(stateless)로 만들고, 세션 데이터는 Redis 같은 공유 저장소에 두는 겁니다. 이렇게 하면 어떤 서버가 요청을 받든 같은 결과를 낼 수 있고, 서버를 늘리거나 줄이는 일이 정말로 트래픽 대응 버튼 하나로 끝납니다. 이 원칙 — 애플리케이션 서버는 무상태로 유지하고 상태는 별도 계층에 위임한다 — 은 이 책 전체에서 반복해서 등장하는 가장 기본적인 설계 원칙입니다.
flowchart TD Client["클라이언트"] --> LB["로드밸런서"] LB --> S1["서버 1 (무상태)"] LB --> S2["서버 2 (무상태)"] LB --> S3["서버 3 (무상태)"] S1 --> Session["공유 세션 저장소 (Redis)"] S2 --> Session S3 --> Session
로드밸런서 자체도 단일 장애점이 될 수 있다
여기서 놓치기 쉬운 함정이 하나 있습니다. 서버는 여러 대로 늘렸는데 로드밸런서는 한 대라면, 단일 장애점을 서버에서 로드밸런서로 옮긴 것뿐입니다. 실무에서는 로드밸런서를 이중화하거나(active-passive), DNS 라운드로빈으로 여러 로드밸런서에 트래픽을 먼저 분산시킵니다. 인터뷰에서 이 지점까지 짚는 지원자는 많지 않은데, 그래서 오히려 이걸 언급하면 "이 사람은 SPOF를 시스템 전체 관점에서 찾아낼 줄 안다"는 인상을 줍니다.