최근에 꽤 규모가 있는 프로젝트를 하면서 아키텍처 설계부터 인프라까지 다뤄보며,
더 큰 트래픽에 대비하기 위해서는 어떻게 해야할지 궁금증이 생겼다.
요즘은 흔히들 AI-Native라고 하여 대부분의 코드 구현은 Codex나 Claude Code와 같은 Agent를 이용하여
개발하고, 설계 단계에서도 많은 도움을 받는 추세이다.
이런 환경에서 개발자의 역할이 이전과는 많이 달라졌음을 느꼈다.
코드가 아니라 아이디어를 통제하라 | GeekNews
AI 시대의 프로그래머는 생성된 코드를 한 줄씩 검토하기보다, 소프트웨어가 담아야 할 아이디어와 설계를 정확히 통제하는 데 시간을 써야 한다고 봄 LLM은 개별 함수처럼 국소적으로 최적인 코
news.hada.io
AI를 사용하면 하루에도 적게는 수천 줄, 많게는 수만줄의 코드가 만들어진다.
이렇게 생성된 모든 코드를 직접 리뷰하고 개선하는것은 어쩌면 불가능하지 않을까라는 생각을 하던 중에
위의 포스트를 보게되었다.
글의 요지는 AI를 통해 생성된 코드를 한 줄 씩 검토하는것보다도 설계를 올바른 방향으로 이끌고,
아이디어 그 자체에 집중하라는 내용이었다.
AI 모델의 성능은 이제 일정 수준의 궤도에 올랐다고 느낀다. 이전에는 아쉬운 부분을 채우기 위해
Superpowers와 같은 스킬을 사용했지만, 모델의 버전이 올라갈수록 스킬을 사용한다고 해서 질적으로 좋은 결과물을
보장하지 못한다고 체감했고, 오히려 토큰의 소비량만 늘어나는 느낌을 받았다.
회사 분들과 이야기를 하던 중에 한 분께서 이 영상을 보시고 스킬들을 지워봤더니 토큰 소비량도 줄고 결과물도 큰 차이가 없다고 하셨다.
나 또한 이미 스킬들을 지운 상태에서의 결과물에 큰 차이를 못느끼고 있던 중이었다.
모델의 성능이 좋아질 수록 사소한 튜닝 조차도 불필요해지고 있다는 생각이 들었다.
그렇기에 이제는 단순한 구현이나 기초적인 코드차원의 설계는 Agent에게 맡기고, 더 큰 차원의 아키텍처, 인프라와 같이
비즈니스적으로 비용이 발생하고, 회사의 내외부 사정까지 고려해야 하는 시스템 디자인에 대해 공부해보려고 한다.
그 첫번째 주제로 사용자가 0명에서 수백만 까지 늘어났을때 서비스가 어떻게 확장되면 좋은지 참고하기 좋은 자료가 있어
해당 자료를 토대로 공부해봤다.
System Design · Coding · Behavioral · Machine Learning Interviews
Ace Every Stage of Your Next Technical Interview
bytebytego.com
Single server setup
가장 기초적인 단일 서버 설정 부터 시작한다. 흔히들 사이드 프로젝트나 작은 규모의 프로젝트에서 많이 경험해 봤을 구성이다.
애플리케이션, 데이터베이스, 캐시 등 모든 구성 요소가 하나의 서버에서 실행되는 구성이다.

위와 같이 하나의 서버에서 모든 역할을 담당하면서 클라이언트와 통신하는 구조가 단일 서버 구조이다.

요청 흐름과 트래픽 소스를 살펴보는 것이 이해에 더 도움이 되기에 간략하게 그 과정을 알아보자.
1. 사용자는 api.mysite.com과 같은 도메인 이름을 통해 웹사이트에 접속한다. DNS가 해당 도메인에 해당하는
ip 주소를 반환한다.
2. ip주소를 얻으면 HTTP 요청이 웹 서버로 직접 전송된다.
3. 웹 서버는 렌더링을 위해 HTML 페이지 또는 JSON 응답을 반환한다.
Database
단일 서버 구조에서는 사용자가 늘어남에 따라 서버의 자원이 부족해질 가능성이 높다.
이럴땐 단순하게 데이터베이스용 서버를 하나 더 만들어 주는것만으로도 서버의 부하를 분산시킬 수 있다.
또한 이러한 구조가 만들어졌다면, 트래픽이 더 커졌을때 웹/모바일 트래픽(웹 계층) 서버와 데이터베이스(데이터 계층) 서버를
각 계층별로 독립적으로 확장할 수 있다.

어떤 데이터베이스를 사용해야 할까?
데이터베이스를 별도의 서버로 분리했다면 어떤 데이터베이스를 사용할지도 고민해야 한다.

크게는 관계형 데이터베이스(RDBMS)와 비관계형 데이터베이스(NoSQL) 중에서 선택할 수 있다.
MySQL, PostgreSQL과 같은 관계형 데이터베이스는 데이터를 테이블과 행의 형태로 저장하고,
각 테이블 간의 관계를 정의하여 SQL의 JOIN 등을 통해 데이터를 조회할 수 있다.
반면 MongoDB, Cassandra, DynamoDB와 같은 NoSQL 데이터베이스는 관계형 데이터베이스와는 다른 방식으로 데이터를 저장한다.
NoSQL이라고 해서 모두 같은 구조를 가지는 것은 아니며, 대표적으로 Key-Value, Document, Column, Graph 등의 형태가 있다.
일반적인 서비스라면 관계형 데이터베이스만으로도 충분한 경우가 많다.
하지만 서비스의 특성에 따라 관계형 데이터베이스가 항상 최선의 선택인 것은 아니다.
예를 들어 다음과 같은 경우에는 NoSQL을 고려해볼 수 있다.
- 매우 낮은 지연 시간이 필요한 경우
- 데이터의 구조가 일정하지 않거나 데이터 간의 관계가 중요하지 않은 경우
- JSON과 같은 형태의 데이터를 그대로 저장하고 조회하는 것이 중요한 경우
- 매우 많은 양의 데이터를 분산하여 저장하고 처리해야 하는 경우
결국 사용자가 많다는 이유만으로 NoSQL을 선택해야 하는 것은 아니다.
서비스에서 다루는 데이터의 형태와 데이터 간의 관계, 필요한 일관성 수준, 읽기/쓰기 패턴 등을 고려하여
적절한 데이터베이스를 선택해야 한다.
이번 글에서는 데이터베이스 자체보다는 서비스가 확장되는 과정에 집중하고 있기 때문에 각각의 데이터베이스에 대한 자세한 내용은 넘어가도록 하겠다
Vertical Scailing (수직 확장) vs Horizontal Scailing(수평 확장)
데이터베이스까지 분리했지만 사용자가 계속해서 늘어난다면 결국 서버가 처리할 수 있는 한계에 도달하게 된다.
이때 서버를 확장하는 방법은 크게 Scale Up(Vertical Scaling)과 Scale Out(Horizontal Scaling) 두 가지로 나눌 수 있다.
Scale Up

Scale Up은 현재 사용하고 있는 서버의 CPU, RAM 등의 사양을 높여 서버 한 대의 성능 자체를 높이는 방법이다.
예를 들어 4 Core CPU와 8GB RAM을 사용하던 서버를 16 Core CPU와 32GB RAM을 가진 서버로 변경하는 방식이다.
단순히 서버의 사양만 높이면 되기 때문에 구조를 변경할 필요가 없고, 트래픽이 크지 않은 상황에서는 가장 간단하게 성능을 높일 수 있는 방법이다.
하지만 Scale Up에는 명확한 한계가 있다.
서버 한 대에 CPU와 메모리를 무한정 추가할 수 없기 때문에 언젠가는 물리적인 한계에 도달하게 된다.
또한 서버가 여전히 한 대뿐이라는 문제도 남아있다.
해당 서버에 장애가 발생하면 이를 대신해서 요청을 처리할 서버가 없기 때문에 서비스 전체에 장애가 발생할 수 있다.
Scale Out

Scale Out은 서버 한 대의 성능을 높이는 대신 서버의 개수를 늘리는 방법이다.
Scale Up과 달리 필요에 따라 서버를 계속 추가할 수 있기 때문에 대규모 트래픽을 처리해야 하는 서비스에서는 Scale Out을 고려하게 된다.
하지만 여기서 새로운 문제가 생긴다.
기존에는 서버가 한 대였기 때문에 사용자가 해당 서버에 바로 요청을 보내면 됐다.
서버가 여러 대가 된다면 사용자의 요청을 어떤 서버로 보내야 할까?
또 특정 서버에만 요청이 몰린다면 서버를 여러 대로 늘린 의미도 없어진다.
이러한 문제를 해결하기 위해 Load Balancer를 사용한다.
Load Balancer
앞에서 Scale Out을 통해 여러 대의 웹 서버를 구성했다.
하지만 서버의 개수만 늘린다고 해서 트래픽이 자동으로 각 서버에 분산되는 것은 아니다.
여러 대의 서버 중 어떤 서버로 요청을 전달할지 결정하고, 특정 서버에 요청이 몰리지 않도록 트래픽을 분산해 줄 역할이 필요하다.
이러한 역할을 하는 것이 Load Balancer이다.
Load Balancer를 추가하면 사용자는 각각의 웹 서버에 직접 요청하지 않고 Load Balancer로 요청을 보낸다.
Load Balancer는 전달받은 요청을 현재 연결되어 있는 여러 웹 서버에 적절하게 분산한다.

이때 외부에 공개되는 IP는 Load Balancer의 Public IP가 되고, 각각의 웹 서버는 Private IP를 이용해 내부 네트워크에서 Load Balancer와 통신하도록 구성할 수도 있다. 따라서 클라이언트가 각각의 웹 서버에 직접 접근할 필요가 없어진다.
장애에도 대응할 수 있다

Load Balancer를 사용하는 또 다른 이유는 가용성(Availability)이다.
예를 들어 두 대의 웹 서버가 동작하고 있다고 가정해보자.

이때 Web Server 1에 장애가 발생한다면 Load Balancer는 정상적으로 동작하고 있는 Web Server 2로 요청을 전달할 수 있다.
서버 한 대에 장애가 발생했다고 해서 서비스 전체가 바로 중단되는 상황을 피할 수 있게 된 것이다.

트래픽이 더 증가하는 경우에도 마찬가지다.
기존 웹 서버만으로 요청을 처리하기 어렵다면 새로운 웹 서버를 추가하고 Load Balancer가 해당 서버에도 요청을 분산하도록 구성할 수 있다.
Load Balancer도 장애가 발생하면?
참고한 게시글에는 별도로 나와있지 않았지만, 문득 여기서 한 가지 궁금증이 생겼다.
앞에서는 하나의 서버에 장애가 발생해도 서비스가 중단되지 않도록 서버를 Scale Out하고, Load Balancer를 통해 여러 서버로 트래픽을 분산시켰다.
그런데 정작 Load Balancer가 하나라면 Load Balancer 자체가 단일 장애 지점(SPOF, Single Point of Failure)이 되는 것이 아닐까?
아무리 Web Server를 여러 대 구성해 놓더라도 모든 요청이 하나의 Load Balancer를 거쳐야 한다면, Load Balancer에 장애가 발생했을 때 결국 사용자의 요청이 Web Server까지 도달하지 못하게 된다.
조금 더 찾아보니 실제 환경에서는 이러한 문제를 방지하기 위해 Load Balancer 자체도 이중화하여 고가용성(HA, High Availability)을 확보하는 방식을 사용한다.

예를 들어 여러 Load Balancer를 구성하여 하나에 장애가 발생하더라도 다른 Load Balancer가 트래픽을 처리하도록 구성할 수 있다.
다만 AWS의 ALB(Application Load Balancer)와 같은 관리형 서비스를 사용하는 경우에는 이러한 이중화 구조를 직접 구성할 필요가 없다.
ALB는 여러 Availability Zone(AZ)에 걸쳐 Load Balancer 노드를 구성할 수 있으며, AWS가 해당 인프라의 가용성과 확장을 관리한다. 따라서 개발자가 보는 아키텍처에서는 하나의 Load Balancer로 표현하더라도, 이를 단순히 하나의 서버가 모든 요청을 처리하고 있는 구조라고 생각할 필요는 없다.
결국 시스템을 확장할 때는 단순히 서버의 개수를 늘리는 것뿐만 아니라, 현재 구조에서 단일 장애 지점이 어디에 존재하는지를 계속해서 확인하는 것도 중요하다.
그리고 이 관점에서 현재 구조를 다시 살펴보면 또 하나의 단일 장애 지점이 보인다.
Database는 여전히 하나뿐이다.
다음으로는 이를 해결하기 위한 Database Replication에 대해 알아보자.
Database Replication
앞에서 Scale Out과 Load Balancer를 통해 Web Server를 여러 대로 구성하면서 웹 계층의 가용성을 높였다.
하지만 현재 구조를 다시 살펴보면 Database는 여전히 하나뿐이다.

Web Server를 아무리 여러 대로 늘리더라도 모든 서버가 하나의 Database를 바라보고 있다면, 사용자가 증가하면서 Database가 새로운 병목 지점이 될 수 있다.
뿐만 아니라 해당 Database에 장애가 발생한다면 데이터를 정상적으로 읽거나 쓸 수 없기 때문에 결국 서비스 전체에 영향을 미치게 된다.
이를 해결하기 위한 방법 중 하나가 Database Replication이다.

Database Replication은 하나의 Database에 저장된 데이터를 다른 Database에도 복제하여 여러 Database가 동일한 데이터를 가지도록 구성하는 방식이다.
대표적으로 하나의 Primary DB와 여러 개의 Replica DB를 구성할 수 있다.

일반적인 Primary/Replica 구조에서는 데이터의 Insert, Update, Delete와 같은 쓰기 작업은 Primary DB에서 처리하고, Primary DB의 데이터를 Replica DB로 복제한다.
반면 데이터 조회와 같은 읽기 작업은 Replica DB로 분산할 수 있다.
많은 서비스에서는 쓰기 작업보다 읽기 작업이 훨씬 많이 발생하는 경우가 있기 때문에, Replica를 여러 대 구성하고 읽기 요청을 분산하면 하나의 Database에 모든 조회 요청이 집중되는 것을 줄일 수 있다.
결과적으로 Database의 읽기 부하를 분산하면서 동시에 데이터의 복제본도 확보할 수 있게 된다.
Database에 장애가 발생한다면?

Database Replication은 성능뿐만 아니라 가용성을 높이는 데에도 사용할 수 있다.
예를 들어 여러 Replica 중 하나에 장애가 발생한다면 해당 Replica로 보내던 읽기 요청을 다른 정상적인 Replica로 전달할 수 있다.
이후 장애가 발생한 Replica를 새로운 Database로 교체하고 Primary의 데이터를 다시 복제할 수 있다.
그렇다면 Primary DB에 장애가 발생하면 어떻게 될까?
Primary는 쓰기 작업을 담당하고 있기 때문에 Replica 하나가 죽는 것보다 문제가 조금 더 복잡해진다.
이 경우 정상적인 Replica 중 하나를 새로운 Primary로 승격시키는 Failover를 수행할 수 있다.

이렇게 하면 기존 Primary에 장애가 발생하더라도 다른 Database가 그 역할을 이어받을 수 있다.
다만 실제 환경에서의 Failover는 이렇게 단순하지만은 않다.
Primary의 데이터가 Replica에 복제되는 과정에서 지연이 발생할 수 있기 때문에 장애가 발생한 순간 Replica가 Primary의 최신 데이터를 모두 가지고 있다고 보장할 수 없는 경우도 있다.
따라서 실제 운영 환경에서는 복제 방식과 데이터 일관성, Failover 과정 등을 함께 고려해야 한다.
이번 글에서는 Database Replication 자체를 깊게 다루기보다는 Database의 부하를 분산하고 장애에 대응하기 위한 방법 정도로 이해하고 넘어가려고 한다.
추가로 Replica DB가 여러 개라면 애플리케이션에서 각각의 DB 주소를 직접 관리해야 하는지 의문이 생겨서 좀 더 알아봤다.
실제 환경에서는 애플리케이션이나 DB Driver가 Replica를 선택하도록 구성할 수도 있지만, 여러 Replica 앞에 Read Endpoint나 Proxy를 두어 읽기 요청을 분산하는 방식도 사용할 수 있다.


예를 들어 Amazon Aurora에서는 Reader Endpoint를 제공하며, 애플리케이션은 각각의 Replica 주소 대신 하나의 Reader Endpoint에 연결하여 여러 Read Replica를 활용할 수 있다.
따라서 개념적으로는 쓰기 요청은 Primary로, 읽기 요청은 여러 Replica로 분산한다고 이해하고, 실제 분산 방식은 사용하는 Database와 인프라에 따라 달라질 수 있다.
Cache
Database Replication을 통해 읽기 요청을 여러 Replica DB로 분산하면서 Database의 부하를 줄일 수 있게 되었다.
하지만 사용자가 계속해서 늘어난다면 또 다른 부분을 생각해볼 수 있다.
자주 조회되는 데이터까지 매번 Database에서 가져올 필요가 있을까?
예를 들어 메인 화면에서 항상 노출되는 상품 정보처럼 동일한 데이터를 수많은 사용자가 반복해서 요청한다고 가정해보자.
데이터가 변경되지 않았음에도 요청이 들어올 때마다 Database를 조회한다면 불필요한 Database 접근이 계속해서 발생하게 된다.
이러한 문제를 줄이기 위해 Cache를 사용할 수 있다.
Cache는 자주 사용되는 데이터를 메모리와 같이 빠르게 접근할 수 있는 공간에 임시로 저장해두고, 동일한 데이터가 다시 필요할 때 Database까지 접근하지 않고 빠르게 반환할 수 있도록 한다.
대표적으로 Redis, Memcached와 같은 In-Memory Cache를 사용할 수 있다.
Cache를 추가하면

Cache를 추가하면 Web Server는 데이터를 조회하기 전에 먼저 Cache를 확인한다.
요청한 데이터가 Cache에 존재한다면 이를 Cache Hit이라고 하며, Database를 조회하지 않고 Cache에 저장되어 있는 데이터를 바로 사용한다.
반대로 데이터가 존재하지 않는다면 Cache Miss가 발생한다.
이 경우 Database에서 데이터를 조회하고, 조회한 데이터를 Cache에 저장한 뒤 사용자에게 반환한다.
이후 동일한 데이터에 대한 요청이 들어오면 Cache에 데이터가 존재하기 때문에 Database까지 접근할 필요가 없어진다.
결과적으로 자주 조회되는 데이터에 대한 응답 시간을 줄이면서 Database에 발생하는 읽기 부하 역시 줄일 수 있다.
모든 데이터를 Cache에 저장하면 될까?
그렇다고 모든 데이터를 Cache에 저장한다고 해서 항상 좋은 것은 아니다.
Cache는 어디까지나 원본 데이터를 대신하는 영구적인 저장소가 아니라 빠른 조회를 위해 데이터를 임시로 저장해두는 계층이다.
따라서 자주 조회되지만 상대적으로 변경이 적은 데이터에 사용하는 것이 효과적이다.
또한 Cache를 사용할 때는 데이터의 만료 시간(TTL) 역시 고려해야 한다.
예를 들어 어떤 데이터를 10분 동안 Cache에 저장하도록 했다면 해당 시간이 지나면 Cache에서 제거하고, 다음 요청에서 다시 Database의 최신 데이터를 조회할 수 있다.
TTL을 너무 짧게 설정하면 Cache가 자주 만료되어 Database를 반복해서 조회하게 되고, 반대로 너무 길게 설정하면 Database의 데이터는 변경되었지만 Cache에는 이전 데이터가 남아있는 Stale Data 문제가 발생할 수 있다.
따라서 서비스에서 해당 데이터가 얼마나 자주 조회되고, 얼마나 자주 변경되는지를 고려하여 적절한 Cache 정책을 정하는 것이 중요하다.
CDN (Content Delivery Network)
Cache를 사용하면서 반복적으로 조회되는 데이터에 대해 Database까지 접근하는 횟수를 줄일 수 있게 되었다.
하지만 사용자가 서버로부터 전달받는 것은 Database에 저장된 데이터뿐만이 아니다.
웹 페이지를 구성하는 이미지, CSS, JavaScript, 영상과 같은 정적 파일들도 사용자에게 전달해야 한다.
이러한 정적 파일까지 매번 Web Server에서 직접 전달한다면 사용자가 증가할수록 서버와 네트워크에 불필요한 부하가 발생하게 된다.
또 다른 문제는 사용자와 서버 사이의 물리적인 거리이다.
예를 들어 서버가 한국에만 존재한다고 가정해보자.
한국에서 접속하는 사용자와 미국에서 접속하는 사용자가 동일한 서버에서 파일을 받아간다면, 일반적으로 미국 사용자가 데이터를 전달받기까지 더 많은 네트워크 구간을 거쳐야 한다.
이러한 문제를 해결하기 위해 사용할 수 있는 것이 CDN(Content Delivery Network)이다.
CDN은 어떻게 동작할까?
CDN은 여러 지역에 분산되어 있는 서버에 정적 콘텐츠를 캐싱해두고, 사용자와 가까운 서버에서 콘텐츠를 전달하는 방식이다.
기존에는 사용자가 정적 파일을 요청하면 Origin Server까지 요청이 전달됐다.


CDN을 사용하면 중간에 CDN이 위치하게 된다.

예를 들어 사용자가 image.png를 요청했다고 가정해보자.
처음 요청에서는 CDN에 해당 파일이 존재하지 않을 수 있다.
이 경우 CDN은 Origin Server에서 파일을 가져온 뒤 이를 자신의 Cache에 저장하고 사용자에게 반환한다.
이후 다른 사용자가 동일한 image.png를 요청하면 이미 CDN에 파일이 존재하기 때문에 Origin Server까지 요청하지 않고 바로 반환할 수 있다.
Cache와 CDN은 뭐가 다를까?
앞에서 Cache를 설명했기 때문에 여기서 둘이 비슷하게 느껴질 수도 있다.
둘 다 자주 사용하는 데이터를 미리 저장해두고 빠르게 반환한다는 점에서는 비슷하다.
하지만 이 글에서 다루는 역할을 단순하게 구분하면 다음과 같다.
| Cache | CDN | |
| 위치 | Web Server와 Database 사이 | 사용자와 Origin Server 사이 |
| 주로 저장하는 것 | 자주 조회되는 데이터 | 이미지, CSS, JS, 영상 등의 정적 파일 |
| 주요 목적 | DB 조회 감소 | Origin 부하 감소 + 전송 지연 감소 |
| 대표적인 예 | Redis, Memcached | CloudFront, Cloudflare CDN |
마무리

처음에는 하나의 서버에서 시작했지만 사용자가 증가하면서 하나씩 발생하는 문제를 해결하다 보니 처음과는 꽤 다른 구조가 만들어졌다.
서버의 처리량이 부족해지면서 Scale Out과 Load Balancer를 적용했고, Database 역시 Replication을 통해 부하를 분산하고 장애에 대응할 수 있도록 구성했다.
여기에 Cache를 통해 반복적인 Database 조회를 줄이고, CDN을 통해 정적 콘텐츠를 Origin Server까지 요청하지 않고 사용자와 가까운 위치에서 전달할 수 있게 되었다.
하지만 이것만으로 수백만 사용자를 안정적으로 처리할 수 있는 시스템이 완성되는 것은 아니다.
다음 글에서는 Stateless Web Tier, Auto Scaling, Multi Data Center, Message Queue, Database Sharding 등 시스템을 더 큰 규모로 확장하기 위해 필요한 구조에 대해 이어서 알아보려고 한다.
'Infra' 카테고리의 다른 글
| 사용자 0명에서 수백만 명까지, 서비스는 어떻게 확장될까? #2 (1) | 2026.09.29 |
|---|---|
| Elasticsearch 알아보기 (0) | 2026.03.18 |
| Kafka란? (feat.성능과 고가용성) (0) | 2025.03.26 |
| 클러스터와 노드 개념 (0) | 2025.03.24 |
| Nginx 알아보기 (0) | 2025.03.04 |