1. 3-Tier Architecture 설계
상황: 작은 이커머스 서비스 구축. 기능은 사용자 로그인, 상품 조회, 상품 주문, 회원 정보 저장, 주문 정보 저장.
1) 서버 한 대에 웹서버+애플리케이션+DB를 모두 설치
문제: 사용자가 늘자 서버가 느려졌음. 웹 처리가 느린 건지 주문 로직이 느린 건지 DB 조회가 느린 건지 구분이 안 됨. 하나를 고치려고 재시작하면 나머지도 같이 멈춤.
질문: 이 서버를 어떤 기준으로 나눠야 원인 분리와 개별 확장이 가능해지는가?
구체적인 설명
서버 한 대에서 모니터링 지표(CPU, 메모리, 디스크 I/O)를 봐도 원인을 못 찾는 이유는, 세 가지 완전히 다른 작업 패턴이 하나의 리소스 풀을 공유하고 있기 때문이다.
- 웹 서버(정적 파일 응답, 요청 라우팅) → 네트워크 I/O 위주
- 애플리케이션(주문 검증, 재고 계산 같은 비즈니스 로직) → CPU 연산 위주
- DB(조회, 쓰기, 인덱스 스캔) → 디스크 I/O + 메모리(캐시) 위주
한 서버에 다 있으면 "CPU 사용률 80%"라는 지표 하나가 애플리케이션 로직 때문인지, DB가 디스크를 읽으면서 커널 레벨에서 CPU를 같이 쓴 건지 구분이 안 된다. 그리고 세 프로세스가 메모리를 두고 경쟁하기 때문에, DB 캐시가 커지면 애플리케이션이 쓸 메모리가 줄어드는 식으로 서로한테 영향을 준다.
나누는 기준은 **역할(계층)**이다. 사용자를 직접 상대하는 계층(Presentation), 비즈니스 로직을 처리하는 계층(Application), 데이터를 영속적으로 저장하는 계층(Data) — 이렇게 셋으로 나누면:
- 각 서버의 지표(CPU/메모리/디스크)가 온전히 그 계층의 작업량만 반영하게 되어 원인 분리가 가능해짐
- 하나를 재시작해도 나머지 두 계층은 프로세스 자체가 물리적으로 분리돼 있어 영향을 안 받음
- 계층마다 필요한 리소스가 다르므로(웹은 네트워크, DB는 메모리·디스크 위주로), 각자에 맞는 사양으로 서버를 고를 수 있음
핵심 개념
- 3-Tier Architecture: Presentation(표현) / Application(응용) / Data(데이터) 3개 계층으로 물리적·논리적으로 분리하는 설계
- 핵심 목적: 장애 격리(Isolation) + 원인 관측 가능성(Observability) + 계층별 독립적 확장
2) Web / Application / DB 세 대로 나눔
문제: 사용자가 100명에서 10,000명으로 늘어남. Application 서버만 CPU 100%. 더 큰 서버로 교체해도 잠깐 버티다 다시 한계, 교체할 때마다 서비스를 내려야 함.
질문: 서버를 키우는 것 말고 다른 확장 방법은? 두 방식의 한계는 각각 무엇인가?
구체적인 설명
서버 확장에는 두 축이 있다.
Vertical Scaling(수직 확장, Scale-up)
- 서버 1대의 CPU 코어 수, 메모리 용량을 올림 (예: AWS EC2라면
t3.medium→t3.2xlarge처럼 인스턴스 타입을 교체)
- 한계 ①: 아무리 좋은 인스턴스 타입도 물리적 상한선이 있음 (클라우드 제공사가 제공하는 최대 스펙까지만 가능)
- 한계 ②: 인스턴스 타입 변경은 보통 인스턴스 중지 → 타입 변경 → 재시작이 필요해서 몇 분간 서비스가 끊김(다운타임)
- 한계 ③: 트래픽이 다시 늘면 또 똑같은 작업을 반복해야 함 — 근본적 해결이 아니라 "미루는" 것
Horizontal Scaling(수평 확장, Scale-out)
- 같은 사양의 서버를 여러 대로 늘림 (Application 서버 1대 → 3대, 5대…)
- 장점: 이론상 대수를 늘리는 데 제한이 거의 없고, 한 대를 새로 추가/제거해도 나머지는 서비스 중단 없이 계속 동작
- 다만 이 방식이 실제로 작동하려면, 여러 대의 서버를 "하나처럼" 다루기 위한 앞단 장치가 필요해짐 — 사용자가 서버 여러 대 중 어디로 접속해야 할지 모르는 문제가 새로 생기기 때문 (다음 단계로 이어짐)
핵심 개념
- Scale-up: 서버 성능 자체를 올림. 빠르고 간단하지만 상한선·다운타임 존재
- Scale-out: 서버 대수를 늘림. 무중단·거의 무제한 확장 가능하지만 트래픽 분산 장치가 필수로 따라와야 함
3) Application 서버를 두 대로 늘림
문제: 사용자는 여전히 서버 한 대의 주소(IP/도메인)로만 접속함. 두 번째 서버는 트래픽을 전혀 받지 못함.
질문: 사용자 요청을 두 서버에 나눠주려면 앞단에 어떤 역할이 있어야 하는가? 그 역할은 어떤 기준으로 요청을 나누는가?
구체적인 설명
서버를 아무리 늘려도, "사용자가 알고 있는 주소"가 한 대만 가리키고 있으면 나머지는 죽은 자원이다. 여기 필요한 게 Load Balancer다.
동작 방식:
- 사용자는 로드밸런서의 주소 하나만 알고 접속함 (뒤에 App 서버가 몇 대인지는 전혀 모름)
- 로드밸런서가 요청을 받아서, 미리 등록된 서버 목록(Target Group) 중 하나로 트래픽을 전달
- 분배 기준(알고리즘)은 여러 가지가 있음:
- Round Robin: 순서대로 돌아가면서 배정 (서버1 → 서버2 → 서버1 → …)
- Least Connections: 현재 연결 수가 가장 적은 서버로 배정 (부하가 균등하지 않을 때 더 정교함)
- 뒤에서 서버를 추가하거나 빼도(Auto Scaling으로 자동 증감해도), 로드밸런서의 Target Group만 갱신되면 되고 사용자 쪽 주소는 전혀 안 바뀜 — 이게 로드밸런서가 "추상화 계층" 역할을 한다는 의미
핵심 개념
- Load Balancing: 여러 서버로 요청을 분산시키는 기법. Horizontal Scaling이 실제로 작동하기 위한 필수 전제조건
- AWS에서는 **ALB(Application Load Balancer)**가 이 역할 — HTTP/HTTPS 레벨(7계층)에서 URL 경로 기반 라우팅까지 가능
4) 앞단에 요청 분산 장치를 둠
문제: 로그인한 사용자가 페이지를 넘길 때마다 로그아웃됨. 요청이 App1로 갔다가 App2로 가면, App2는 이 사용자를 모름.
질문: 서버가 "이 사용자를 기억하는 상태"를 어디에 두어야 서버를 늘려도 문제가 없는가?
구체적인 설명
원인은 "로그인했다"는 정보(세션, Session)가 App1의 로컬 메모리 안에만 저장돼 있었기 때문이다. 로드밸런서는 매 요청을 어느 서버로 보낼지 그때그때 결정하기 때문에, 같은 사용자의 요청이라도 App1, App2로 번갈아 갈 수 있다. App2 입장에서는 이 사용자의 로그인 기록이 자기 메모리에 없으니 "로그인 안 한 사용자"로 취급하고 다시 로그인 화면을 보여준다.
두 가지 서버 설계 방식을 구분해야 한다.
- Stateful: 서버가 사용자별 상태(세션 등)를 자기 메모리/디스크에 들고 있음. 이 방식이면 같은 사용자의 다음 요청은 반드시 같은 서버로 가야만 정상 동작함(이걸 강제하는 걸 Sticky Session이라 하는데, 이러면 결국 그 서버에 부하가 쏠리는 문제가 다시 생김)
- Stateless: 서버 자체는 아무 상태도 안 갖고 있고, 매 요청마다 "이 사용자가 누구인지"를 확인하려면 외부의 공용 저장소를 조회함
해결책: 세션 데이터를 App 서버 밖의 별도 공용 저장소(예: Redis, Memcached 같은 인메모리 DB)로 빼낸다. 그러면 어떤 App 서버가 요청을 처리하든, 그 공용 저장소에 "이 사용자 로그인 상태"를 물어보면 동일한 답을 얻는다 — 서버가 몇 대든 상관없이 사용자 경험은 끊기지 않는다.
핵심 개념
- Stateful vs Stateless: 서버가 사용자 상태를 자기 안에 보관하는지 여부
- Horizontal Scaling이 되려면 애플리케이션 서버는 반드시 Stateless로 설계하고, 상태는 서버 밖 공용 저장소로 분리해야 함 (AWS라면 ElastiCache for Redis)
5) 세션을 서버 밖으로 뺌
문제: App1이 죽음. 로드밸런서는 이걸 모르고 여전히 App1로도 요청을 보냄. 사용자 절반이 에러를 봄.
질문: 서버가 죽었다는 것을 누가 어떻게 판단하고, 죽은 서버를 어떻게 트래픽에서 빼는가? 한 대가 죽어도 서비스가 유지되려면 최소 몇 대가 필요한가?
구체적인 설명
로드밸런서는 기본적으로 "서버 상태"를 스스로 알지 못한다. 그래서 로드밸런서가 각 서버에게 정기적으로(예: 30초마다) 확인 요청을 보내야 한다. 이걸 Health Check라고 한다.
동작 방식:
- 로드밸런서가 각 서버의 특정 경로(예:
/health)로 HTTP 요청을 주기적으로 보냄
- 정상이면 200 OK 응답, 서버가 죽었거나 응답이 없으면 실패로 판단
- 연속으로 일정 횟수(예: 3회) 실패하면 → 그 서버를 자동으로 Target Group에서 제외, 이후 트래픽을 안 보냄
- 서버가 다시 정상 응답하면 → 자동으로 다시 트래픽 대상에 포함
"몇 대가 필요한가"에 대한 답은 최소 2대 이상. 1대뿐이면 그 서버가 죽는 순간 대체할 서버가 없어서 전체 서비스가 멈춘다(단일 장애점, Single Point of Failure). 이렇게 여분의 자원을 미리 두는 것을 **Redundancy(이중화)**라 부르고, 이런 식으로 일부가 죽어도 전체 서비스는 유지되는 상태를 **High Availability(고가용성, HA)**라고 한다.
핵심 개념
- Health Check: 로드밸런서가 서버 생존 여부를 주기적으로 확인하고, 죽은 서버를 자동으로 트래픽 대상에서 제외하는 메커니즘
- High Availability: 일부 구성 요소가 죽어도 서비스 전체는 계속 정상 동작하는 상태
- Redundancy: 여분의 자원(서버, 인스턴스 등)을 미리 확보해두는 것 — HA를 실현하는 방법
6) App 서버를 다중화함
문제: DB는 아직 한 대이고, 처음에 편하다고 App1과 같은 서버에 뒀음. App1을 배포하느라 재시작했더니 DB도 같이 내려갔고, App2까지 전부 멈춤.
질문: 하나가 죽었을 때 같이 죽는 범위는 어디까지인가? DB를 왜 애플리케이션과 다른 장애 범위에 두어야 하는가?
구체적인 설명
DB를 App1과 같은 물리 서버(또는 같은 인스턴스)에 뒀다는 건, 이 둘이 하나의 장애 단위로 묶였다는 뜻이다. App1을 배포·재시작하면 그 서버의 프로세스 전체(App1 + DB)가 같이 내려간다. 그리고 DB가 죽으면, DB에 의존하는 App2도(같은 물리 서버에 있지 않더라도) DB 접속이 안 되니 서비스가 안 된다. 결국 App1 서버 하나의 재시작이 전체 서비스 마비로 번진 것이다.
이렇게 "하나의 장애가 함께 영향을 미치는 범위"를 **Failure Domain(장애 범위, 장애 도메인)**이라고 한다. 잘 설계된 시스템은 장애 범위를 최대한 잘게, 서로 독립적으로 쪼개서 하나가 죽어도 다른 게 안 죽게 만든다.
해결책: DB를 App 서버와 **물리적으로 완전히 별도의 서버(인스턴스)**로 독립시킨다.
- App1을 재시작·배포해도 DB는 별개 프로세스/인스턴스이므로 영향받지 않음
- App1이 죽어도 App2는 여전히 DB에 접근 가능해서 서비스 지속
- DB는 데이터(상태)를 들고 있는 유일한 계층이라, App(Stateless)보다 훨씬 신중하게 다뤄야 함 — 예를 들어 DB는 Multi-AZ(서로 다른 물리적 데이터센터) 복제로 별도의 이중화를 하기도 함
핵심 개념
- Failure Domain: 하나의 장애가 전파되는 범위. 장애 범위를 독립적으로 최대한 작게 쪼개는 게 안정적인 설계의 기본 원칙
- App(무상태, 쉽게 늘리고 죽여도 됨) vs DB(상태 보유, 더 신중하게 관리) — 이 성격 차이 때문에 애초에 3-Tier에서 Data 계층을 별도로 분리하는 것
7) 구조가 정리됨 — AWS 서비스 매핑
질문: 지금까지 그린 구조의 각 역할(가상 네트워크, 요청 분산, 컴퓨팅, 관계형 DB, DNS)을 AWS에서는 어떤 서비스가 담당하는가?
구체적인 설명 + 매핑표
지금까지 만든 역할 | AWS 서비스 | 구체적인 담당 기능 |
가상 네트워크 | VPC (Virtual Private Cloud) | 클라우드 안에 논리적으로 격리된 내 전용 네트워크 구역. Subnet, 라우팅 등 이후 모든 네트워크 구성의 기반 |
요청 분산 장치 | ALB (Application Load Balancer) | Round Robin/Least Connections 기반 트래픽 분산 + Health Check 기본 내장 |
컴퓨팅 (Web/App 서버) | EC2 + Auto Scaling Group | 가상 서버 인스턴스. ASG를 함께 쓰면 "다중화"를 트래픽에 따라 자동으로 늘리고 줄여줌 |
세션 저장소 | ElastiCache for Redis | App 서버 밖에 두는 공용 세션 저장소 |
관계형 DB | RDS | 관리형 DB. Multi-AZ 옵션을 켜면 "DB 독립 장애범위" 요구사항을 AWS가 자동으로 구현해줌(자동 장애조치 포함) |
DNS (도메인 → 서버 주소 연결) | Route 53 | 사용자가 도메인으로 접속하면 실제 ALB 주소로 연결해주는 DNS 서비스 |
정리하면: 앞서 하나씩 부딪히며 해결해온 문제들(계층 분리, 수평 확장, 트래픽 분산, 세션 외부화, 헬스체크·다중화, 장애범위 분리)을, 실제 AWS는 이미 관리형 서비스 형태로 제공한다. VPC 안에 EC2(Web/App)를 여러 대 띄우고 ALB로 앞단에서 분산하며, 세션은 ElastiCache로, DB는 RDS로 분리하고, Route 53으로 도메인을 연결하는 구조가 지금까지 그려온 설계의 실제 구현체다.
✏️ 정리하면서 든 생각
3-Tier Architecture는 처음부터 정답으로 존재하는 게 아니라, 문제를 하나씩 겪으면서 필요에 의해 만들어진 구조라는 걸 알게 됐다. 서버 분리 → 로드밸런싱 → 세션 외부화 → 헬스체크/다중화 → 장애 범위 분리라는 순서가, 실제로 서비스가 커질 때 마주치는 순서와 정확히 일치한다는 점이 인상 깊었다.

