목차9개 항목
출처와 참고
NAVER D2의 「네이버 메인 페이지의 트래픽 처리」(2018.11)를 바탕으로 재구성하고, 서킷 브레이커 라이브러리와 지표 수집 도구는 지금 기준으로 고친 노트다.
무엇이 문제인가
포털 첫 화면, 뉴스 속보 페이지처럼 누구나 같은 내용을 보는 읽기 위주 서비스는 평소에도 트래픽이 많고, 지진·스포츠 경기 득점 같은 사건이 터지면 수십 초 만에 평소의 몇 배로 치솟는다. 이때 목표는 하나다. 무슨 일이 있어도 빈 화면을 내보내지 않는다.
로드 밸런서 → 웹 서버 → WAS → DB로 이어지는 흔한 3계층 구조(웹 확장성의 구성 요소의 3-Tier)는 계층마다 다중화의 난이도가 다르다.
| 계층 | 다중화 | 어려운 점 |
|---|---|---|
| 로드 밸런서 | 여러 대 두고 DNS 라운드로빈 등으로 나눈다 | 비교적 쉽다 |
| WAS | 여러 대 두고 웹 서버가 살아 있는 WAS를 찾게 한다 | 로그인 세션을 공유하려면 세션 클러스터링이나 외부 세션 저장소가 필요하다 |
| DB | 복제·분산 | 데이터 동기화와 정합성 문제가 따라온다. 가장 어렵다 |
읽기 위주 서비스는 이 어려움을 쓰기와 상태를 없애는 쪽으로 피한다.
설계 원칙
| 원칙 | 뜻 |
|---|---|
| 무상태(stateless) | 어느 서버에 붙어도 같은 내용을 보여 준다. 로그인 여부 같은 상태에 서버가 의존하지 않는다 |
| 빈 페이지 금지 | 뒤쪽이 전부 죽어도 무언가는 보여 준다 |
| 빠른 실패(fail fast) | 연동하는 외부 시스템은 언제든 느려질 수 있다고 가정하고, 기다리지 말고 포기한다 |
| 수평 확장 | 트래픽이 몰리면 서버를 늘리는 것만으로 대응할 수 있어야 한다 |
| 앞단에서 처리 | 간단한 일은 뒤쪽(WAS)까지 보내지 않는다 |
앞단에서 부하 덜기
요청이 뒤로 갈수록 처리 비용이 비싸진다. 가능한 한 앞에서 끝낸다.
flowchart TB user["사용자"] --> cdn["CDN 엣지<br/>정적 자산(JS·CSS·이미지)은 여기서 끝"] cdn --> web["웹 서버<br/>파일 읽기·쿠키 판별·단순 API 중계는 여기서 끝"] web --> was["WAS<br/>화면 조립에 꼭 필요한 로직만"] was --> ext["외부 시스템·DB"]
| 단계 | 하는 일 | 효과 |
|---|---|---|
| CDN | 잘 바뀌지 않는 공통 리소스를 엣지에 둔다. GSLB(Global Server Load Balancing)가 사용자와 가까운 엣지로 보낸다 | 100KB 이미지를 10만 명이 보면 약 10GB다. 이 전송량이 오리진에서 빠진다 |
| 웹 서버의 SSI | SSI(Server Side Includes)는 Apache·Nginx가 지원하는 간단한 서버 스크립트다. 파일 끼워 넣기, 쿠키 유무 분기 정도를 웹 서버가 처리한다 | WAS까지 갈 요청이 줄어든다 |
| 웹 서버 확장 모듈 | Apache 모듈(C)이나 Nginx 모듈로 단순 외부 API 호출, WAS 장애 시 우회 같은 로직을 웹 서버에 넣는다 | WAS 의존도가 더 줄고 성능도 좋다 |
웹 서버와 WAS의 역할 분담 자체는 웹 서버, WAS, 그리고 Nginx에 있다.
정적 폴백: 마지막 정상 응답을 파일로 둔다
빈 화면을 막는 핵심 장치는 미리 만들어 둔 정상 응답이다. 별도 배포 작업(퍼블리셔)이 주기적으로 화면 조각과 외부 API의 정상 결과를 파일로 떠서 웹 서버에 깔아 둔다.
flowchart LR web["웹 서버"] --> a["WAS A 호출"] a -->|"실패"| b["같은 데이터센터의 WAS B 호출"] b -->|"실패"| s["오류 대신 미리 깔아 둔 정적 파일 반환"]
일반적인 리버스 프록시 설정은 WAS가 응답하지 않으면 그대로 502·504를 낸다. 웹 서버가 다른 WAS 재시도 → 정적 파일까지 책임지면, 모든 WAS가 죽어도 웹 서버만 살아 있으면 서비스는 계속된다. 내용은 조금 오래될 수 있지만 빈 화면보다 낫다. Nginx라면 proxy_next_upstream(다음 업스트림 재시도)과 error_page(실패 시 정적 파일)로 비슷한 동작을 만든다.
같은 생각을 HTTP 캐시 차원에서 표준화한 것이 stale-if-error 지시어다. 오리진이 오류를 내면 만료된 캐시라도 내보내게 한다(HTTP 캐시).
서킷 브레이커: 장애 전파 끊기
외부 시스템 하나가 느려지면 장애는 이렇게 번진다.
flowchart TB n1["트래픽 폭증"] --> n2["외부 API 응답 지연"] --> n3["타임아웃"] --> n4["재시도"] n4 --> n5["처리 못 한 요청 위에 재시도가 쌓임"] --> n6["외부 API 다운"] --> n7["우리 서비스도 다운"]
일시적 오류라면 두세 번 재시도로 풀리지만, 진짜 장애에서 재시도는 상대의 복구를 방해할 뿐이다. 서킷 브레이커(circuit breaker) 는 실패가 일정 수준을 넘으면 호출 자체를 막고 준비된 응답을 돌려준다. 전기 회로처럼 닫힘이 정상, 열림이 차단이다.
| 상태 | 동작 | 전이 |
|---|---|---|
| Closed | 정상 호출한다. 실패율을 센다 | 실패율이 임계치를 넘으면 Open |
| Open | 호출하지 않고 즉시 폴백(실패 값이나 미리 저장한 정상 응답)을 반환한다 | 정해진 시간이 지나면 Half-Open |
| Half-Open | 시험 삼아 몇 건만 보낸다 | 성공하면 Closed, 실패하면 다시 Open |
폴백 값으로 위의 정적 폴백 파일을 쓰면, 외부 시스템이 죽은 동안에도 화면은 정상처럼 그려진다.
| 환경 | 쓰는 것 |
|---|---|
| Java·Spring | Resilience4j, 또는 이를 감싼 Spring Cloud Circuit Breaker |
| Node.js | opossum |
| 서비스 메시 | Istio·Envoy의 이상치 감지(outlier detection)로 애플리케이션 밖에서 처리 |
과거에는 Netflix Hystrix(Node.js는 HystrixJS)가 표준이었다. Hystrix는 2018년 말 유지 보수 모드로 들어갔고, Spring Cloud도 2020.0 릴리스에서 Hystrix 지원을 뺐다.
서비스 디스커버리: 늘어난 서버를 자동으로 찾기
서버 목록을 설정 파일에 박아 두면, 트래픽 때문에 서버를 늘려도 호출하는 쪽은 재시작하기 전까지 새 서버를 모른다. 운영 중 재시작은 롤링이라도 위험하다.
서비스 디스커버리(service discovery) 는 서버가 뜰 때 자기 IP·포트를 레지스트리에 등록하고, 호출하는 쪽은 레지스트리에서 최신 목록을 받아 쓰게 한다. 오토스케일링으로 서버가 수시로 생겼다 사라지는 클라우드에서 필수다. 쿠버네티스에서는 Service와 클러스터 DNS가 이 역할을 기본으로 맡는다(쿠버네티스 위 웹 서비스의 요청 경로와 구성 요소).
한계치 모니터링과 자동 방어 모드
구조를 갖춰도 시스템이 버틸 수 있는 한계치를 숫자로 정해 두지 않으면 급증 순간에 판단이 늦는다.
- 지표 수집: 애플리케이션이 요청 수·지연·오류율을 내보내고 대시보드로 본다. Spring Boot라면 Actuator와 Micrometer로 수집해 Prometheus·Grafana로 시각화하는 구성이 흔하다.
- 자동 방어 모드: 사람이 24시간 지킬 수는 없으니 애플리케이션이 스스로 판단한다.
| 방아쇠 | 판단 방법 |
|---|---|
| 트래픽 예측 | 실시간 트래픽의 이동평균 추세로 일정 시간 뒤 한계치 도달이 예상되면 발동 |
| 외부 신호 | 지진 조기 경보처럼 폭증이 확실한 외부 이벤트를 받으면 즉시 발동 |
방어 모드에서는 모든 외부 연동을 끊고 자체 서버와 정적 데이터만으로 서비스한다. 기능 일부를 포기하고 핵심 화면을 지키는 우아한 기능 저하(graceful degradation) 이자, 폭증한 트래픽이 외부 시스템까지 흘러가 연쇄 장애를 일으키지 않게 막는 장치다. 발동 직전 외부 API 오류율이 치솟다가, 연결을 끊은 뒤 가라앉는 모양이 전형적이다.
한계
- 이 방식은 읽기 위주·무상태라서 통한다. 주문·결제처럼 쓰기와 트랜잭션이 중심인 서비스는 정적 폴백을 쓸 수 없고, 대기열·데이터 분산 같은 다른 해법이 필요하다.
- 정적 폴백과 방어 모드는 조금 오래된 데이터를 보여 준다. 실시간성이 생명인 정보(점수, 재고)는 무엇을 포기할지 미리 정해야 한다.
- 서킷 브레이커 임계치를 너무 낮게 잡으면 일시적 지연에도 회로가 열려 멀쩡한 기능이 꺼진다.
정리
- 읽기 위주 서비스는 무상태로 만들어 서버만 늘리면 버티는 구조를 먼저 갖춘다.
- 요청은 CDN → 웹 서버 → WAS 순으로 가능한 한 앞에서 끝낸다.
- 뒤쪽이 모두 죽어도 미리 떠 둔 정적 응답으로 빈 화면을 막는다.
- 외부 의존성은 빠른 실패 + 서킷 브레이커 + 폴백으로 감싸 장애 전파를 끊는다.
- 한계치를 숫자로 정하고, 넘을 것 같으면 자동으로 기능을 줄여 핵심을 지킨다.
관련 노트: CDN, HTTP 캐시, 웹 서버, WAS, 그리고 Nginx, 프록시: 포워드 프록시와 리버스 프록시, 웹 확장성의 구성 요소, 쿠버네티스 위 웹 서비스의 요청 경로와 구성 요소, 와이드 컬럼 DB와 대규모 메시지 저장: Cassandra·ScyllaDB, 캐시 클러스터 운영과 장애 대응, 고가용성과 재해 복구 설계, 서버 장애 진단 패턴