콘텐츠로 이동

OSI 모델 & Web Server

분류: Layer 2 - 인프라 기초 | 작성일: 2026-03-22

OSI 모델은 네트워크 통신을 7개 계층으로 나눈 개념 모델이고, Web Server(Nginx 등)는 HTTP 요청을 받아 정적 파일을 제공하거나 애플리케이션 서버로 요청을 전달하는 소프트웨어다.

네트워크 문제가 생겼을 때 “어느 계층에서 문제인가”를 판단하는 기준이 OSI 모델이다. Web Server는 Nest.js 앞에 Nginx가 붙어 있는 구성에서 요청이 어떻게 흘러들어오는지 이해하는 데 필요하다. “80포트로 들어온 요청이 3000번 포트의 Node.js 서버에 도달하는가”의 답이 여기에 있다.

2.5 선행 기술의 한계 — OSI · Web Server가 풀려던 문제

섹션 제목: “2.5 선행 기술의 한계 — OSI · Web Server가 풀려던 문제”

이 토픽은 서로 다른 두 궤적의 추상화가 한 문서에 묶여 있다. “왜 이 모델이 필요했는가”는 그 이전 세계가 무엇 때문에 못 돌아갔는지를 봐야 답이 나온다. 단순 역사 메모가 아니라, 두 추상화가 지금 사라지면 무엇이 깨지는지를 동시에 짚는다.

OSI 7계층 — proprietary protocol incompatibility의 해결

1970년대 중반 네트워킹은 벤더 잠금이 기본값이었다. IBM SNA(1974), DEC DECnet(1975), Xerox XNS, Burroughs BNA 모두 자체 계층 구조를 갖고 있었으나 서로 호환되지 않았다 — 멀티벤더 환경에서 두 네트워크를 연결하려면 게이트웨이를 매번 작성해야 했다 (Wikipedia: Protocol Wars). ISO는 1977년 시드니 회의에서 표준화를 시작해 1984년 OSI 7계층 모델을 공식 발표했다 (Wikipedia: OSI model). 실제 인터넷 표준 전쟁은 TCP/IP가 승리했지만, 계층 인터페이스를 표준화해 벤더 종속을 끊는다는 OSI의 어휘 자체가 살아남아 오늘날 진단 도구가 됐다.

선행 토픽 L1/http-basics는 7계층에서 끝난다 — HTTP만 알면 4xx/5xx는 보이지만 그 아래 TCP/IP/TLS는 블랙박스다. OSI 모델은 이 블랙박스를 4·6·3계층으로 분해해 “HTTPS 인증서 만료(L6)“와 “Security Group 차단(L4)“를 같은 단어로 부르지 않게 한다.

사라지면 깨지는 것: 멀티벤더·멀티프로토콜 환경의 문제 분류 공통어가 사라져 oncall이 진단 범위를 좁히지 못한다.

Web Server — Apache prefork의 C10K wall 해결

Apache 1.x(1995~)는 prefork MPM이었다 — 부모 프로세스가 자식 프로세스를 미리 띄워두고 연결마다 하나씩 점유한다. mod_php·CGI 시절엔 요청마다 새 PHP 인터프리터 프로세스를 spawn했다 (Apache mod_cgi 공식). 1999년 Dan Kegel의 “The C10K problem”이 이 모델의 정량 한계를 못박았다: 32-bit Linux는 thread당 기본 2MB 스택을 잡으므로 가상 메모리가 ~512 thread에서 고갈된다. 하드웨어가 받쳐줘도 OS가 동시 1만 연결 전에 무너졌다.

해결책은 하나의 thread가 다수 fd를 비동기로 감시하는 방향이었다 — Linux 2.6의 epoll, FreeBSD/macOS의 kqueue. Igor Sysoev가 2002년 rambler.ru 트래픽을 받아내려고 작성한 Nginx는 이 두 syscall을 기본 channel로 채택했다. 3절의 “Nginx 50MB vs Apache Prefork 2.5GB”(keepalive 10K) 비교가 이 차이의 결과다 — 연결당 메모리 비용이 prefork는 선형, event-loop는 거의 평탄.

사라지면 깨지는 것: 단일 코어로 수만 연결을 처리하는 가정이 무너진다. 3절에 나오는 Node.js libuv, Redis, HAProxy/Envoy 전부 동일 모델 위에 있으므로 모던 reverse proxy 생태계 전체가 재설계 대상이 된다.

언제 이 lineage 자체가 깨지는가 (Inversion)

두 추상화 모두 만능이 아니다. 도입 후 이상 신호로 감지해야 할 silent failure에 해당한다.

  • OSI 7계층의 모호 영역 — QUIC/HTTP3: QUIC은 UDP(L4) 위에 TLS(L6)와 connection 관리(L5)를 함께 패킹한다. 같은 패킷이 transport·session·presentation 의미를 동시에 갖는다. 감지법: tcpdump -i any 'udp port 443'에서 트래픽이 보이는데 ss -tan(TCP만)에선 안 보이면 QUIC 흐름 — 4계층 진단 도구가 무력화된 상태다.
  • event-loop의 CPU bound 함정: 이미지 인코딩·동기 JSON 직렬화·정규식 백트래킹이 들어오면 이벤트 루프가 점유되어 동시 연결 모델이 prefork보다 못해진다. 감지법: Node.js process.eventLoopUtilization() ≥ 0.9가 1초 이상 지속되면 워커 풀(worker_threads) 오프로드 또는 별도 서비스 분리 필요. Nginx도 동일 — worker_processes auto로 코어 수만큼 늘려도 CPU 바운드면 평탄해진다.

비유로 시작 — “국제 택배”

편지(HTTP 요청)를 해외로 보낼 때 여러 단계가 있다: 편지 내용 작성(Application Layer) → 암호화 봉투(Presentation/TLS) → 주소 라벨 붙이기(Network/IP) → 택배 차량 탑재(Transport/TCP) → 실제 도로 이동(Physical). 각 단계는 독립적이라 상위 단계는 하위 단계가 어떻게 동작하는지 몰라도 된다.

OSI 7계층 — 원리와 실무 관점

📖 더 보기: Understanding OSI & TCP/IP with Real-World Examples - Medium — 실제 요청 흐름으로 OSI 계층 비유 설명

실제 네트워크 통신은 OSI보다 단순한 TCP/IP 4계층을 사용하지만, 문제 진단 시 OSI 7계층이 유용하다. “어느 계층에서 문제인가”를 좁히면 원인을 빠르게 찾을 수 있다.

계층이름실무 관련 예시문제 증상
7ApplicationHTTP, HTTPS, DNS4xx/5xx 에러, API 응답 이상
6PresentationTLS/SSL 암호화인증서 오류, HTTPS 핸드셰이크 실패
5Session세션 연결 유지연결이 갑자기 끊김
4TransportTCP/UDP, 포트 번호포트 미오픈, 방화벽 차단
3NetworkIP 주소, 라우팅IP 충돌, VPC 라우팅 오류
2Data LinkMAC 주소(실무에서 거의 안 만남)
1Physical케이블, Wi-Fi(클라우드 환경에서 AWS 책임)

실무에서 주로 보는 계층: 7(HTTP 에러) → 4(포트/방화벽) → 3(IP/VPC/Security Group) → 6(TLS 인증서)

왜 7계층으로 나누는가 — Layered Abstraction Principle

각 계층이 독립적으로 설계된 덕분에 상위 계층은 하위 계층이 어떻게 동작하는지 몰라도 된다. 예를 들어, HTTP는 TCP 위에서 동작하지만 HTTP 코드를 작성할 때 TCP 3-way handshake를 신경 쓸 필요가 없다. Nginx도 마찬가지로 TLS 핸드셰이크(6계층)를 처리하면서 내부 Nest.js 서버에는 평문 HTTP(7계층)로 전달한다. 이 분리가 리버스 프록시의 핵심 원리다.

이 원리는 OSI 전용이 아니다. 상위 layer가 하위 layer를 구현 세부 없이 호출할 수 있도록 안정된 인터페이스를 두는 패턴은 컴퓨팅 전반에서 반복된다.

  • OS 커널 ↔ 유저 공간: read() 시스템 콜은 디스크 컨트롤러 명령을 추상화한다. 디스크가 NVMe든 SATA든 호출자는 같은 API를 쓴다.
  • React 컴포넌트 트리: 상위 컴포넌트는 자식 컴포넌트의 내부 state·렌더 알고리즘을 모른 채 props라는 안정된 인터페이스로만 협력한다.
  • DB 드라이버 ↔ 와이어 프로토콜: 애플리케이션은 SQL만 쓰고, 드라이버가 MySQL 바이너리 프로토콜로 변환한다. 프로토콜이 5.7 → 8.0으로 바뀌어도 앱 코드는 영향 없다.

진단에 활용하는 법: 문제가 어느 layer에 속하는지 확정하면 그 위·아래 layer는 일단 의심 목록에서 제외할 수 있다. OSI 7계층은 이 추상화 경계를 네트워크 도메인에 적용한 사례에 해당한다.

실제 HTTPS 요청의 전체 흐름

브라우저에서 https://api.example.com/users를 호출할 때 내부적으로:

1. DNS 조회: api.example.com → IP 주소 (예: 52.68.1.100) [7계층]
2. TCP 3-way Handshake [4계층]
클라이언트 → SYN → 서버
클라이언트 ← SYN-ACK ← 서버
클라이언트 → ACK → 서버
(연결 완료)
3. TLS Handshake [6계층]
→ 인증서 교환, 암호화 키 협상 (약 1~2 RTT 추가 지연)
4. HTTP 요청 전송 [7계층]
GET /users HTTP/1.1
Host: api.example.com
5. 서버 응답 → 브라우저 수신

프론트엔드 → 플랫폼 브릿지: fetch() 호출이 OSI 레이어를 어떻게 통과하는가

프론트엔드 코드에서 fetch('https://api.example.com/users')를 호출하면, 브라우저 내부에서 OSI 7계층을 모두 거치게 된다. 개발자는 JavaScript 레벨(7계층)만 보지만, 실제로는 아래처럼 모든 계층이 협력한다.

JavaScript 코드 (개발자가 작성)
fetch('https://api.example.com/users')
[Layer 7 — Application]
브라우저 HTTP 엔진: 요청 URL 파싱, 헤더 조립
GET /users HTTP/1.1
Host: api.example.com
Authorization: Bearer token...
[Layer 6 — Presentation / TLS]
TLS 라이브러리(BoringSSL): 요청 본문 암호화
인증서 검증, 세션 키 협상 (이미 연결이 있으면 세션 재사용)
[Layer 5 — Session]
브라우저 연결 풀: 같은 origin(api.example.com:443)에 대한
기존 TCP 연결이 있으면 재사용 (HTTP Keep-Alive)
→ 새 연결이면 3-way handshake 후 TLS handshake 수행
[Layer 4 — Transport / TCP]
TCP 세그먼트화: 큰 요청 데이터를 MSS(1460B) 단위로 분할
시퀀스 번호 부여, ACK 대기
Source Port: 55123 (브라우저가 임의 선택)
Dest Port: 443
[Layer 3 — Network / IP]
IP 패킷: Source IP(클라이언트), Dest IP(52.68.1.100)
라우터가 목적지 IP를 보고 다음 홉 결정
[Layer 2 — Data Link]
Ethernet 프레임: MAC 주소(게이트웨이/공유기)
[Layer 1 — Physical]
실제 신호: Wi-Fi 전파 또는 UTP 케이블 전기 신호

핵심 포인트: 개발자가 fetch()를 호출하고 await response.json()을 받는 사이에, 브라우저는 보이지 않는 곳에서 TCP 3-way handshake → TLS handshake → HTTP 요청 전송 → TCP 수신 확인 → TLS 복호화의 전체 과정을 수행한다. curl -v로 이 과정을 직접 볼 수 있다.

Terminal window
# fetch()와 동일한 요청을 curl로 시뮬레이션 (OSI 레이어 동작 관찰)
curl -v https://api.example.com/users
# 예상 출력 (각 줄이 어느 OSI 계층인지 주석 추가):
# * Trying 52.68.1.100:443... ← Layer 3: IP 주소로 연결 시도
# * Connected to api.example.com port 443 ← Layer 4: TCP 연결 완료 (3-way handshake)
# * TLSv1.3 (OUT), TLS handshake... ← Layer 6: TLS 협상 시작
# * SSL certificate verify ok. ← Layer 6: 인증서 검증 성공
# > GET /users HTTP/2 ← Layer 7: HTTP 요청 전송
# > Host: api.example.com
# > Authorization: Bearer ...
# < HTTP/2 200 ← Layer 7: 서버 응답 수신
# < content-type: application/json

📖 더 보기: What Happens When You Type a URL — Medium/HackerNoon — 브라우저 URL 입력부터 응답까지 OSI 레이어별 전체 흐름 설명 (입문)

TCP vs UDP

  • TCP: 연결 확인 후 전송, 순서 보장, 신뢰성 높음 → HTTP, DB 연결, SSH
  • UDP: 연결 없이 전송, 빠르지만 손실 가능 → DNS 쿼리(초기), 동영상 스트리밍, WebRTC

Web Server (Nginx) — 리버스 프록시 원리

📖 더 보기: NGINX Reverse Proxy 공식 문서 — proxy_pass, 업스트림 설정 공식 가이드

Nginx가 리버스 프록시로 동작할 때의 핵심은 이벤트 기반 비동기 아키텍처다. 더 구체적으로는 I/O Multiplexing 원리 — 커널이 제공하는 epoll(Linux) / kqueue(BSD·macOS) / IOCP(Windows) 시스템 콜을 통해 하나의 스레드가 수많은 소켓의 준비 이벤트를 한 번에 감시하고, 준비된 것만 골라 처리하는 패턴이다. Apache의 prefork처럼 요청마다 프로세스/스레드를 생성하지 않으므로 컨텍스트 스위치 비용과 메모리 사용량이 연결 수에 거의 비례하지 않는다. C10K(동시 1만 연결) 문제는 이 원리로 해결되었다.

이 원리는 Nginx 전용이 아니다. 하나의 실행 흐름이 다중 I/O 핸들을 비동기로 감시·처리한다는 공식은 여러 도메인에서 반복된다.

  • Node.js의 libuv: 단일 이벤트 루프가 epoll/kqueue로 수천 개의 fd를 감시하고 콜백을 디스패치한다. Nginx 워커 1개와 거의 동일한 모델이다.
  • Redis 단일 스레드: 마찬가지로 epoll 기반 이벤트 루프로 10만+ QPS를 단일 코어에서 처리한다. CPU bound가 아니라 I/O bound라는 가정이 같다.
  • HAProxy·Envoy의 데이터 플레인: 둘 다 같은 비동기 I/O 모델 위에 빌드되어 있다.

정량 비교 — 메모리 footprint (출처: Nginx vs Apache 벤치마크): 동시 keepalive 연결 10,000개를 유지할 때 Nginx는 약 50MB, Apache Prefork는 약 2.5GB, Apache Event MPM은 약 200MB를 사용한다. Prefork는 연결당 ~2MB의 프로세스 메모리가 선형으로 누적되는 반면, Nginx는 연결당 메모리 비용이 거의 평탄하다.

출처: Nginx vs Apache: 10x More Connections (2026) — 동시 keepalive 10K 기준 메모리 사용량 비교

조합의 이유: Nginx(앞단)도 Node.js(뒷단)도 같은 I/O Multiplexing 원리 위에서 동작하므로 “어느 한쪽이 스레드를 블로킹해서 다른 쪽이 멈춘다” 같은 모델 불일치가 없다. 단, CPU 바운드 작업(이미지 인코딩, 무거운 JSON 직렬화)은 이벤트 루프를 점유하므로 이 모델이 깨진다 — 그 시점부터는 워커 프로세스 수를 늘리거나 별도 워커 풀로 오프로드해야 한다.

클라이언트 ──HTTPS(443)──→ Nginx ──HTTP(3000)──→ Nest.js
├─ SSL 인증서 처리 (클라이언트 ↔ Nginx 구간만 암호화)
├─ 요청 헤더 추가: X-Real-IP, X-Forwarded-For
├─ 로드밸런싱: upstream 서버 여러 개로 분배
└─ 정적 파일은 직접 서빙 (Node.js 부하 감소)

Nginx 설정 핵심 예시:

server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/ssl/cert.pem; # TLS 인증서
ssl_certificate_key /etc/ssl/key.pem;
location / {
proxy_pass http://localhost:3000; # Nest.js로 전달
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s; # 업스트림 응답 대기 시간
}
# 정적 파일은 Nginx가 직접 처리 (Node.js 부하 감소)
location /static/ {
root /var/www;
expires 30d;
}
}

HTTP/2 — 성능 개선과 Nginx 설정

HTTP/1.1에서는 요청마다 새 TCP 연결을 열거나 순차적으로 처리해 헤드-오브-라인 블로킹이 발생한다. HTTP/2는 하나의 TCP 연결로 여러 요청을 동시에 처리(멀티플렉싱)하므로 지연 시간이 크게 줄어든다. Nginx에서 HTTP/2를 활성화하면 클라이언트 ↔ Nginx 구간에서 이 혜택을 받을 수 있다.

server {
# ✅ HTTP/2 활성화 — SSL과 함께 사용 필요
listen 443 ssl;
http2 on; # Nginx 1.25.1+ 방식 (구버전은 listen 443 ssl http2;)
# TLS 1.3 + HTTP/2 조합으로 최적 성능
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Keep-alive 설정 — 연결 재사용으로 오버헤드 감소
keepalive_timeout 65;
keepalive_requests 100;
location / {
proxy_pass http://localhost:3000;
# Nginx 1.29.4+ (2025.12)부터 upstream HTTP/2 프록시 지원
# gRPC 또는 소규모 응답이 많은 백엔드에서 멀티플렉싱 효과 있음
# 대부분의 REST API 환경에서는 HTTP/1.1로도 충분
proxy_http_version 1.1;
proxy_set_header Connection ""; # Keep-alive upstream
}
}

ALB → ECS Fargate 환경에서는 ALB가 이미 HTTP/2를 지원하므로 별도 Nginx 설정 없이 자동 적용된다. ALB Target Group 프로토콜 버전을 HTTP2로 설정하면 ALB → 컨테이너 구간도 HTTP/2로 동작한다.

ECS Fargate 환경의 실제 흐름

📖 더 보기: AWS ALB 공식 문서 - Target Groups — ECS Fargate 환경에서 ALB 설정 이해

인터넷
↓ HTTPS (443)
ALB (Application Load Balancer) ← Layer 7에서 동작 (HTTP 헤더 기반 라우팅 가능)
│ ← SSL 인증서 처리 (ACM 인증서)
│ ← Health Check로 정상 컨테이너만 라우팅
↓ HTTP (3000) — Target Group
Nest.js 컨테이너 (Fargate Task)
※ Fargate 환경에서는 ALB가 Nginx 역할을 대신함
※ Security Group으로 ALB → 컨테이너 포트만 허용
※ ALB는 경로 기반 라우팅 가능: /api/* → Nest.js, /static/* → S3
  • 네트워크 장애 발생 시 계층별 원인 찾아내기 (“3계층 문제인가, 7계층 문제인가”)
  • Nginx 설정 파일 읽기/수정 (리버스 프록시, 타임아웃 설정)
  • HTTPS 인증서 적용 위치 이해
  • 로컬 개발 환경에서 포트 충돌 디버깅
  • ALB Health Check 실패 원인 분석 및 /health 엔드포인트 구현

BackOps 실무 시나리오

  • 배포 후 ECS Task가 계속 재시작됨 → Health Check 실패 → /health 엔드포인트 없거나, Security Group이 ALB → 컨테이너 포트 막음
  • “HTTPS로 접속하는데 인증서 오류” → 6계층 문제, 인증서 도메인·만료일 확인 (openssl s_client)
  • 특정 API만 504 응답 → Nginx proxy_read_timeout 설정 확인, 근본적으로는 무거운 작업을 Queue로 분리
  • “로컬에서는 되는데 배포하면 안 됨” → Security Group이 해당 포트 열려있는지 확인 (4계층 문제)
  • 서비스 앞에 Nginx가 있는지, ALB만 있는지 파악
  • HTTPS 인증서가 어디서 처리되는지 이해
  • 네트워크 에러 발생 시 계층 기반으로 원인 찾아내기
개념 A개념 B차이점
Web ServerWAS(Web Application Server)Web Server는 정적 파일/프록시, WAS는 비즈니스 로직 실행 (Node.js는 WAS)
리버스 프록시포워드 프록시리버스는 서버 앞에서 요청 분배, 포워드는 클라이언트 대신 요청
NginxApache둘 다 Web Server. Nginx는 비동기/논블로킹으로 고성능, Apache는 프로세스 기반
ALBNginxALB는 AWS 관리형 로드밸런서, Nginx는 직접 운영하는 Web Server
HTTP/1.1HTTP/2HTTP/1.1은 요청 순차 처리, HTTP/2는 하나의 연결로 동시 처리(멀티플렉싱)

6.5 선택 매트릭스 — Nginx vs ALB vs Envoy

섹션 제목: “6.5 선택 매트릭스 — Nginx vs ALB vs Envoy”

세 가지 모두 리버스 프록시/로드밸런서 역할을 할 수 있지만 적합한 상황이 다르다. “다 가능하다”가 아니라 어떤 조건이면 무엇이 최선인가를 판단해야 한다.

판단 기준NginxALB (AWS)Envoy
운영 환경온프레미스 또는 EC2 직접 운영AWS 클라우드 (ECS/EKS/Lambda)마이크로서비스 메시 (Kubernetes)
관리 복잡도높음 (직접 설치·설정·업그레이드)낮음 (AWS 완전 관리)매우 높음 (Istio/콘트롤 플레인 필요)
트래픽 규모 기준소~중규모, 고정적 트래픽중~대규모, 급격한 트래픽 변동대규모 다중 서비스 간 내부 트래픽
핵심 강점정적 파일 서빙, 세밀한 설정 제어AWS 네이티브 통합, 자동 스케일링서비스 디스커버리, gRPC, 동적 구성
비용 구조서버 비용 + 엔지니어 운영 비용트래픽·LCU 기반 종량제인프라 비용 + 높은 운영 인건비
circuit breaker없음 (앱 레벨에서 별도 구현)없음 (Target Group health check으로 대체)내장 (outlier detection, retry)
프로토콜HTTP/1.1, HTTP/2, TCPHTTP/1.1, HTTP/2, WebSocketHTTP/1.1, HTTP/2, HTTP/3, gRPC, TCP

실무 의사결정 기준 (BackOps)

팀 규모 소~중, AWS ECS 환경
→ ALB 단독 사용이 기본값
→ Nginx를 추가하는 이유: 정적 파일 서빙, 세밀한 요청 재작성, ALB 뒤에서 부분 캐싱
트래픽 < 1,000 RPS & EC2 단일 인스턴스
→ Nginx 직접 운영이 ALB보다 저렴할 수 있음 (ALB 최소 비용 ~$15/월 + LCU)
마이크로서비스 10개 이상, 서비스 간 gRPC 통신 필수
→ Envoy 검토 (Istio 기반 서비스 메시)
→ 단, 학습곡선과 운영 복잡도를 감당할 팀 역량이 전제

각 선택지가 “깨지는” 조건 — 도입 전 반드시 평가

“다 가능하다”는 답이 가장 위험하다. 각 옵션이 어떤 정량적 조건에서 실패하는지 알고 도입해야 한다.

Nginx가 깨지는 조건

  • 인적 SPOF: 설정·인증서 갱신·커널 튜닝을 아는 사람이 1명이면 그 사람이 휴가일 때 변경이 멈춘다. ALB와 달리 컨트롤 플레인이 사람이다.
  • 단일 upstream에서 passive health check 무력화 (silent failure): upstream 블록에 server 1개만 두면 max_fails·fail_timeout·slow_start 파라미터가 공식적으로 무시된다 (Nginx 공식 docs). “장애 시 자동 제외되겠지”라고 가정하면 에러 로그에 경고 없이 그냥 무시된 채 돌아간다.
  • TLS 인증서 자동 갱신 미설치: cron + certbot 또는 acme.sh를 직접 운영해야 한다. 자동화가 빠지면 만료일에 전체 서비스가 6계층(TLS) 에러로 다운된다.
  • OS 커널 한계: worker_connections × worker_processesulimit -n(파일 디스크립터 한도)을 넘으면 신규 연결이 accept() failed (24: Too many open files) 로 거부된다.

ALB가 깨지는 조건 — LCU 임계값 정량

ALB는 4가지 차원 중 최대값으로 LCU가 산정되므로, 한 차원만 폭증해도 비용이 튄다. 1 LCU의 허용량 (AWS 공식 가격 페이지):

차원1 LCU 허용량폭증 트리거
신규 연결25 conn/skeepalive 미사용 클라이언트 다수 (모바일)
활성 연결3,000 active conn/min장기 WebSocket 또는 SSE
처리 바이트1 GB/h (Lambda는 0.4GB/h)대용량 파일 응답, 이미지·비디오 프록시
룰 평가1,000/s리스너 룰 수 × RPS — 룰 50개 × 100 RPS = 5K/s = 5 LCU
  • 기준 비용: ALB 시간당 $0.0225 + LCU당 $0.008 = 최소 ~$16/월
  • 폭증 예시: 100K WebSocket 클라이언트 상시 연결 시 활성 연결 차원만으로 100,000 / 3,000 ≈ 33 LCU × $0.008 × 730h ≈ 월 $193 (이 차원만)
  • 다른 깨짐: ALB는 L7 circuit breaker가 내장되어 있지 않다. Target 단위 health check로만 격리하므로 “느린 응답”은 못 막는다(아래 6.6 Cascading Failure 섹션 참조). gRPC unary는 가능하지만 양방향 스트리밍은 NLB가 필요하다.

Envoy가 깨지는 조건

  • 운영 인건비 폭증: 컨트롤 플레인(Istio·Consul)이 별도 시스템이다. 사이드카 패턴은 Pod마다 50MB 추가 메모리 + 0.52ms latency를 더한다. 마이크로서비스가 5개 미만이면 ROI 음수.
  • xDS(컨트롤 플레인 API) 학습 곡선: Listener·Cluster·Route·Endpoint 4축의 동적 구성을 다루어야 한다. Nginx의 nginx -s reload 같은 단순 모델이 없다.
  • 버전 호환: Istio · Envoy · Kubernetes 3개의 버전 매트릭스를 동시에 맞춰야 한다. 한쪽 EOL이 다가오면 다른 두 개도 같이 끌어올려야 한다.

의사결정 예시 (사례 적용): 트래픽 200 RPS, EC2 1대, 팀원 1명이 Nginx를 운영한다고 가정. 위 기준으로 점검하면 (1) 인적 SPOF — 그 사람이 휴가 가면 위험, (2) 단일 upstream — passive health check 무력화, (3) cert 자동화 — certbot 설치 여부 미확인. → 결정: ALB로 이전. 월 비용 $15~25 증가하지만 (1)(2)(3) 위험이 사라지고, 이후 트래픽이 1K RPS 이상으로 늘어도 LCU 폭증 차원은 룰 수 10개 이내·keepalive 사용으로 통제 가능.

📖 참고: AWS ALB 공식 문서 - Features | Envoy 공식 문서 - What is Envoy


🔧 502 Bad Gateway — Nginx가 업스트림 서버에 연결 실패

섹션 제목: “🔧 502 Bad Gateway — Nginx가 업스트림 서버에 연결 실패”

📖 더 보기: 502 Bad Gateway Nginx Fix - CloudPanel — 502 원인별 진단과 해결 방법 정리

증상: 브라우저에서 502 Bad Gateway 에러 발생. Nginx 에러 로그에 아래 메시지

connect() failed (111: Connection refused) while connecting to upstream

원인: Nginx는 정상이지만 업스트림(Nest.js) 서버가 응답을 못하는 상태. 주요 원인:

  1. Node.js 프로세스가 죽어있음
  2. proxy_pass에 설정한 포트가 틀림 (3000 vs 8080 등)
  3. Node.js가 아직 기동 중 (헬스체크 실패)

해결:

Terminal window
# 1. Node.js 프로세스 실행 여부 확인
ps aux | grep node
# 예상 출력 (정상):
# young 1234 1.2 2.3 node dist/main.js
# 결과 없으면 → 프로세스 재시작 필요
# 2. 해당 포트 리스닝 여부 확인
ss -tlnp | grep 3000
# 예상 출력 (정상):
# LISTEN 0 128 0.0.0.0:3000 users:(("node",pid=1234,fd=18))
# 3. Nginx에서 직접 업스트림 연결 테스트
curl -v http://localhost:3000/health
# → 200 OK이면 Nginx 설정 문제, 연결 실패면 Node.js 문제
# 4. Nginx 에러 로그 실시간 확인
tail -f /var/log/nginx/error.log
# → 에러 메시지로 원인 파악

🔧 504 Gateway Timeout — 업스트림 응답 시간 초과

섹션 제목: “🔧 504 Gateway Timeout — 업스트림 응답 시간 초과”

증상: 특정 API에서 504 Gateway Timeout 발생. 오래 걸리는 작업(대용량 CSV 처리, 복잡한 쿼리 등)에서 자주 발생 원인: Nginx의 proxy_read_timeout (기본 60초)보다 업스트림 응답이 늦음

해결:

# nginx.conf — 해당 location 블록에 타임아웃 늘리기
location /api/heavy-job {
proxy_pass http://localhost:3000;
proxy_read_timeout 300s; # 기본 60s → 필요에 따라 조정
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
}

근본 해결책은 타임아웃을 늘리는 게 아니라 무거운 작업을 Queue로 분리하고 즉시 응답하는 것


🔧 502 Bad Gateway — 응답 헤더/바디가 버퍼 크기를 초과

섹션 제목: “🔧 502 Bad Gateway — 응답 헤더/바디가 버퍼 크기를 초과”

증상: 특정 API만 502 Bad Gateway 발생. 특히 대용량 쿠키나 긴 Authorization 헤더를 사용하는 요청에서 발생. Nginx 에러 로그:

Terminal window
upstream sent too big header while reading response header from upstream

원인: Nginx는 업스트림 응답을 버퍼에 담아 처리하는데, 응답 헤더가 기본 버퍼 크기(4k 또는 8k)를 초과하면 처리하지 못하고 502를 반환한다. JWT 토큰이 길거나 Set-Cookie 헤더가 많은 경우 흔히 발생한다.

해결 — 추측이 아니라 측정 후 설정

기본값 4k/8k를 무작정 128k로 늘리면 워커당 메모리가 4×128k = 512k씩 누적되어 동시 연결 수가 많을 때 메모리가 튄다. 실제 헤더 크기를 먼저 측정한 뒤 그보다 약간 큰 값으로 설정한다.

Terminal window
# 1단계 — 업스트림 응답 헤더의 실제 크기 측정 (Nginx를 거치지 않고 직접 호출)
curl -s -w '%{size_header}\n' -o /dev/null http://localhost:3000/api/heavy
# 예상 출력:
# 6231 ← 헤더 크기 ~6KB → 기본 4k에서 502, 8k에선 통과
# 12480 ← 헤더 크기 ~12KB → 8k 기본값도 부족, 16k 이상 필요
# 2단계 — 어떤 응답에서 큰지 좁히기 (인증된 요청 vs 비인증, Set-Cookie 많은 응답)
curl -s -w 'header=%{size_header}\n' -o /dev/null \
-H 'Authorization: Bearer eyJhbG...(실제 JWT)' \
http://localhost:3000/api/me
# Set-Cookie 헤더 여러 개 + 긴 JWT가 원인인 경우가 가장 흔하다.
# 3단계 — 측정값에 맞춰 설정 (proxy_buffer_size = 측정값을 4KB 경계로 올림한 값)
location / {
proxy_pass http://localhost:3000;
proxy_buffer_size 16k; # 첫 헤더 청크 크기 (기본 4k 또는 8k)
proxy_buffers 4 16k; # 본문 버퍼 개수 × 크기
proxy_busy_buffers_size 16k; # 클라이언트로 전송 중인 버퍼 한도
}
Terminal window
# 4단계 — 변형 실험으로 동작 확인 (한 번에 한 변수만 바꿔야 인과를 좁힐 수 있음)
sudo nginx -t && sudo nginx -s reload
curl -I http://your-nginx/api/me # 502 → 200으로 바뀌면 버퍼 크기가 원인 확정
tail -f /var/log/nginx/error.log # "upstream sent too big header" 재현 여부 모니터링
# 5단계 — proxy_buffering off로 비교 (직관과 달리 이 설정만으로는 해결되지 않음)
# location / { proxy_buffering off; } 만 추가하고 reload하면 → 여전히 502 가능
# proxy_buffer_size는 buffering off 상태에서도 헤더 저장에 쓰이기 때문

왜 측정 후 설정해야 하는가: proxy_buffer_size는 워커 메모리에서 연결당 할당된다. 1만 동시 연결 × 128k = 1.3GB. 16k로 충분한데 128k를 쓰면 1.1GB가 낭비된다.

📖 더 보기: Tuning proxy_buffer_size in NGINX - GetPageSpeed — 측정 기반 튜닝 가이드 | Nginx 502 Bad Gateway Debugging Checklist - ZeonEdge — 502 원인별 체계적 진단 체크리스트 (중급)


🔧 HTTPS 접속 시 “인증서 오류” 또는 연결 실패

섹션 제목: “🔧 HTTPS 접속 시 “인증서 오류” 또는 연결 실패”

증상: 브라우저에서 ERR_CERT_COMMON_NAME_INVALID 또는 SSL_ERROR_RX_RECORD_TOO_LONG

원인 1: 인증서 도메인과 실제 접속 도메인 불일치 (www. 유무, 와일드카드 여부) 원인 2: HTTP 포트(80)로 HTTPS 요청을 보내는 경우 (ERR_SSL_PROTOCOL_ERROR)

해결:

Terminal window
# 인증서 정보 확인
openssl s_client -connect api.example.com:443 -showcerts 2>/dev/null | \
openssl x509 -noout -text | grep "Subject\|DNS"
# 예상 출력:
# Subject: CN=api.example.com
# DNS:api.example.com, DNS:*.example.com
# Nginx가 어느 포트에서 SSL을 처리하는지 확인
grep -n "listen\|ssl_certificate" /etc/nginx/conf.d/*.conf
# 예상 출력:
# /etc/nginx/conf.d/default.conf:2: listen 443 ssl;
# /etc/nginx/conf.d/default.conf:5: ssl_certificate /etc/ssl/cert.pem;

🔧 ALB Health Check 실패로 ECS Task가 계속 재시작됨

섹션 제목: “🔧 ALB Health Check 실패로 ECS Task가 계속 재시작됨”

증상: ECS Fargate에서 태스크가 시작되자마자 Unhealthy 판정을 받고 반복 재시작. ALB Target Group에서 “unhealthy” 상태 지속

원인:

  1. Health Check 경로(/health)가 Nest.js에 구현되어 있지 않음
  2. Health Check 포트/프로토콜 설정이 실제 앱과 다름
  3. Nest.js 앱이 기동 완료 전에 ALB가 체크를 시작 (Startup 지연)

해결:

// Nest.js에 헬스체크 엔드포인트 추가
@Controller()
export class HealthController {
@Get("/health")
health() {
return { status: "ok" };
// → ALB가 이 엔드포인트를 호출해 200 OK이면 Healthy 판정
}
}
Terminal window
# AWS 콘솔 또는 CLI로 Target Group Health Check 설정 확인
# 경로: EC2 → Load Balancers → Target Groups → [해당 그룹] → Health checks 탭
# 확인 항목:
# - Health check path: /health (앱에 있는 경로인지 확인)
# - Healthy threshold: 2 (2번 연속 성공 시 Healthy)
# - Unhealthy threshold: 3 (3번 연속 실패 시 Unhealthy)
# - Timeout: 5s (앱 응답이 이 시간 내에 와야 함)

🔧 503 Service Unavailable — ALB에 등록된 Target이 없거나 모두 Unhealthy

섹션 제목: “🔧 503 Service Unavailable — ALB에 등록된 Target이 없거나 모두 Unhealthy”

증상: ALB DNS로 요청 시 503 Service Unavailable 반환. ECS 콘솔에서 태스크는 실행 중으로 보임

원인:

  1. ALB Target Group에 등록된 ECS Task가 없음 (태스크 배포 직후 공백 구간)
  2. 모든 Target이 Unhealthy 상태 (Health Check 실패 상태)
  3. Security Group 설정으로 ALB → ECS 컨테이너 포트가 막혀 있음

해결:

Terminal window
# 1. Target Group의 Target 상태 확인
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:ap-northeast-2:123456789:targetgroup/my-tg/abc123
# 예상 출력 (정상):
# { "TargetHealthDescriptions": [
# { "Target": {"Id": "10.0.1.5", "Port": 3000},
# "TargetHealth": {"State": "healthy"} }
# ] }
# 예상 출력 (문제 있을 때):
# { "TargetHealth": {"State": "unhealthy", "Reason": "Target.FailedHealthChecks"} }
# 2. Security Group 규칙 확인 — ALB → ECS 컨테이너 포트 허용 여부
aws ec2 describe-security-groups --group-ids sg-xxxx \
--query 'SecurityGroups[].IpPermissions'
# → Inbound 규칙에 ALB의 Security Group에서 3000 포트 허용이 있어야 함
# 3. ECS 태스크 배포 직후 503이 발생한 경우
# → ALB deregistration_delay(기본 300초) 동안 기존 Target이 유지되므로
# → 신규 배포 시 "최소 정상 백분율"을 100%로 설정하여 공백 방지

🔧 Cascading Failure — L4 timeout이 L7 circuit breaker를 무력화하는 패턴

섹션 제목: “🔧 Cascading Failure — L4 timeout이 L7 circuit breaker를 무력화하는 패턴”

시나리오: DB 응답이 느려졌을 때 앱 전체가 멈추는 이유

[정상]
클라이언트 → ALB → Nest.js → DB (100ms)
[DB 느려짐 — L4 timeout 유발]
클라이언트 → ALB → Nest.js ──────────────────→ DB (30s 이상)
TCP 연결은 살아있음 (L4 레벨)
하지만 응답은 안 옴

핵심 문제: ALB Health Check는 HTTP 200을 기준으로 판단한다. DB가 느릴 때 /health 엔드포인트는 DB를 조회하지 않으면 여전히 200을 반환하므로 ALB는 Healthy로 판단하지만, 실제 API는 모두 타임아웃 상태다.

  1. DB 응답 지연 → Nest.js 워커 스레드/이벤트루프 점유 증가
  2. ALB Health Check는 /health(DB 미조회)만 보므로 Target = Healthy 유지
  3. ALB가 계속 새 요청을 전송 → Nest.js 커넥션 풀 포화
  4. L4 TCP 연결은 유지되지만 L7 HTTP 응답이 없음 → 클라이언트 전체 타임아웃
  5. 클라이언트가 재시도 → 부하 가중 → 전체 서비스 다운 (cascading failure)

circuit breaker가 없으면 왜 멈추는가: Nginx나 ALB에는 L7 circuit breaker가 내장되어 있지 않다. “업스트림이 n번 연속 실패하면 요청을 차단” 로직을 앱 레벨에서 별도로 구현하지 않으면, 느린 업스트림이 회복될 때까지 모든 요청이 쌓인다.

방어 패턴:

// 1. Health Check에 DB 연결 상태 포함 (shallow health check와 분리)
@Get('/health/deep')
async healthDeep() {
await this.dataSource.query('SELECT 1'); // DB 실제 조회
return { status: 'ok', db: 'connected' };
// ALB Health Check는 /health(빠른 응답), 모니터링은 /health/deep
}
# 2. Nginx: upstream 응답 실패 시 서버를 임시 제외 (passive health check)
upstream backend {
server localhost:3000 max_fails=3 fail_timeout=30s;
server localhost:3001 max_fails=3 fail_timeout=30s backup; # 최소 2개 이상 등록
# 30초 내 3번 실패 시 해당 서버를 30초간 제외
}
location / {
proxy_pass http://backend;
proxy_read_timeout 10s; # 너무 길면 연결이 쌓임 — DB timeout보다 짧게 설정
proxy_connect_timeout 3s;
}

⚠️ silent failure 주의 — 단일 server에서는 max_fails가 무시된다

Nginx 공식 docs 인용: “If there is only a single server in a group, max_fails, fail_timeout and slow_start parameters are ignored, and such a server will never be considered unavailable.” (공식 출처)

upstream backend { server localhost:3000 max_fails=3 fail_timeout=30s; } 처럼 server를 1개만 등록하면 위 설정은 에러 없이 무시되고 실패해도 그대로 요청이 전달된다. backup server를 추가하거나, Nginx Plus의 active health check를 쓰거나, 앱 레벨 circuit breaker를 별도로 구현해야 한다.

변형 실험으로 max_fails 동작 직접 관찰

Terminal window
# 1단계 — upstream에 server 2개를 두고 한쪽을 의도적으로 죽임
# nginx.conf: upstream backend { server 127.0.0.1:3000; server 127.0.0.1:3001; }
sudo nginx -t && sudo nginx -s reload
kill -9 $(lsof -ti:3000) # 3000번 서버 강제 종료
# 2단계 — 요청 반복하며 에러 로그에서 제외 시점 확인
for i in {1..20}; do curl -s -o /dev/null -w "%{http_code} " http://localhost/; done; echo
# 예상 출력 (3번 실패 후 3000번 서버가 30초간 제외되고 200으로 안정):
# 502 502 502 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200 200
# 3단계 — 동일 시나리오를 server 1개 upstream에서 재현 (max_fails 무시 확인)
# nginx.conf: upstream backend { server 127.0.0.1:3000 max_fails=3 fail_timeout=30s; }
# → kill 후 같은 루프 실행 시 20번 모두 502가 나온다 (passive health check 작동 안 함)
# 4단계 — 에러 로그에서 "upstream server temporarily disabled" 메시지로 제외 시점 확인
tail -f /var/log/nginx/error.log | grep "temporarily disabled"
# server 2개 케이스에서만 이 메시지가 보임. 단일 server에서는 영원히 안 보임 (silent)

정리: L4(TCP) 연결이 살아있다고 L7(HTTP) 요청이 정상 처리되는 것이 아니다. 느린 업스트림은 빠른 업스트림 다운보다 더 위험하다 — Health Check를 통과한 채로 요청을 계속 받기 때문이다. 또한 “안전망”이라 생각한 max_fails 설정도 토폴로지(server 개수)에 따라 silent 무력화될 수 있으므로 설정 후 반드시 변형 실험으로 검증한다.


  • OSI 7계층을 외우지 않아도 “이 에러는 몇 계층 문제”인지 판단할 수 있다
  • 리버스 프록시가 뭔지, 왜 쓰는지 설명할 수 있다
  • 80포트로 들어온 요청이 Node.js 3000포트까지 도달하는 과정을 설명할 수 있다
  • 팀 서비스 앞에 Web Server가 있는지, ALB만 있는지 확인했다
  • 502와 504 에러의 차이를 설명할 수 있다

TCP 3-way handshake, TLS handshake, Nginx upstream, upstream timeout, CDN, DNS round-robin, ALB Target Group, Health Check, HTTP/2 multiplexing, keepalive_timeout

  • 팀 서비스의 트래픽 진입점 확인 (ALB → Nginx → Node? ALB → Node?)
Terminal window
# ALB DNS를 직접 curl로 호출해서 응답 헤더 확인
curl -I https://your-alb-dns.ap-northeast-2.elb.amazonaws.com/health
# 예상 출력:
# HTTP/1.1 200 OK
# Server: nginx/1.24.0 ← Nginx 있으면 표시됨
# Server: Cowboy ← Fastify(Node.js) 직접이면 이쪽
  • Nginx 설정 파일이 있다면 proxy_pass 설정 확인
Terminal window
# Nginx 설정 파일 위치 찾기
find /etc/nginx -name "*.conf" | xargs grep -l "proxy_pass"
# 설정 확인
cat /etc/nginx/conf.d/default.conf
# 예상 출력 (핵심 부분):
# location / {
# proxy_pass http://127.0.0.1:3000;
# proxy_set_header Host $host;
# }
  • curl -v https://서비스주소로 TLS 핸드셰이크 과정 확인
Terminal window
curl -v https://api.example.com/health 2>&1 | head -30
# 예상 출력 (주요 부분):
# * Trying 52.68.1.100:443...
# * Connected to api.example.com (52.68.1.100) port 443
# * TLSv1.3 (OUT), TLS handshake, Client hello
# * TLSv1.3 (IN), TLS handshake, Server hello
# * SSL certificate verify ok. ← 인증서 정상
# > GET /health HTTP/2
# < HTTP/2 200
  • ALB Target Group에서 ECS 태스크 Health 상태 확인
Terminal window
# AWS CLI로 Target Group Health 상태 조회
aws elbv2 describe-target-health \
--target-group-arn $(aws elbv2 describe-target-groups \
--query 'TargetGroups[0].TargetGroupArn' --output text)
# 예상 출력 (정상):
# { "TargetHealthDescriptions": [
# { "TargetHealth": { "State": "healthy" } }
# ] }
# 예상 출력 (문제 있을 때):
# { "TargetHealth": { "State": "unhealthy", "Reason": "Target.FailedHealthChecks",
# "Description": "Health checks failed" } }
# → /health 엔드포인트 구현 여부, Security Group 포트 허용 여부 순서로 확인
항목핵심 내용
OSI 7계층문제 진단 기준 — 7(HTTP) → 4(포트/방화벽) → 3(IP/VPC) → 6(TLS) 순으로 확인
리버스 프록시클라이언트 앞에서 SSL 처리 + 내부 서버로 요청 전달 (포트 숨김, 로드밸런싱)
Nginx vs ALBNginx는 직접 운영, ALB는 AWS 관리형 — ECS Fargate에서는 ALB가 대신함
502 vs 504502: 업스트림 연결 실패(Node.js 다운), 504: 응답 시간 초과(타임아웃)
Health CheckALB가 /health를 주기적으로 체크 — 실패하면 Unhealthy로 태스크 교체

5줄 핵심

  1. OSI 7계층은 네트워크 문제를 계층별로 나눠서 진단하는 기준이다
  2. 실무에서 주로 마주치는 계층은 7(HTTP), 4(TCP/포트), 3(IP), 6(TLS)이다
  3. Web Server(Nginx)는 클라이언트 요청을 가장 먼저 받아 내부 서버로 전달하며 SSL 처리, 로드밸런싱을 담당한다
  4. 502(업스트림 연결 실패)와 504(응답 시간 초과)는 원인과 해결 방향이 다르다
  5. ECS Fargate 환경에서는 ALB가 Nginx 역할을 대신하고, Security Group이 포트 접근을 제어한다
OSI 모델 & Web Server (지금 여기)
TCP/IP 심화 ← TCP 3-way handshake, Keep-Alive, TIME_WAIT 상태
HTTPS / TLS 심화 ← TLS 1.3 핸드셰이크, mTLS, 인증서 갱신 자동화 (ACM)
AWS 네트워킹 ← VPC, Subnet, Security Group, Route 53, ALB 라우팅 규칙
서비스 메시 / API Gateway ← Kong, AWS API Gateway, 트래픽 제어 패턴

인터뷰 대비 핵심 질문 (실제 자주 출제)

  • “HTTP와 HTTPS의 차이는? TLS Handshake는 어느 OSI 계층인가?”
  • “502와 504 에러의 차이는? 각각 어떻게 디버깅하는가?”
  • “리버스 프록시와 포워드 프록시의 차이는? 리버스 프록시를 쓰는 이유는?”
  • “Nginx가 Apache보다 고성능인 이유는? (이벤트 기반 vs 프로세스 기반)”
  • “ECS Fargate에서 ALB Health Check가 실패하는 주요 원인은?”

최종 수정: 2026-04-01