L7 Application
HTTP, HTTPS, DNS 같은 애플리케이션 프로토콜의 의미를 다룬다. 상태 코드, URL, 헤더, 라우팅 규칙이 여기에 가깝다.
4xx/5xx, 잘못된 Host 헤더, 프록시 라우팅, API 응답 바디가 이상할 때분류: Layer 2 - 인프라 기초 | 작성일: 2026-03-22
OSI 모델은 네트워크 통신을 역할별 계층으로 나눠서 설명하는 진단용 지도다. Web Server는 클라이언트의 HTTP 요청을 가장 먼저 받아 정적 파일을 직접 응답하거나, 애플리케이션 서버로 요청을 넘기는 서버 소프트웨어다.
이 문서에서 말하는 Web Server는 주로 Nginx 같은 리버스 프록시와, AWS ALB(Application Load Balancer)처럼 비슷한 앞단 역할을 하는 관리형 로드밸런서를 함께 가리킨다. 리버스 프록시는 서버 앞에서 요청을 대신 받아 내부 애플리케이션으로 넘기는 앞단 서버 역할이다. ALB와 Nginx를 미리 알고 올 필요는 없다. 여기서 처음 등장할 때 정의하고, 첫 회독에서는 “요청이 어디서 멈췄는지 계층으로 좁히는 법”만 잡으면 된다.
HTTP Basics에서는 요청과 응답, 상태 코드, 헤더를 배웠다. 하지만 실제 서비스 장애는 HTTP 한 층에서만 끝나지 않는다. 브라우저에는 504 Gateway Timeout 하나만 보이는데 원인은 DNS, TCP 포트, TLS 인증서, 프록시 타임아웃, 애플리케이션 슬로우 쿼리 중 하나일 수 있다.
OSI 모델은 이 혼란을 줄이는 공통어다. “7계층 HTTP 응답은 왔는가”, “4계층 TCP 연결은 맺혔는가”, “6계층 TLS 검증은 통과했는가”처럼 질문을 쪼개면 진단 범위가 줄어든다.
Web Server는 그 계층들이 실제 서비스 앞단에서 만나는 지점이다. 사용자는 https://api.example.com/users 하나만 호출하지만, 서비스 안쪽에서는 다음 일이 일어난다.
프론트엔드에서 fetch() 한 줄로 보이는 호출도 실제로는 이 경로를 통과한다. 중요한 것은 JavaScript 호출 자체가 아니라, 그 아래의 DNS, TCP, TLS, HTTP, 프록시 경계를 분리해서 볼 수 있어야 한다는 점이다.
이 토픽은 두 가지 문제를 함께 다룬다. 첫째, 네트워크 문제를 서로 다른 팀과 장비가 같은 언어로 분류하지 못하던 문제다. 둘째, 많은 클라이언트 연결을 애플리케이션 서버가 직접 감당하면서 생기던 비용 문제다.
초기 네트워크 세계에서는 장비 벤더와 프로토콜이 제각각이었다. 어떤 시스템은 자체 주소 체계를 쓰고, 어떤 시스템은 자체 세션 관리 방식을 썼다. 서로 다른 네트워크를 연결하려면 “이 장비의 이 실패가 상대 장비의 어떤 실패와 같은가”를 매번 해석해야 했다.
OSI(Open Systems Interconnection) 모델의 철학은 네트워크를 기능 경계로 나누는 것이다. 물리 신호, 링크, IP 라우팅, TCP/UDP 전송, 세션, 표현, 애플리케이션을 분리하면 각 계층은 자기 책임을 가진다. 여기서 TCP와 UDP는 L4 전송 계층의 대표 프로토콜이며, 연결과 재전송을 맡는 TCP와 지연을 줄이고 책임을 애플리케이션에 넘기는 UDP의 차이는 다음 문서에서 자세히 본다. 현실의 인터넷은 OSI 7계층을 그대로 구현하지 않고 TCP/IP 모델을 쓰지만, OSI의 언어는 문제 진단에 남았다.
예를 들어 HTTP 502와 TLS certificate expired는 둘 다 “API가 안 된다”로 보고될 수 있다. 하지만 앞의 것은 주로 7계층 프록시와 업스트림 사이의 문제이고, 뒤의 것은 6계층 표현/암호화 경계 문제다. 같은 증상처럼 보여도 의심해야 할 로그, 담당 시스템, 복구 방향이 다르다.
초기 웹 서버는 요청마다 프로세스나 스레드를 붙잡는 구조가 흔했다. 정적 파일 몇 개를 응답하는 시기에는 충분했지만, keep-alive 연결과 동시 접속이 늘면서 연결 하나가 서버 메모리와 스케줄링 비용을 오래 점유했다.
이 한계가 C10K 문제다. C10K는 동시에 1만 개 연결을 안정적으로 처리할 수 있느냐는 질문이다. 연결마다 스레드 하나를 두고 스레드 스택을 1MB만 잡아도 1만 연결은 스택 예약만 약 10GB가 된다. 실제 요청을 거의 보내지 않는 idle keep-alive 연결도 같은 방식이면 비용을 계속 차지한다.
Nginx류의 이벤트 기반 Web Server는 방향을 바꿨다. 연결마다 스레드를 붙이는 대신, 소수의 worker가 커널의 I/O 이벤트를 감시한다. 읽을 수 있는 소켓, 쓸 수 있는 소켓만 골라 처리하므로 idle 연결이 많아도 메모리와 컨텍스트 스위치 비용이 훨씬 작다.
이 철학은 Node.js, Redis, HAProxy, Envoy 같은 시스템에도 반복된다. I/O가 대부분이고 CPU 작업이 짧다는 가정에서는 이벤트 루프가 효율적이다. 반대로 이미지 인코딩, 큰 JSON 직렬화, 정규식 백트래킹처럼 CPU를 오래 잡는 작업이 들어오면 이 가정이 깨진다.
이 문서를 읽은 뒤에는 아래 질문에 답할 수 있어야 한다.
OSI 7계층을 외우는 것 자체가 목표는 아니다. 실무에서는 “이 증상이 어느 계층의 실패 신호인가”를 좁히기 위해 쓴다.
HTTP, HTTPS, DNS 같은 애플리케이션 프로토콜의 의미를 다룬다. 상태 코드, URL, 헤더, 라우팅 규칙이 여기에 가깝다.
4xx/5xx, 잘못된 Host 헤더, 프록시 라우팅, API 응답 바디가 이상할 때데이터 표현과 암호화 경계다. 웹에서는 주로 TLS 인증서, 암호화 협상, 서버 이름 검증이 여기에 걸린다.
인증서 만료, 도메인 불일치, HTTPS 핸드셰이크 실패가 보일 때연결의 유지와 재사용을 설명하는 계층이다. 현실에서는 L4/L7 기능과 섞여 나타나는 경우가 많다.
keep-alive 재사용, 연결이 갑자기 닫힘, 장기 연결의 수명 문제가 의심될 때TCP/UDP, 포트, 연결 수립, 재전송, 타임아웃을 다룬다.
포트 미오픈, 방화벽 차단, TCP handshake 실패, UDP 443 QUIC 흐름을 볼 때IP 주소와 라우팅 경로를 다룬다. 클라우드에서는 VPC, subnet, route table, Security Group 판단과 자주 연결된다.
목적지 IP가 틀리거나, 라우팅 테이블과 네트워크 ACL 경계가 의심될 때같은 네트워크 링크 안에서 프레임과 MAC 주소를 다룬다.
일반 클라우드 앱 운영에서는 드물고, 온프레미스 네트워크나 로컬 네트워크 장비 문제에서 주로 확인한다.케이블, 무선 신호, 전기 신호 같은 물리 매체다.
클라우드 서비스 운영자는 보통 직접 다루지 않고, 제공자 책임 영역으로 둔다.첫 회독에서는 7개를 모두 같은 밀도로 외우지 않아도 된다. 백엔드와 플랫폼 입문 단계에서 자주 만나는 순서는 보통 L7 HTTP → L4 TCP/포트 → L3 라우팅/IP → L6 TLS다.
현실의 인터넷 스택은 TCP/IP 모델로 설명하는 편이 더 정확하다. OSI는 세밀한 진단 언어이고, TCP/IP는 실제 구현에 가까운 묶음이다.
| OSI 계층 | TCP/IP 모델에서의 위치 | 웹 요청에서 보이는 예시 |
|---|---|---|
| L7 Application | Application | HTTP 메서드, URL, 상태 코드, DNS 질의 |
| L6 Presentation | Application에 흡수 | TLS 암호화, 인증서 검증 |
| L5 Session | Application/L4에 흡수 | keep-alive, 연결 재사용, 세션 수명 |
| L4 Transport | Transport | TCP, UDP, 포트, handshake, timeout |
| L3 Network | Internet | IP 주소, 라우팅, VPC route table |
| L2 Data Link | Link | Ethernet frame, MAC 주소 |
| L1 Physical | Link/Physical | 케이블, 무선 신호, 물리 장비 |
이 매핑을 알아야 하는 이유는 단순하다. OSI 계층은 실무에서 깔끔하게 분리되지 않는다. TLS는 OSI로는 L6처럼 설명하지만, 현실에서는 애플리케이션 스택과 라이브러리 안에서 처리된다. HTTP/3의 QUIC는 UDP 위에서 전송, TLS, 연결 관리를 한 프로토콜 안에 묶는다. 모델은 지도가 될 수 있지만, 실제 지형을 완전히 대체하지 않는다.
https://api.example.com/users 요청을 하나 따라가 보자. HTTP Basics의 요청/응답 구조에 네트워크 계층을 붙이면 다음처럼 보인다.
sequenceDiagram participant Client as Browser or Client participant DNS participant Edge as ALB or Nginx participant App as API Server participant DB Client->>DNS: api.example.com 조회 DNS-->>Client: IP 주소 반환 Client->>Edge: TCP 3-way handshake to 443 Client->>Edge: TLS handshake and certificate validation Client->>Edge: HTTP GET /users Edge->>Edge: routing, header normalization, timeout policy Edge->>App: proxy request to internal port App->>DB: optional query DB-->>App: rows App-->>Edge: HTTP status + headers + body Edge-->>Client: response
이 흐름에서 “요청이 안 된다”는 말을 계층 질문으로 바꾸면 진단이 쉬워진다.
| 질문 | 통과했다면 알 수 있는 것 | 실패하면 먼저 의심할 것 |
|---|---|---|
| DNS가 IP를 돌려줬는가? | 도메인 이름은 해석된다. | DNS 레코드, resolver(대리 조회 서버), 내부/외부 DNS 설정 |
| TCP 443 연결이 맺혔는가? | 목적지 IP와 포트까지 L4 연결 가능성이 있다. | 포트, 방화벽, Security Group, listener |
| TLS 인증서 검증이 통과했는가? | 도메인과 인증서, 암호화 협상은 통과했다. | 인증서 만료, SAN 불일치, TLS 설정 |
| HTTP 상태 코드가 왔는가? | L7까지 도달했고 서버/프록시가 응답을 만들었다. | 라우팅, upstream, 앱 오류, timeout |
| 앱 로그에 요청 ID가 찍혔는가? | 프록시를 지나 앱 프로세스까지 도달했다. | 프록시 라우팅, 포트, health check, timeout |
curl -v는 이 중 일부를 한 화면에 보여준다. 긴 명령 출력 전체를 외울 필요는 없고, 아래 신호만 읽으면 된다.
* Trying 203.0.113.10:443... -> L3/L4: IP와 포트로 연결 시도* Connected to api.example.com -> L4: TCP 연결 완료* TLSv1.3, TLS handshake -> L6: TLS 협상 진행* SSL certificate verify ok -> L6: 인증서 검증 통과> GET /users HTTP/2 -> L7: HTTP 요청 전송< HTTP/2 200 -> L7: HTTP 응답 수신반례도 중요하다. curl이 Connected까지 찍고 그 뒤 TLS에서 실패하면 방화벽 문제라고 단정하면 안 된다. 이미 L4 연결은 맺혔기 때문이다. 반대로 HTTP 상태 코드가 아예 없고 연결 타임아웃만 난다면 애플리케이션 코드보다 L3/L4 경계를 먼저 보는 편이 빠르다.
Web Server는 HTTP 요청을 받는 서버다. 예전에는 정적 HTML, CSS, 이미지 파일을 직접 내려주는 의미가 컸고, 지금은 애플리케이션 서버 앞에서 리버스 프록시 역할을 하는 경우가 많다.
리버스 프록시는 서버 쪽 대리인이다. 클라이언트는 프록시를 실제 서버처럼 보고 요청을 보내지만, 프록시는 내부의 여러 애플리케이션 서버 중 어디로 보낼지 결정한다. 반대로 포워드 프록시는 클라이언트 쪽 대리인이다. 사내망에서 외부 인터넷으로 나갈 때 쓰는 프록시가 여기에 가깝다.
| 개념 | 누구 앞에 서 있는가 | 대표 책임 |
|---|---|---|
| Web Server | 클라이언트와 앱 사이 | 정적 파일 응답, TLS 종료, HTTP 요청 수신 |
| Reverse Proxy | 서버 앞 | 라우팅, 로드밸런싱, 헤더 추가, timeout 정책 |
| Forward Proxy | 클라이언트 앞 | 외부 접속 대리, 사내 보안 정책, 익명화 |
| WAS/App Server | 프록시 뒤 | 비즈니스 로직 실행, DB 조회, 응답 생성 |
| ALB | AWS 관리형 진입점 | L7 라우팅, Target Group, Health Check, TLS 종료 |
Nginx가 앞단이고 Node.js가 뒤단인 구성은 다음처럼 생각하면 된다.
flowchart LR Client["Client"] --> Edge["ALB or Nginx<br/>443 HTTPS"] Edge --> Static["Static files<br/>optional"] Edge --> App["Node.js or NestJS<br/>3000 HTTP"] App --> DB["DB or external API"] App --> Edge Edge --> Client
프록시가 하는 일은 단순 전달만이 아니다.
X-Forwarded-For, X-Request-Id, Host 같은 정보를 정리해 앱에 넘긴다.최소 Nginx 설정은 아래 정도만 읽으면 충분하다. 첫 회독의 목표는 모든 지시어를 외우는 것이 아니라 “443으로 받은 요청을 3000번 앱으로 넘긴다”는 구조를 읽는 것이다.
server { listen 443 ssl; server_name api.example.com;
ssl_certificate /etc/ssl/cert.pem; ssl_certificate_key /etc/ssl/key.pem;
location / { proxy_pass http://localhost:3000; 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; }}이 설정에서 계층 경계는 분명하다. listen 443 ssl은 외부 L4 포트와 L6 TLS 경계를 잡고, proxy_pass는 L7 HTTP 요청을 내부 앱 서버로 넘긴다. proxy_read_timeout은 프록시가 업스트림 HTTP 응답을 얼마나 기다릴지 정한다.
같은 Node.js API라도 앞단 구성에 따라 장애가 보이는 위치가 달라진다. 첫 회독에서는 아래 세 가지 패턴만 구분해도 충분하다.
| 패턴 | 요청 경로 | 장점 | 자주 보이는 실패 신호 |
|---|---|---|---|
| Node.js 직접 노출 | Client -> Node.js:3000 | 구조가 단순하고 로컬 실습에 쉽다. | TLS, 로드밸런싱, health check를 앱이 직접 떠안는다. |
| ALB -> Node.js | Client -> ALB:443 -> Node.js:3000 | ECS/Fargate와 잘 맞고 관리형 health check를 쓴다. | Target unhealthy, 503, ALB idle timeout, Security Group 차단 |
| ALB -> Nginx -> Node.js | Client -> ALB:443 -> Nginx -> Node.js | Nginx rewrite, buffer, 정적 파일, 세밀한 timeout을 추가할 수 있다. | 책임 경계가 늘어 502/504 원인을 한 단계 더 분리해야 한다. |
중요한 판단 기준은 “앞단을 하나 더 넣으면 무엇을 얻고 무엇을 잃는가”다. ALB만으로 충분한데 Nginx를 추가하면 설정과 장애 지점이 늘어난다. 반대로 Nginx가 필요한 rewrite, 정적 파일, 큰 헤더 처리, 특정 upstream 정책이 있다면 추가 비용을 감수할 이유가 생긴다.
작은 worked example을 보자. Client -> ALB -> Node.js 구조에서 503이 난다면 ALB가 보낼 정상 target을 찾지 못했다는 뜻에 가깝다. Client -> ALB -> Nginx -> Node.js 구조에서 502가 난다면 ALB가 Nginx를 못 만난 것인지, Nginx가 Node.js를 못 만난 것인지 로그 위치를 나눠야 한다. 같은 5xx라도 프록시가 하나 늘면 “누가 누구에게 실패했는가” 질문도 하나 늘어난다.
리버스 프록시는 내부 앱에 원래 클라이언트 정보를 전달하기 위해 헤더를 보강한다. 가장 자주 보는 헤더는 다음 세 가지다.
| 헤더 | 담는 정보 | 앱에서의 사용 예시 |
|---|---|---|
Host | 클라이언트가 요청한 도메인 | 멀티테넌트 라우팅, canonical URL 생성 |
X-Forwarded-For | 원래 클라이언트 IP와 프록시 체인 | 감사 로그, rate limit, 지역 기반 정책 |
X-Forwarded-Proto | 원래 요청이 HTTP였는지 HTTPS였는지 | HTTPS redirect 판단, secure cookie 설정 |
여기에는 중요한 반례가 있다. 앱이 인터넷에서 직접 들어온 X-Forwarded-For를 그대로 믿으면 클라이언트가 자기 IP를 위조할 수 있다. 이 헤더는 “신뢰하는 프록시가 덮어쓰거나 추가했다”는 전제가 있을 때만 의미가 있다.
예를 들어 요청 헤더가 아래처럼 앱에 도착했다고 하자.
X-Forwarded-For: 198.51.100.10, 10.0.1.25왼쪽은 원래 클라이언트 IP, 오른쪽은 가까운 프록시 IP처럼 해석하는 경우가 많다. 하지만 이 해석은 프록시 체인을 신뢰할 수 있을 때만 안전하다. 앱 서버가 public internet에 직접 노출되어 있다면 공격자가 이 헤더를 임의로 보낼 수 있다. 그래서 앱 서버는 보통 ALB나 Nginx 같은 신뢰 프록시에서 온 요청만 받고, 프록시가 전달한 헤더만 신뢰하도록 네트워크 경계를 닫아야 한다.
이 내용은 보안 심화가 아니라 프록시 학습의 일부다. 리버스 프록시는 요청을 전달할 뿐 아니라, 앱이 “누가 어떤 프로토콜로 들어왔는가”를 판단할 근거를 만들어 준다. 그 근거가 위조 가능하면 rate limit, 감사 로그, secure cookie 판단이 모두 틀어질 수 있다.
Nginx가 Apache prefork보다 자주 “동시 연결에 강하다”고 설명되는 이유는 이벤트 기반 I/O 모델 때문이다. 핵심은 연결 수와 스레드 수를 1:1로 묶지 않는 데 있다.
전통적인 연결당 프로세스/스레드 모델에서는 클라이언트가 요청을 거의 보내지 않고 연결만 유지해도 서버 리소스를 점유한다. keep-alive 연결 10,000개가 있고 각 연결이 스레드 스택 1MB를 예약한다고 가정하면, 실제 요청 처리 전에도 스택 예약만 약 10GB가 된다. 스택이 2MB라면 20GB다. 여기에 컨텍스트 스위치 비용과 프로세스 메타데이터가 붙는다.
이벤트 기반 모델에서는 worker가 많은 파일 디스크립터(fd)를 감시하다가, 읽기나 쓰기가 준비된 연결만 처리한다. idle 연결은 “감시 목록의 항목”에 가깝고, 요청 처리 중인 연결만 실제 CPU 시간을 쓴다.
| 모델 | 10,000 idle keep-alive 연결에서의 비용 감각 | 강한 상황 | 약한 상황 |
|---|---|---|---|
| 연결당 프로세스/스레드 | 스택과 스케줄링 비용이 연결 수에 거의 선형 증가 | 요청마다 CPU 작업이 길고 격리가 필요할 때 | idle 연결이 많고 keep-alive가 길 때 |
| 이벤트 기반 worker | fd와 버퍼 메타데이터 중심. 보통 수십~수백 MB 감각 | I/O 대기가 많고 요청 처리가 짧을 때 | CPU bound 작업이 worker를 오래 점유할 때 |
대표적인 C10K 비교에서는 이벤트 기반 Nginx가 10,000 idle 연결을 수십 MB 단위로 다룰 수 있었던 반면, 연결당 프로세스/스레드 모델은 스택 예약 때문에 GB 단위 메모리를 요구하기 쉬웠다. 정확한 숫자는 OS와 설정마다 달라지지만, 학습 포인트는 “연결 수가 늘어날 때 어떤 리소스가 선형으로 늘어나는가”다.
여기서 “Nginx는 항상 빠르다”는 결론을 내리면 틀린다. 이벤트 루프의 강점은 I/O 대기를 겹쳐 처리하는 것이다. 다음 작업은 이벤트 루프를 오래 붙잡아 장점을 깨뜨린다.
Node.js도 같은 원리를 공유한다. Nginx 앞단과 Node.js 뒷단은 둘 다 이벤트 기반 I/O에 강하지만, 둘 중 하나가 CPU bound가 되면 전체 요청 경로가 느려진다. 이때는 단순히 timeout을 늘리는 것보다 worker pool, queue, 별도 서비스 분리, 캐싱 같은 구조 변경을 검토해야 한다.
상황을 단순화해 보자. 모바일 클라이언트 10,000개가 앱을 켜 둔 채 60초 keep-alive 연결을 유지한다. 각 클라이언트는 30초에 한 번 작은 API 요청만 보낸다.
연결당 스레드 모델에서는 10,000개 연결이 동시에 서버 스케줄러의 관리 대상이 된다. 요청을 안 보내는 순간에도 스레드 스택과 컨텍스트 전환 후보가 남는다. 이벤트 기반 모델에서는 실제로 읽을 데이터가 생긴 소켓만 처리한다. 나머지는 커널 이벤트 감시 대상일 뿐이다.
이 차이가 C10K 문제의 핵심이다. “초당 요청 수”만 보지 말고 “동시에 열려 있는 연결 수”와 “그 연결들이 idle인지 active인지”를 같이 봐야 한다.
이 문서는 L2의 첫 문서이므로 ALB와 Envoy를 깊게 운영하는 법까지 요구하지 않는다. 다만 팀 서비스 앞단을 볼 때 어떤 이름이 어떤 역할을 뜻하는지는 구분해야 한다.
직접 운영하는 Web Server/Reverse Proxy다. 설정 파일로 라우팅, 정적 파일, rewrite, buffer, timeout을 세밀하게 다룰 수 있다.
EC2나 온프레미스에서 직접 제어가 필요하거나, 정적 파일/요청 재작성/부분 캐싱을 세밀하게 운영할 때AWS가 관리하는 L7 로드밸런서다. Target Group, Health Check, TLS 인증서 연결, ECS/EKS 통합을 관리형으로 제공한다.
ECS Fargate처럼 AWS 네이티브 환경에서 직접 프록시 운영 부담을 줄이고 싶을 때서비스 메시와 동적 설정에 강한 프록시다. gRPC, retry, outlier detection, circuit breaking 같은 데이터 플레인 기능이 풍부하다.
서비스 수가 많고, 서비스 간 통신 정책을 중앙에서 동적으로 제어해야 할 때의사결정의 기본값은 단순하다. 작은 AWS ECS 서비스라면 ALB 단독을 먼저 생각한다. Nginx는 “ALB가 못 해주는 세밀한 제어가 필요하다”는 이유가 있을 때 추가한다. Envoy는 서비스 메시 운영 비용을 감당할 만큼 서비스 간 통신 문제가 커졌을 때 검토한다.
반례를 같이 기억해야 한다.
max_fails 같은 passive health check는 upstream 서버가 여러 개일 때 의미가 커진다. 단일 upstream만 두고 “자동 제외되겠지”라고 가정하면 기대와 다르게 계속 실패 요청을 보낼 수 있다.즉 passive health check는 “앞단이 알아서 안전하게 제외해 주는 기능”이라기보다, 여러 후보 중 실패한 후보를 덜 쓰게 만드는 장치에 가깝다. 후보가 하나뿐이면 제외할 대체 경로가 없으므로, 단일 서버 구성을 안전망처럼 믿으면 silent failure를 만들기 쉽다.
ALB는 LCU(Load Balancer Capacity Unit) 단위로 사용량을 계산한다. 첫 회독에서는 모든 가격 공식을 외우지 않아도 된다. 다만 “요청 수만 비용을 만든다”는 직관은 틀릴 수 있다.
| 비용 차원 | 비용이 늘어나는 상황 예시 |
|---|---|
| 신규 연결 수 | keep-alive 없이 매 요청마다 새 연결을 만드는 클라이언트 |
| 활성 연결 수 | WebSocket, SSE처럼 오래 열려 있는 연결 |
| 처리 바이트 | 이미지, 파일 다운로드, 큰 JSON 응답 |
| 룰 평가 수 | listener rule이 많고 RPS가 높은 서비스 |
이 표의 목적은 ALB를 피하라는 뜻이 아니다. 관리형 진입점은 운영 부담을 크게 줄인다. 다만 대용량 미디어, 장기 연결, 룰이 많은 멀티테넌트 라우팅처럼 한 차원이 커지는 서비스에서는 비용 신호를 같이 봐야 한다. LCU는 보통 여러 차원을 각각 환산한 뒤 가장 크게 튀는 차원이 전체 사용량을 밀어 올린다고 보면 된다. RPS가 평범해도 WebSocket 연결이 오래 유지되거나, 응답 바이트가 크거나, listener rule 평가가 많으면 “요청 수는 적은데 왜 비용이 늘지?”라는 질문이 생길 수 있다.
긴 트러블슈팅 명령을 외우기보다, 실패 신호가 어떤 계층과 책임 경계를 가리키는지 이해하는 편이 먼저다.
HTTP Basics에서 502와 504를 상태 코드로 배웠다면, 여기서는 프록시 관점으로 다시 본다.
| 상태 코드 | 프록시가 본 상황 | 먼저 떠올릴 계층/경계 | 흔한 원인 | 잘못된 단정 |
|---|---|---|---|---|
| 502 Bad Gateway | 업스트림에 연결하지 못했거나, 업스트림 응답이 프록시가 받아들일 수 없는 형태였다. | L7 프록시 ↔ 앱, 일부 L4 포트 | 앱 프로세스 다운, 포트 불일치, 큰 응답 헤더, 연결 조기 종료 | ”무조건 앱 코드 예외다” |
| 504 Gateway Timeout | 업스트림 연결 후 응답을 기다리다 제한 시간을 넘겼다. | L7 timeout, 앱 내부 의존성 | 슬로우 쿼리, 외부 API 지연, 큐 미사용, 너무 긴 동기 작업 | ”timeout만 늘리면 해결된다” |
| 503 Service Unavailable | 프록시나 로드밸런서가 보낼 정상 target을 찾지 못했다. | 로드밸런서 Target/Health Check | 모든 target unhealthy, 배포 중 target 공백, 포트 차단 | ”앱은 떠 있으니 로드밸런서는 문제없다” |
502의 worked example을 하나 보자. Nginx가 connect() failed (111: Connection refused) while connecting to upstream를 남겼다면, 프록시는 살아 있고 뒤의 앱 포트 연결이 거절된 것이다. 이때는 TLS 인증서보다 앱 프로세스, 포트, proxy_pass 주소를 먼저 본다.
반대로 504는 프록시가 기다리다 포기한 신호다. 프록시와 앱 사이 연결 자체는 성립했을 수 있다. 무거운 CSV 생성, 슬로우 쿼리, 외부 결제 API 지연처럼 앱 안쪽의 시간이 긴 작업이 원인일 수 있다. 이 경우 proxy_read_timeout을 60초에서 300초로 늘리면 증상은 늦게 나타날 뿐, 요청이 worker와 connection pool을 오래 붙잡는 구조는 그대로 남는다.
퀴즈
힌트: 프록시가 뒤 서버를 만났는지, 만났지만 기다리다 포기했는지를 나눠 보라.
502는 프록시가 업스트림에 연결하지 못했거나 잘못된 응답을 받은 신호이고, 504는 연결 후 응답을 기다리다 시간 제한을 넘긴 신호다. 전자는 프로세스, 포트, 헤더 크기, 연결 종료를 먼저 보고, 후자는 느린 작업, timeout, 큐 분리, 의존성 지연을 본다.
Health Check는 로드밸런서가 “이 target에 새 요청을 보내도 되는가”를 판단하는 최소 신호다. 보통 /health 같은 경로에 주기적으로 요청을 보내고, 일정 횟수 이상 실패하면 target을 제외한다.
| 실패 신호 | 의미 | 먼저 볼 것 |
|---|---|---|
| Target이 계속 unhealthy | 로드밸런서가 앱을 정상 target으로 인정하지 않는다. | health check path, port, protocol, 응답 코드 |
| 배포 직후 503 | 정상 target이 일시적으로 0개가 되었을 수 있다. | 배포 전략, minimum healthy percent, deregistration delay |
| 앱은 실행 중인데 health만 실패 | 앱 포트와 health check 포트/경로가 다를 수 있다. | 컨테이너 port mapping, listener, target group |
| health는 성공인데 실제 API는 timeout | 얕은 health check가 내부 의존성 지연을 보지 못한다. | shallow/deep health 분리, DB/API latency, circuit breaker |
여기서 중요한 반례는 “헬스체크 200이면 서비스가 건강하다”는 믿음이다. /health가 단순히 { status: "ok" }만 반환하면 프로세스 생존은 확인하지만 DB, Redis, 외부 API 지연은 보지 못한다. 반대로 ALB의 public health check에 DB 조회까지 매번 넣으면 DB가 잠깐 느린 것만으로 정상 target을 모두 빼버릴 수 있다.
그래서 실무에서는 보통 빠른 shallow health와 관측용 deep health를 분리한다.
// ALB가 자주 호출하는 빠른 생존 확인@Get("/health")health() { return { status: "ok" };}
// 모니터링이나 운영자가 보는 의존성 확인@Get("/health/deep")async healthDeep() { await this.dataSource.query("SELECT 1"); return { status: "ok", db: "connected" };}이 코드는 운영 런북이 아니라 경계 설명이다. 로드밸런서가 모든 내부 의존성 상태를 자동으로 이해하지 못하므로, 어떤 신호를 health로 노출할지 애플리케이션이 설계해야 한다.
502는 앱 프로세스 다운만 뜻하지 않는다. Nginx가 업스트림 응답 헤더를 읽다가 버퍼 한계를 넘으면 upstream sent too big header while reading response header from upstream 같은 에러와 함께 502를 반환할 수 있다.
예를 들어 인증 응답이 큰 JWT와 여러 Set-Cookie를 포함해 헤더가 12KB가 되었다고 하자. 프록시의 기본 헤더 버퍼가 4KB 또는 8KB 수준이면 앱은 정상 200을 만들었는데 프록시가 응답을 받아들이지 못한다. 클라이언트에는 502만 보이므로 “앱이 죽었다”고 오해하기 쉽다.
이때의 판단 기준은 측정 후 최소한으로 키우는 것이다.
| 측정된 헤더 크기 | 해석 | 설정 감각 |
|---|---|---|
| 3KB | 기본 4KB 버퍼에서도 대체로 통과 | 설정 변경보다 원인 유지 관찰 |
| 6KB | 4KB 기본이면 실패, 8KB면 통과 가능 | 8KB 또는 16KB 경계 검토 |
| 12KB | 8KB도 부족할 수 있음 | 16KB 정도 검토, 헤더 자체 축소도 검토 |
| 50KB 이상 | 버퍼 증설보다 쿠키/JWT 설계부터 재검토 | 토큰 크기, 쿠키 분리, 응답 설계 |
무작정 proxy_buffer_size를 128KB로 키우면 연결당 메모리 비용도 커진다. 동시 연결 10,000개에 128KB를 잡으면 이 버퍼만 약 1.28GB 감각이다. 16KB로 충분한 응답에 128KB를 주는 것은 C10K에서 줄였던 연결당 비용을 다시 키우는 선택이다.
느린 업스트림은 빠르게 죽은 업스트림보다 더 위험할 때가 있다. TCP 연결은 살아 있으므로 프록시는 “연결 가능”하다고 보고 계속 요청을 보낸다. 하지만 앱은 DB나 외부 API 응답을 기다리며 connection pool과 worker를 점유한다.
정상:Client -> ALB -> App -> DB (100ms)
DB 지연:Client -> ALB -> App -----------------> DB (30s 이상) | | TCP 연결은 유지되지만 HTTP 응답이 늦다.이 흐름에서 shallow health check가 계속 200이면 ALB는 target을 healthy로 유지한다. 새 요청은 계속 들어오고, 앱의 DB connection pool은 느린 요청으로 찬다. 클라이언트는 timeout 후 재시도하고, 재시도가 다시 부하를 키운다. 이것이 cascading failure의 전형적인 모양이다.
실패 신호는 다음과 같이 읽는다.
/health는 200인데 실제 API p95/p99 latency가 급등한다.방어의 출발점은 timeout을 무작정 늘리는 것이 아니다. 느린 작업을 queue로 분리하거나, 앱 레벨 circuit breaker를 두거나, 의존성별 timeout을 프록시 timeout보다 짧게 잡아 실패를 빨리 표면화해야 한다.
OSI는 좋은 진단 언어지만 완벽한 구현도는 아니다. 특히 QUIC/HTTP3는 경계를 일부러 흐린다. QUIC는 UDP 위에서 동작하지만, TLS 1.3 암호화와 연결 관리, 스트림 멀티플렉싱을 함께 제공한다. OSI 식으로 나누면 L4, L5, L6, L7의 의미가 한 프로토콜 안에 섞여 있다.
그래서 HTTP/3 장애를 볼 때 ss -tan처럼 TCP 연결만 보는 도구로는 흐름이 보이지 않을 수 있다. UDP 443 트래픽이 있는데 TCP 443 연결이 기대와 다르게 보인다면 QUIC 경계를 의심해야 한다. 이것은 OSI 모델이 쓸모없다는 뜻이 아니라, 모델을 절대적인 계층표가 아니라 진단 질문 목록으로 써야 한다는 뜻이다.
Web Server는 요청을 받고 전달하는 경계에 강하다. Application Server는 비즈니스 로직을 실행한다. Nginx가 앞에서 TLS를 종료하고 X-Forwarded-For를 붙인 뒤 Node.js로 넘기면, Node.js는 사용자 인증, DB 조회, 주문 생성 같은 도메인 일을 처리한다.
둘을 섞어 생각하면 장애 진단이 흐려진다. 404가 Nginx에서 난 것인지 앱 라우터에서 난 것인지, 502가 앱 다운인지 프록시 버퍼 문제인지, 504가 프록시 timeout인지 앱 내부 의존성 지연인지 분리해야 한다.
ECS Fargate 같은 환경에서는 ALB가 보통 Nginx 역할의 상당 부분을 대신한다. TLS 종료, L7 라우팅, Health Check, Target Group 관리가 가능하다. 그래서 단순 API 서비스에는 ALB 단독 구성이 충분한 경우가 많다.
하지만 ALB가 모든 Nginx 기능을 그대로 제공하지는 않는다. 세밀한 rewrite, 특정 버퍼 정책, 파일 시스템 기반 정적 파일 서빙, 복잡한 캐싱은 Nginx가 더 직접적이다. 반대로 Nginx를 직접 운영하면 인증서 갱신, 설정 배포, 보안 패치, 커널 한계 같은 책임이 팀에 남는다.
HTTP/2는 하나의 TCP 연결에서 여러 스트림을 동시에 주고받아 요청 지연과 연결 수를 줄인다. HTTP/3는 QUIC를 사용해 TCP 수준의 Head-of-Line Blocking 일부를 줄인다.
그렇다고 모든 NestJS나 Node.js 앱 서버가 직접 HTTP/2/3를 처리해야 하는 것은 아니다. 많은 서비스에서는 클라이언트와 ALB/CloudFront/Nginx 사이에서 HTTP/2나 HTTP/3를 처리하고, 내부 앱 서버와는 HTTP/1.1로 통신한다. 첫 회독에서는 “엣지가 클라이언트 연결 최적화를 맡고, 앱은 HTTP 요청 의미를 처리한다”는 경계를 잡으면 된다.
증상: 배포 직후 502 Bad Gateway가 몇 분간 보이고 재시도하면 성공한다.
생각 순서:
증상: /reports/export만 60초 뒤 504가 나고 /users/me는 정상이다.
생각 순서:
증상: 브라우저가 ERR_CERT_COMMON_NAME_INVALID를 보여주고 앱 로그에는 요청이 없다.
생각 순서:
증상: Target은 healthy인데 실제 주문 API는 timeout이 늘어난다.
생각 순서:
/health면 프로세스 생존만 확인했을 수 있다.아래 명령은 외우기보다 “어떤 계층 신호를 보려는지”와 함께 읽는다. 실제 운영에서는 팀의 표준 관측 도구와 권한에 맞춰 바꿔야 한다.
# DNS가 어떤 IP로 해석되는지 확인한다.dig api.example.com
# TCP/TLS/HTTP 경계를 한 번에 훑는다.curl -v https://api.example.com/health
# 응답 시간과 상태 코드만 빠르게 본다.curl -o /dev/null -s -w "code=%{http_code} total=%{time_total}s\n" \ https://api.example.com/health
# 로컬 서버가 포트를 듣고 있는지 본다.ss -tlnp읽는 법:
Connected 이후 TLS에서 실패하면 L4는 통과했고 L6를 본다.listen 443 ssl;server_name api.example.com;proxy_pass http://localhost:3000;proxy_read_timeout 60s;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;읽는 법:
listen은 외부 포트와 TLS 경계를 정한다.server_name은 어떤 Host 요청을 받을지 정한다.proxy_pass는 내부 앱 서버 주소다.proxy_read_timeout은 504와 연결되는 대기 시간이다.X-Forwarded-For는 클라이언트 IP 전달과 신뢰 경계의 출발점이다.listen, server_name, proxy_pass, proxy_read_timeout의 의미를 읽을 때Web Server는 HTTP 요청 수신, 정적 파일, 프록시, TLS 종료가 중심이고, WAS나 앱 서버는 비즈니스 로직을 실행한다.
Nginx가 앞단이고 NestJS가 뒤단인 구조를 읽을 때리버스 프록시는 서버 앞에서 클라이언트 요청을 받아 내부 서버로 보내고, 포워드 프록시는 클라이언트 앞에서 외부 접속을 대신한다.
X-Forwarded-For, 사내 프록시, 클라이언트 IP 문제를 구분할 때Nginx는 이벤트 기반 worker로 많은 연결을 감시하고, Apache prefork는 연결당 프로세스/스레드 비용이 커지는 모델에 가깝다.
idle keep-alive 연결이 많거나 C10K 계열 문제를 볼 때ALB는 AWS 관리형 L7 로드밸런서이고, Nginx는 직접 운영하는 Web Server/Reverse Proxy다.
직접 제어와 운영 부담 중 무엇이 더 중요한지 결정할 때HTTP/2는 TCP 위의 멀티플렉싱이고, HTTP/3는 QUIC를 통해 UDP 위에서 스트림과 TLS를 함께 다룬다.
QUIC 트래픽이 OSI 계층표에 깔끔히 들어가지 않는 이유를 설명할 때TCP 3-way handshake, TLS handshake, Nginx upstream, reverse proxy, upstream timeout, Health Check, ALB Target Group, keep-alive, HTTP/2 multiplexing, QUIC, C10K, event loop, circuit breaker, X-Forwarded-For
proxy_pass, upstream, reverse proxy 설정의 공식 설명max_fails, fail_timeout 경계 확인| 항목 | 핵심 내용 |
|---|---|
| OSI 모델 | 실제 구현도가 아니라 장애를 계층별로 좁히는 진단 지도다. |
| TCP/IP 매핑 | 현실 구현은 TCP/IP가 더 가깝고, OSI의 L5/L6 일부는 애플리케이션 스택에 흡수된다. |
| Web Server | 클라이언트 요청을 받고 정적 파일, TLS 종료, 프록시, timeout 정책을 처리한다. |
| Reverse Proxy | 서버 앞에서 요청을 내부 앱으로 전달하고, 라우팅과 헤더 보강, 로드밸런싱을 수행한다. |
| C10K | 연결당 스레드 모델의 메모리/스케줄링 비용 때문에 많은 idle 연결이 문제가 된다. |
| 502 vs 504 | 502는 업스트림 연결/응답 문제, 504는 업스트림 응답 대기 시간 초과다. |
| Health Check | target 생존 신호일 뿐, 모든 내부 의존성의 건강을 자동 보장하지 않는다. |
| QUIC/HTTP3 | UDP 위에서 TLS와 스트림을 함께 다뤄 OSI 경계를 일부 흐린다. |
5줄 핵심
OSI 모델 & Web Server (지금 여기) ↓TCP/UDP Internals ← handshake, retransmission, timeout, TIME_WAIT ↓DNS Basics ← 도메인이 IP로 해석되는 과정과 캐싱 ↓HTTP Cache ← Cache-Control, ETag, CDN, freshness이 문서는 L2의 입구다. 다음 문서에서 TCP와 UDP를 더 깊게 보면, 여기서 “L4 포트/연결 문제”라고만 불렀던 현상을 handshake, retransmission, congestion, socket state 단위로 더 정확히 나눌 수 있다.
인터뷰 대비 핵심 질문
최종 수정: 2026-07-08