infra
Platform

모듈 맵

[Infra Ops] Web Server와 WAS 미들웨어 구조의 이해

0 / 52 완료

펼치기
0 / 52 완료0%

인프라 운영 & SRE · 09 / 52

[Infra Ops] Web Server와 WAS 미들웨어 구조의 이해

Nginx(Web Server)와 Tomcat(WAS)의 근본적인 차이를 이해하고, 역방향 프록시(Reverse Proxy)로 연동하는 실무 웹 서비스 아키텍처를 구축합니다.

🚨INCIDENT ALERT
HIGH

"마케팅 팀에서 이벤트를 진행하자마자 서버가 마비되었습니다. CPU 점유율은 낮은데 동적 API 서버가 응답을 안 해요!"

원인을 분석해보니, 이벤트 페이지에 삽입된 대용량 고화질 이미지 파일들을 사용자들이 다운로드하면서 문제가 생겼습니다. 동적 자바 비즈니스 로직을 수행해야 할 WAS의 스레드 풀 200개가 이미지 파일을 디스크에서 읽어 보내는 '정적 전송 대기 상태'로 모두 잠겨버린 것입니다.

만약 앞단에 가볍고 분주하게 움직이는 Web Server(Nginx)를 두고 이미지 전송을 전담시킨 뒤, 진짜 동적 연산(API)만 WAS(Tomcat)로 흘려보내는 Web-WAS 2-Tier 구조를 구축했다면, 단 한 대의 서버로도 마비 없이 수천 명의 트래픽을 소화했을 것입니다.

이번 챕터에서 배울 것
  • 1웹 서버(Nginx)와 웹 애플리케이션 서버(Tomcat)의 구조적 차이를 비교할 수 있다
  • 2이벤트 기반(Event-driven)과 멀티스레드 스레드 풀(Thread Pool) 동작 모델을 이해할 수 있다
  • 3리버스 프록시(Reverse Proxy) 연동 필요성을 이해하고 설정할 수 있다
  • 4Nginx 가상 호스트와 Tomcat 연동용 proxy_pass를 구성할 수 있다
  • 5클라이언트 실제 IP(X-Real-IP) 전송 헤더를 설정할 수 있다
  • 6502 Bad Gateway와 504 Gateway Timeout 장애 패턴을 분석할 수 있다
실습 환경 준비
Nginx 웹 서버 설치 (Ubuntu)
sudo apt-get update && sudo apt-get install -y nginx
Nginx 정상 구동 상태 확인
sudo systemctl status nginx
Nginx 구문 유효성 검사
sudo nginx -t

Web-WAS의 핵심 분리 철학

💡개념

안내원 Nginx와 요리사 Tomcat — 왜 역할을 나눠야 하는가

WAS(Tomcat)만 단독으로 운영하면 처음엔 잘 돌아가는 것처럼 보입니다. 그런데 이벤트 트래픽이 몰리거나 대용량 파일 요청이 겹치는 순간, 스레드 풀 200개가 이미지 전송 대기에 모두 묶여 정작 핵심인 API 처리가 멈춰버립니다. 이 문제를 구조적으로 해결하는 방법이 Web-WAS 분리입니다. 정적 파일은 Nginx가 초고속으로 처리하고, Tomcat은 오직 비즈니스 로직에만 집중하게 만드는 것이 핵심입니다.

Web-WAS 분리 구조 — Nginx가 정적 파일을 처리하고 동적 요청만 Tomcat으로 전달하는 흐름확대

Nginx (Web Server) = 카페 접수원

Nginx는 이벤트 루프(Event Loop)를 사용하는 비동기 구조입니다. CPU 코어당 1개의 Worker 프로세스만으로 수만 건의 정적 요청을 동시에 처리할 수 있습니다. 파일 I/O가 발생해도 스레드를 블로킹하지 않고 이벤트 큐에서 관리하기 때문에 메모리와 CPU 소모가 극단적으로 적습니다.

Tomcat (WAS) = 전문 요리사

Tomcat은 요청 1개당 JVM 스레드 1개를 완전히 점유하는 Thread-per-request 구조입니다. 스레드 풀이 최대 200개로 설정된 상태에서 200개의 느린 요청이 유입되면, 201번째 요청은 스레드가 릴리즈될 때까지 대기하다 타임아웃됩니다.

인터넷 요청이 Web Server와 WAS를 거치는 흐름입니다.

  1. 인터넷 요청Nginx Web Server (Port 80) — HTML·CSS·Image 직접 처리
  2. proxy_pass로 동적 요청만 전달 → Tomcat WAS (Port 8080) — 비즈니스 로직, DB 접근 집중

Web Server vs WAS — 정적/동적 분리 이유확대

💡개념

요청 하나가 Web과 WAS를 지나 응답이 되기까지 — 6단계

브라우저에서 페이지를 열면 잠깐 사이에 화면이 뜹니다. 그 짧은 순간에 요청은 Nginx(Web)와 Tomcat(WAS)을 정적/동적으로 갈라져 지나가고, 응답은 그 역순으로 되돌아옵니다. 이 흐름을 단계로 알면 "어디서 깨졌나"를 상태 코드로 좁힐 수 있습니다 — 정적 파일을 못 찾아 Web 앞단에서 막힌 404인지, 요청이 WAS까지 갔다가 로직에서 터진 500인지가 단계로 구분됩니다.

TEXT
[브라우저]  GET /order/123      ← 동적 요청 예시
   │
   ① Nginx(Web:80/443) 수신 — TLS 종단, location 규칙으로 정적/동적 판정
   │
   ② 정적/동적 분기
   │    ├─ 정적(/static/·.png·.css) → Nginx가 디스크에서 직접 응답하고 종료
   │    └─ 동적(/order/·/api/)       → proxy_pass로 아래 ③ 진행
   │
   ③ Nginx → Tomcat(WAS:8080) 중계 — X-Forwarded-For에 실제 클라이언트 IP 부착
   │
   ④ Tomcat 스레드 풀에서 워커 1개 배정 → 비즈니스 로직 실행(필요 시 DB 조회) → HTML/JSON 생성
   │
   ⑤ WAS → Nginx로 응답 반환
   │
   ⑥ Nginx가 헤더 보정·gzip 후 클라이언트로 전달
   ▼
[브라우저]  200 OK + 본문   (역순: WAS → Nginx → 브라우저)

각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:

단계하는 일여기서 막히면
① Web 수신클라이언트 TCP가 Nginx 80/443에 도달, TLS를 여기서 종단Nginx 다운·포트 미개방 → 브라우저에 Connection refused/타임아웃 (요청이 아예 안 들어옴)
② 정적/동적 분기location 규칙으로 정적은 Nginx가 직접, 동적은 WAS로 넘길지 결정정적 파일 경로·alias가 틀림 → Nginx가 곧장 404 (WAS까지 가지도 않음)
③ WAS 중계proxy_pass http://127.0.0.1:8080으로 동적 요청 전달WAS 다운·포트 불일치·바인딩 엇갈림 → Nginx가 502 Bad Gateway
④ 로직 실행스레드 배정 후 애플리케이션 코드 실행, 필요 시 DB 조회로직 예외(NPE 등) → WAS가 500 · 처리가 느려 지연 → proxy_read_timeout 초과 시 504
⑤ 응답 반환WAS가 생성한 응답을 Nginx로 되돌림응답 도중 WAS가 커넥션을 끊음 → 502(upstream prematurely closed)
⑥ Nginx 전달헤더 보정·압축 후 클라이언트로응답 버퍼·헤더 크기 초과 → 502(로그에 buffer 관련 에러)

정리하면 상태 코드가 "어느 단계에서 깨졌는지"를 가리킵니다. 정적 자원의 404는 대개 ②(Nginx가 파일을 못 찾음)로 WAS와 무관하고, 502는 ③(Nginx는 살아있으나 WAS에 연결 못함), 500은 ④(요청이 WAS까지 가서 로직이 터짐), 504는 ④(WAS가 제때 응답 못 줌)를 뜻합니다. 즉 대략 4xx는 Nginx 앞단, 5xx는 WAS 뒷단 상태를 비추는 신호입니다(단, 동적 경로에 매핑이 없어 나는 404는 WAS가 낼 수도 있으니, 정적 404인지 동적 404인지부터 가릅니다). 진단은 이 단계 지도 위에서 curl -I http://localhost(②까지)와 curl -v http://127.0.0.1:8080/(③을 우회해 WAS 직접)로 끊긴 지점을 좁히는 것입니다.

Nginx 리버스 프록시 연동 실습

1Nginx 가상 호스트 설정 파일 생성

기존 기본 설정을 비활성화하고, 프록시 헤더를 정확히 전달하는 전용 서버 블록을 작성합니다.

로컬 터미널
# 기본 설정 비활성화
sudo rm -f /etc/nginx/sites-enabled/default

# 새로운 전용 설정 파일 생성
sudo tee /etc/nginx/sites-available/infra-platform.conf > /dev/null <<'EOF'
server {
    listen 80;
    server_name localhost;

    # 정적 리소스는 Nginx가 직접 처리
    location /static/ {
        alias /var/www/infra-platform/static/;
        expires 30d;
        access_log off;
    }

    # 동적 API 요청은 WAS(Tomcat)로 중계
    location / {
        proxy_pass http://127.0.0.1:8080;
        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_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 10s;
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
    }
}
EOF
sudo rm -f /etc/nginx/sites-enabled/default && sudo tee /etc/nginx/sites-available/infra-platform.conf > /dev/null
🔍실행 후 확인할 것
  • ls /etc/nginx/sites-available/infra-platform.conf 로 파일 존재 먼저 확인, 그 다음 grep proxy_pass /etc/nginx/sites-available/infra-platform.conf 로 proxy_pass 지시어 확인 — 파일이 있어도 proxy_pass가 없으면 프록시 설정이 안 된 것
  • proxy_pass 기준: http://127.0.0.1:8080 이어야 정확 — localhost 대신 127.0.0.1을 써야 IPv6 혼용으로 인한 연결 실패 방지, 포트가 실제 WAS 포트와 다르면 즉시 수정
  • proxy_pass가 127.0.0.1:8080인데 proxy_set_header X-Real-IP가 없으면 → Nginx를 경유해도 WAS 로그에 클라이언트 IP 대신 127.0.0.1만 찍힘 — 헤더 설정 추가 후 반드시 nginx -t && systemctl reload nginx
2가상 호스트 활성화 및 설정 검증

심볼릭 링크로 설정을 활성화하고 문법 오류가 없는지 검사한 뒤 리로드합니다.

로컬 터미널
# 심볼릭 링크 생성
sudo ln -sf /etc/nginx/sites-available/infra-platform.conf /etc/nginx/sites-enabled/

# 구문 검사
sudo nginx -t
# 성공 출력: nginx: configuration file /etc/nginx/nginx.conf test is successful

# 무중단 설정 리로드
sudo systemctl reload nginx
sudo ln -sf /etc/nginx/sites-available/infra-platform.conf /etc/nginx/sites-enabled/ && sudo nginx -t
🔍실행 후 확인할 것
  • curl -I http://localhost 로 Server 헤더 먼저 확인, 그 다음 Tomcat 액세스 로그에서 클라이언트 IP 확인 — Server: nginx 가 보여야 Nginx가 앞단에서 프록시 역할 중, 없으면 Nginx를 경유하지 않는 것
  • 응답 코드 기준: 200=Nginx+WAS 모두 정상, 502=WAS가 다운이거나 8080 포트 연결 실패, 504=WAS 응답 시간 초과 — 502와 504는 Nginx 오류가 아니라 WAS 상태 문제
  • curl 200인데 WAS 로그에 127.0.0.1만 찍히면 → X-Real-IP 헤더 설정 누락 또는 Tomcat의 액세스 로그 패턴에 %{X-Forwarded-For}i 가 없는 것 — 두 곳 모두 확인 필요

심화 — 스레드 풀이 꽉 차는 순간, WAS 안에서 벌어지는 일

💡개념

심화: maxThreads·maxConnections·acceptCount — 포화의 3단 문턱

앞에서 "스레드 200개가 다 묶이면 201번째 요청은 대기하다 타임아웃"이라고 했습니다. 실제 Tomcat 안에는 그 '대기'를 결정하는 문턱이 하나가 아니라 셋입니다. 이 셋을 알아야 부하 상황에서 증상이 왜 단계적으로 바뀌는지가 보입니다.

  • maxThreads: 동시에 요청을 '처리'하는 워커 스레드 상한(기본 200). 이 스레드들이 다 바쁘면 새 요청은 더 이상 처리 단계로 들어오지 못합니다.
  • maxConnections: 동시에 '수락(accept)해 들고 있을' 수 있는 커넥션 총수. NIO 커넥터는 스레드보다 커넥션을 많이 붙잡을 수 있어, 처리할 스레드는 없어도 연결 자체는 유지한 채 순서를 기다리게 할 수 있습니다.
  • acceptCount: 위 둘이 모두 찼을 때 OS 소켓 backlog 큐에 줄 세울 수 있는 대기 연결 수. 이 큐마저 넘치면 커널이 새 연결을 즉시 거절(connection refused)합니다.

그래서 부하가 오르면 증상이 여유 → 지연(스레드 대기) → 큐 대기(acceptCount) → 거절로 단계적으로 악화됩니다. Nginx 관점에서 큐에서 오래 기다리다 응답이 늦으면 504, 커널이 연결을 거절하면 502로 보입니다. 즉 같은 502라도 'WAS가 죽어서'와 'WAS가 포화라서'는 전혀 다른 상태입니다.

한 가지 함정: 그래서 maxThreads만 무작정 키우면 되지 않습니다. 스레드는 대개 느린 다운스트림(특히 DB 커넥션 풀)을 기다리느라 묶이는데, 스레드만 늘리면 그만큼 DB 커넥션 경쟁·JVM 힙·컨텍스트 스위칭이 먼저 터집니다. 스레드 풀은 '동시에 감당 가능한 다운스트림 크기'에 맞춰야 합니다. 정적·대용량 전송을 Nginx로 빼는 이유가 바로 이 귀한 스레드를 비즈니스 로직에만 쓰기 위함입니다.

상황: 트래픽이 몰리는 시간대에 Tomcat은 죽지 않았고 catalina.out에도 특별한 에러가 없습니다. 그런데 어느 임계점부터 응답 시간이 계단식으로 뛰고, 전체가 아니라 일부 요청만 간헐적으로 502로 떨어집니다. WAS를 재시작하면 잠깐 괜찮아졌다가 부하가 다시 차면 재현됩니다.

원인: 'WAS 다운'이 아니라 'WAS 포화' 입니다. maxThreads가 모두 바쁜 상태라 새 요청은 즉시 처리되지 못하고, 커넥션은 maxConnections까지 수락돼 backlog 큐(acceptCount)에서 대기합니다 — 이때 지연이 계단식으로 뜁니다. 그 대기 큐마저 순간적으로 넘치는 연결은 커널이 거절해 Nginx 눈에 502로 보입니다. 그래서 '전체가 아니라 일부만' 502가 나는 것입니다.

진단: jstack $(pgrep -f tomcat)로 스레드 덤프를 떠서 http-nio-8080-exec-* 워커가 거의 전부 바쁜지, 그리고 대부분이 같은 지점(예: DB 커넥션 획득 대기)에 몰려 있는지 봅니다. JMX/Tomcat manager의 currentThreadsBusymaxThreads에 붙어 있는지, ss -tln에서 8080 리슨 소켓의 Recv-QacceptCount 근처까지 차 있는지도 확인합니다. 세 신호가 겹치면 포화가 확정입니다.

해결: 근본 원인은 대개 '스레드가 느린 다운스트림에 오래 붙잡혀 빨리 반납되지 않는' 것입니다. 슬로우 쿼리·외부 API 지연을 먼저 고치면 스레드 반납이 빨라져 같은 maxThreads로도 훨씬 많은 요청을 처리합니다. maxThreads를 늘려야 한다면 반드시 DB 커넥션 풀·JVM 힙과 함께 늘리고, 정적·대용량 응답은 Nginx로 넘겨 스레드를 아낍니다(WEB/WAS/DB 망 분리와 이중화 구조 설계).

💼
실무 맥락
현업 패턴

실제 업무에서 이 지식이 쓰이는 상황:

주니어 인프라 엔지니어로 입사하면 시니어가 툭 던지는 두 가지 지시가 있습니다.

"Nginx max_connections랑 Worker 프로세스 설정 좀 봐줘"

최대 동시 접속 수는 worker_processes × worker_connections입니다. lscpu로 CPU 코어 수를 확인하고 worker_processes auto로 설정합니다. ulimit -n(파일 디스크립터 한계)보다 worker_connections가 크면 기동 시 에러가 나므로 반드시 확인합니다.

"WAS 실주소 로깅 똑바로 되고 있는지 확인해봐"

Nginx에 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for가 들어있는지 확인합니다. Tomcat의 server.xml AccessLogValve 포맷 패턴에 %{X-Forwarded-For}i가 지정되어 있어야 클라이언트 실제 IP가 로그에 남습니다. 테스트 요청을 직접 쏴서 로그에 실제 IP가 찍히는지 증빙합니다.


명령어·단축키 빠른 참조

이 모듈에서 다룬 Nginx 프록시 연동과 502/504 진단 명령을 실전 옵션과 함께 모았습니다. "예" 열의 조합을 그대로 써도 됩니다.

명령어/단축키용도자주 쓰는 예
nginx -t설정 문법 검사(반영 전 필수)nginx -t && systemctl reload nginx
systemctl reload nginx무중단 설정 반영프로세스 끊김 없이 새 설정 적용
proxy_pass요청을 백엔드 WAS로 중계proxy_pass http://127.0.0.1:8080; (localhost 대신 IP)
proxy_set_header클라이언트 정보 헤더 전달X-Real-IP $remote_addr, X-Forwarded-For $proxy_add_x_forwarded_for
proxy_read_timeoutWAS 응답 대기 시간초과 시 504 — proxy_read_timeout 60s;
worker_processes / worker_connections최대 동시 접속(둘의 곱)worker_processes auto;, ulimit -n보다 작게
ss -tlnp | grep 8080WAS가 8080에 바인딩됐는지502 시 백엔드 생존 1차 확인
curl -I http://localhost프록시 응답·Server 헤더 확인백엔드 직접: curl -v http://127.0.0.1:8080/
ps -ef | grep tomcatWAS 프로세스 생존 확인PID만: pgrep -f tomcat
jstack <PID>스레드 덤프(포화 진단)jstack $(pgrep -f tomcat) > dump.txt
tail -f /var/log/nginx/error.log프록시 오류 실시간 확인Connection refused 대상 파악
ln -sf …sites-available …sites-enabled/가상호스트 활성화활성화 후 nginx -t

관련 모듈로 더 깊이:

다음 모듈에서는 Web/WAS/DB를 실제 네트워크 망으로 분리하는 3-Tier 이중화 구조를 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

정적 자원(HTML, CSS, Image)만 처리할 때 Nginx(Web Server)가 Tomcat(WAS)보다 압도적으로 유리한 기술적 이유는 무엇입니까?

Q2

Nginx 설정에서 'proxy_pass http://127.0.0.1:8080;'을 추가했습니다. 이 설정의 정확한 역할을 가장 잘 설명한 것은?

Q3

Nginx를 거쳐 WAS에 도달했을 때, WAS의 애플리케이션 로그에 실제 클라이언트 IP가 아닌 '127.0.0.1'만 남는 문제가 생겼습니다. 이 현상의 근본 원인과 해결책은 무엇입니까?

Q4

Nginx로 웹 애플리케이션 접근 시 '502 Bad Gateway' 에러 페이지가 노출되었습니다. 이 에러 코드가 지닌 가장 유력한 의미는 무엇입니까?

Q5

Web Server(Nginx)와 WAS(Tomcat)를 분리하는 핵심 이유는?

Q6

WAS(Tomcat 8080)를 외부에 직접 노출하지 않고 Nginx 뒤에 두는 보안상 이점은?

Q7

[심화] Tomcat에서 maxThreads가 모두 사용 중이 됐을 때, 새로 들어온 요청이 곧바로 connection refused로 거절되지 않고 잠시 대기할 수 있는 이유는?

Q8

[심화] Tomcat이 죽지도 않았고 로그도 깨끗한데, 동시 접속이 늘면 응답이 계단식으로 느려지다가 일부 요청만 502가 난다. 원인을 확정하기 위한 진단으로 가장 유효한 것은?

0 / 8 답변

🧪 실습으로 확인하기

로드밸런서 헬스체크·페일오버 — 죽은 노드로 트래픽이 가는 이유

중급

백엔드 1대가 죽었는데 LB가 계속 그쪽으로 보내 502가 절반씩 난다. 헬스체크가 왜 동작하지 않는지(경로·주기·임계치) 진단하고, 헬스체크를 바로잡아 죽은 노드를 풀에서 자동 제외하고, 노드를 살린 뒤 자동 복귀까지 확인한다. 헬스체크 경로가 실제 서비스 상태를 반영하는지(얕은 체크의 함정)도 다룬다.

45📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요