웹 서버, WAS, 그리고 Ingress까지
- -

들어가며
이번에 프로젝트에서 인프라를 전담으로 담당해주신 분이 계셔서 크게 신경은 안쓰고 있다가, 이번 기회에 한번 제대로 알고 넘어가야할 것 같아 글로써 정리한다.
면접에서 만약 다음 질문을 받는 상황을 가정해보았다.

사실 당연히 알고 있다고 생각했는데, 막상 설명하려고 하면 말이 잘 안 나왔다.
데브코스 프로젝트에서 실제로 Nginx, Traefik(k3s 기본 인그레스), Spring Cloud Gateway를 모두 썼는데도 왜 각각을 쓰는지 명확하게 정리가 안 돼있었다.
그래서 이참에 웹 서버와 WAS의 차이부터 시작해서 Reverse Proxy, Ingress까지 한 번에 정리해봤다.
웹 서버(Web Server)와 WAS의 차이
이미 아는 내용이지만, 후술할 내용들을 위한 선수 지식을 위해 작성한다.
웹 서버란
웹 서버는 정적 파일을 서빙하는 서버다.
HTML, CSS, JS, 이미지처럼 요청이 들어올 때마다 내용이 바뀌지 않는 파일을 그냥 읽어서 돌려준다. 코드를 실행하지 않는다.
대표적인 웹 서버: Nginx, Apache
WAS란
WAS(Web Application Server)는 동적인 요청을 처리하는 서버다.
"이 사용자의 주문 목록을 DB에서 조회해서 JSON으로 응답해줘" 같은 비즈니스 로직이 필요한 요청을 처리한다. Java 코드를 실행하고, DB와 통신하고, 응답을 만들어낸다.
대표적인 WAS: Tomcat (Spring Boot 내장)
그러면 웹 서버는 왜 필요한가
Spring Boot만 있으면 되는 거 아닌가? 라고 생각할 수 있다.
사실 맞다. 소규모 서비스라면 WAS만으로도 동작한다. 하지만 웹 서버를 앞에 두면 다음과 같은 이점이 생긴다.
| 역살 | 웹 서버 (Nginx) | WAS (Tomcat) |
| 정적 파일 서빙 | 매우 효율적 | 비효율적 |
| SSL/HTTPS 처리 | 담당 | 부담됨 |
| Slow Client 방어 | 버퍼링 가능 | 스레드 낭비 |
| 로드 밸런싱 | 가능 | 본래 역할 아님 |
| 동적 요청 처리 | 못함 | 담당 |
여기서의 핵심은 Slow Client 문제다.
WAS는 Java 스레드로 요청을 처리한다. 클라이언트가 응답을 느리게 받으면 그 시간 동안 스레드가 계속 점유된다.
Nginx가 앞에서 응답을 버퍼링해주면 WAS 스레드는 Nginx에 빠르게 응답을 넘기고 바로 반환할 수 있다.
웹 서버를 앞에 두면 요청 흐름
Nginx를 앞에 두면 외부 요청은 무조건 Nginx를 먼저 거친다.

이 때, Nginx는 요청을 보고 직접 처리할지, WAS로 넘길지 판단한다.
location /static/ {
root /var/www/; # 정적 파일은 Nginx가 직접 처리
}
location /api/ {
proxy_pass http://localhost:8080; # 동적 요청은 WAS로 전달
}
여기서 응답도 나갈 떄 Nginx를 거치는 이유는 Slow Client 방어 때문이다.
WAS는 Nginx에 응답을 넘기고 바로 스레드를 반환한다. 이후 클라이언트가 느리게 받더라도 WAS는 이미 다른 요청을 처리 중이다.
이처럼 클라이언트 입장에서 WAS의 존재가 완전히 숨겨지는 구조를 Reverse Proxy라고 한다.
Reverse Proxy란
Forward Proxy vs Reverse Proxy
비교해서 보면 차이가 명확하다.
Forward Proxy는 클라이언트를 숨기는 프록시다.
Client → [Forward Proxy] → Server
서버 입장에서 실제 클라이언트가 누군지 모른다. VPN, 사내망 우회 등에 쓰인다.
반대로 Reverse Proxy는 서버를 숨기는 프록시다.
Client → [Reverse Proxy] → Server
클라이언트 입장에서 실제 서버가 몇 대인지, 어디 있는지 모른다. Nginx, API Gateway 등이 이 역할을 한다.
| Foward Proxy | Reverse Proxy | |
| 누구를 숨기나 | 클라이언트 | 서버 |
| 누가 설치하나 | 클라이언트 측 | 서버 측 |
| 대표 사례 | VPN, 사내망 | Nginx, API Gateway |
Ingress란
쿠버네티스를 사용하지 않는 경우
서버가 여러 대 있을 때 각 서버에 포트를 따로 열어주면:
jabaclass.store:8080 → Gateway
jabaclass.store:3000 → Nginx
jabaclass.store:9090 → Grafana
사용자가 포트까지 직접 입력해야 하고, 내부 포트가 외부에 전부 노출된다.
이런 문제점을 해결하기 위해 등장한 것이 Ingress이다.
Ingress가 하는 일
Ingress는 URL 경로만으로 내부 서비스로 연결해주는 단일 창구다.
jabaclass.store/api → Gateway
jabaclass.store/ → Nginx
내부 서비스들은 외부에 노출되지 않는다.
Ingress vs Ingress Controller
인그레스와 인그레스 컨트롤러에 대해 알아보자.
| 역할 | |
| Ingress | 어떤 경로를 어디로 보낼지 정의한 규칙 명세 (YAML) |
| Ingress Controller | 그 규칙을 읽고 실제로 트래픽을 분기하는 실행 주체 |
비유하자면 Ingress는 업무 안내문("민원은 1번 창구, 세금은 2번 창구")이고, Ingress Controller는 그 안내문을 보고 실제로 사람을 안내하는 직원이다.
k3s는 Ingress Controller로 Traefik이 기본 내장돼있다. 일반 k8s는 직접 선택해서 설치해야 한다. (우리는 기본 내장 인그레스 사용)
우리 프로젝트 실제 구조
일반적으로는 Nginx가 Reverse Proxy 역할까지 담당하는 경우가 많다.
그런데 MSA 환경에서는 서비스가 여러 개이다 보니 Nginx만으로는 세밀한 라우팅이 어렵다.그래서 API Gateway를 별도로 두고 Reverse Proxy 역할을 맡기는 구조를 많이 사용한다.
우리 프로젝트도 마찬가지였다. 결론적으로는 Nginx는 정적 파일 서빙만 담당하고, Reverse Proxy는 Spring Cloud Gateway가 담당하도록 하였다.

인그레스 적용 예시
인프라 담당해주신 팀원분께서 main-ingress.yml에 정의된 규칙대로 경로를 분기해주셨다.
- /api, /oauth2 등 → Spring Cloud Gateway (8080)
- / → Nginx 프론트엔드 서비스 (3000)
- HTTPS 처리 (cert-manager + Let's Encrypt)
spec:
ingressClassName: traefik
rules:
- host: jabaclass.store
http:
paths:
- path: /api
backend:
service:
name: api-gateway-service
port:
number: 8080
- path: /
backend:
service:
name: frontend-service
port:
number: 3000
Spring Cloud Gateway가 한 일 — 실제 Reverse Proxy
우리 프로젝트에서 Reverse Proxy 역할을 실제로 담당한 건 Spring Cloud Gateway다.
Traefik에서 /api 요청을 넘겨받으면, 경로를 보고 어느 마이크로서비스로 보낼지 결정한다.
| 경로 | 라우팅 대상 |
| /api/v1/auth/**, /api/v1/users/** | user 서비스 (9003) |
| /api/v1/payments/** | payment 서비스 (9001) |
| /api/v1/products/**, /api/v1/files/** | product 서비스 (9004) |
| /api/v1/orders/** | order 서비스 (9005) |
| /api/v1/admins/** | admin 서비스 (9007) |
| /api/v1/recommendations/** | ai 서비스 (9009) |
라우팅 외에도 Gateway에서 공통으로 처리한 것들이 있다.
- JWT 인증 필터 — 모든 요청의 토큰 검증을 Gateway에서 일괄 처리
- Redis Rate Limiting — 이메일 API 등 호출 횟수 제한
- Resilience4j 서킷브레이커 — 특정 서비스 장애 시 요청 차단
각 마이크로서비스는 인증이나 Rate Limiting을 신경 쓰지 않아도 된다. Gateway가 앞에서 다 걸러주기 때문이다.
Nginx가 한 일
잡아클래스의 Nginx는 Reverse Proxy가 아니라 순수한 정적 파일 서버로만 동작했다.
프론트엔드 컨테이너 안에 Nginx를 띄우고, Vue 빌드 결과물(index.html, .js, .css 등)을 /usr/share/nginx/html 디렉토리에 올려뒀다. 즉, 엔진엑스가 하는건 브라우저에서 요청이 오면 그냥 해당 파일을 읽어서 돌려주는 것이 전부다.
server {
listen 3000;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
여기서 try_files $uri $uri/ /index.html 부분이 핵심이다.
Vue는 SPA(Single Page Application)라서 실제 HTML 파일은 index.html 하나뿐이다.
브라우저 주소창이 /class/123 이든 /mypage 이든 서버에 해당 경로의 파일은 존재하지 않는다. 그래서 어떤 경로로 요청이 와도 일단 index.html을 돌려주고, 이후 라우팅은 브라우저가 JS로 처리하는 구조다.
만약 try_files가 없으면 /class/123 으로 직접 접근했을 때 Nginx가 "그런 파일 없음(404)"을 반환한다.
CSR 전체 흐름
우리 프로젝트는 CSR(Client Side Rendering) 방식이었다.
CSR은 서버가 완성된 HTML을 주는 게 아니라, 브라우저가 JS를 실행해서 화면을 직접 그리는 방식이다.
실제 흐름을 따라가보면 이렇다.
1단계 — 페이지 접속
브라우저가 jabaclass.store/ 로 요청을 보내면 Traefik이 받아서 / 경로니까 Nginx로 전달한다. Nginx는 index.html 파일을 돌려준다. 이 시점에서 화면에는 아무것도 없다.
2단계 — JS 실행
브라우저가 index.html 안에 있는 JS 파일을 다운받아 실행한다. JS가 화면을 그린다.
3단계 — 데이터 요청
화면을 그리다 보면 클래스 목록 같은 데이터가 필요하다. 이때 브라우저가 jabaclass.store/api/v1/products 로 별도 API 요청을 보낸다.
이 요청은 처음 페이지 요청이랑 완전히 똑같이 Traefik부터 다시 시작한다. Traefik이 /api 경로를 보고 Gateway로 전달하고, Gateway가 product 서비스(9004)로 라우팅한다.
4단계 — 화면 갱신
서버에서 JSON 응답이 오면 JS가 받아서 화면에 데이터를 채워 넣는다.
정리
| 구성 요소 | 역할 |
| Traefik | Ingress Controller. 외부 요청을 받아 경로 기반으로 분기, HTTPS 처리 |
| Nginx | 프론트엔드 Pod 내부 정적 파일 서버. index.html 서빙 |
| Spring Cloud Gateway | API 요청 라우팅, JWT 인증 |
| 각 MSA 서비스 | 실제 비즈니스 로직 처리 |
정리하자면, 웹 서버와 WAS는 각자 잘하는 일이 다르기 때문에 분리한다.
Reverse Proxy는 서버를 숨기고 버퍼링, 로드밸런싱, SSL을 담당하며, Ingress는 쿠버네티스 환경에서 Reverse Proxy 역할을 추상화한 것이다.
'🧑💻 Backend' 카테고리의 다른 글
| 멱등성(Idempotency)에 대하여 (0) | 2026.05.26 |
|---|---|
| 동기 / 비동기와 블로킹 / 논블로킹 개념 (0) | 2026.02.23 |
| 트랜잭션&락 2편 - InnoDB의 Redo / Undo Log, MVCC (0) | 2025.12.22 |
| 트랜잭션&락 1편 - All Or Nothing / ACID (0) | 2025.12.22 |
| Index 4편 - MySQL의 구조(Optimizer, Storage Engine), 쿼리 플랜 (1) | 2025.12.17 |
소중한 공감 감사합니다