Nginx 리버스 프록시: 서비스 앞에 ‘문지기’를 세우자

📖 13min read

서버가 많아지니, 길을 잃기 시작했다

Redis를 도입하고 나서 API 응답은 눈에 띄게 빨라졌다. 하지만 서비스가 성장하면서 새로운 골칫거리가 생겼다. 서버가 점점 늘어난 것이다.

  • Spring Boot 백엔드: http://서버IP:8080
  • Vue.js 프론트엔드: http://서버IP:3000
  • 개발 서버 백엔드: http://서버IP:8081
  • Redis: 6379 포트
  • PostgreSQL: 5432 포트

담당자에게 개발 서버 URL을 알려줄 때, 매번 포트 번호를 붙여서 보내야 했다.

“확인하실 때 http://123.45.67.89:3000 으로 접속하세요.” “어? 뒤에 3000은 뭐예요? 그냥 주소만 치면 안 돼요?”

사실 우리가 일상에서 접속하는 네이버, 구글에는 포트 번호를 치지 않는다. 그냥 naver.com만 치면 된다. 그런데 왜 내 서비스는 포트 번호를 외워야 하는 걸까?

그리고 더 큰 문제가 있었다. 프론트엔드(3000)에서 백엔드(8080)로 API를 호출할 때, 또다시 ‘CORS’ 에러가 터진 것이다. 시리즈 2에서 SSR/CSR을 다루면서 한번 겪었던 바로 그 문제였다.

‘이 모든 문제를 한 방에 해결할 방법이 없을까?’

검색 끝에 나온 답은 ‘Nginx(엔진엑스)’였다.

서비스가 늘어나면, 요청을 정리해줄 교통 경찰이 필요하다.

잠깐, ‘웹 서버’가 뭔가?

Nginx를 알기 전에 먼저 ‘웹 서버(Web Server)’라는 개념을 짚고 가자. 우리가 브라우저에 주소를 치면 화면이 뜨는데, 그 화면을 구성하는 HTML, CSS, JS, 이미지 같은 파일들을 ‘전달해주는 프로그램’이 바로 웹 서버다.

비유하자면, 웹 서버는 ‘서점 직원’이다. 손님(브라우저)이 "index.html 주세요"라고 요청하면, 창고(서버 디스크)에서 해당 파일을 꺼내 건네주는 것이다. Spring Boot나 Django 같은 ‘애플리케이션 서버(WAS)’가 주문에 맞게 음식을 직접 조리하는 ‘주방’이라면, 웹 서버는 완성된 요리를 손님 테이블에 가져다주는 ‘홀 직원’인 셈이다.

Apache: 한 시대를 지배한 원조 웹 서버

웹 서버의 역사에서 가장 먼저 떠오르는 이름은 ‘Apache(아파치)’다. 1995년에 등장한 Apache는 무려 20년 넘게 웹 서버 점유율 1위를 차지했다. 한국의 공공기관이나 SI 현장에서 ‘Apache + Tomcat’ 조합은 아직도 흔히 볼 수 있다.

하지만 Apache에는 구조적 한계가 있었다. 요청이 들어올 때마다 프로세스나 스레드를 새로 만들어 처리하는 방식이라, 동시 접속자가 수천 명을 넘어가면 메모리 사용량이 폭증했다. 당시에는 이 정도면 충분했지만, 인터넷 사용자가 폭발적으로 늘어나면서 한계가 드러났다.

Nginx: 가볍고 빠른 차세대 웹 서버

2004년, 러시아 개발자 Igor Sysoev가 이 문제를 해결하려고 만든 것이 ‘Nginx(엔진엑스)’다. Nginx는 요청마다 프로세스를 만드는 대신, ‘이벤트 기반 비동기 처리’라는 방식을 사용한다. 하나의 프로세스가 수만 개의 요청을 동시에 처리할 수 있어, 메모리를 거의 먹지 않으면서도 엄청나게 빠르다.

항목 Apache Nginx
처리 방식 요청마다 프로세스/스레드 생성 이벤트 기반 비동기 처리
동시 접속 수천 명이면 메모리 부담 수만 명도 거뜬
정적 파일 전송 보통 압도적으로 빠름
리버스 프록시 가능하지만 설정 복잡 이게 주력 기능, 설정 직관적
현재 점유율 2위 (레거시 중심) 1위 (신규 프로젝트 표준)

지금 새로운 프로젝트를 시작한다면 Nginx를 선택하는 것이 업계 표준이다. Apache를 만날 일은 주로 기존 레거시 시스템을 유지보수할 때다.

Nginx의 두 가지 무기

Nginx는 단순한 웹 서버를 넘어, ‘리버스 프록시’라는 강력한 무기를 가지고 있다. 이 두 역할이 뭔지 알면 Nginx가 왜 필요한지 바로 이해된다.

무기 1: 웹 서버 — 정적 파일 배달부

위에서 말한 웹 서버 본연의 역할이다. 프론트엔드(Vue.js)를 빌드하면 나오는 HTML, CSS, JS 파일을 Nginx가 직접 전달한다. 이걸 Spring Boot가 배달할 필요 없으니, Spring Boot는 API 처리에만 집중할 수 있다.

무기 2: 리버스 프록시 — 교통 경찰

이게 핵심이다. 사용자의 모든 요청을 먼저 Nginx가 받고, 내용을 보고 적절한 서버로 전달해주는 역할이다.

[기존] 사용자 → http://서버:3000 (프론트)
       사용자 → http://서버:8080 (백엔드 API)
       → 포트 번호 노출, CORS 문제 발생

[Nginx 도입 후] 사용자 → http://mydomain.com (Nginx, 80번 포트)
                           ├── /           → 프론트엔드 (3000)
                           └── /api/*      → 백엔드 (8080)
               → 포트 번호 숨김, CORS 문제 해결

사용자 입장에서는 하나의 주소(mydomain.com)로 접속할 뿐이다. 뒤에서 요청이 어디로 가는지는 Nginx만 알고 있다. 마치 호텔 프런트 데스크처럼, 손님이 "레스토랑 어디예요?"라고 물으면 직접 안내해주는 것이다.

리버스 프록시는 호텔 프런트 데스크다. 손님은 정문만 알면 되고, 내부 구조는 몰라도 된다.

실전: Nginx 설정 파일 작성

Nginx의 핵심은 설정 파일 하나다. 위에서 말한 교통 정리 규칙을 이 파일에 적는다.

설치 (Docker)

# Nginx 컨테이너 실행
docker run -d --name nginx \
  -p 80:80 \
  -p 443:443 \
  -v /etc/nginx/conf.d:/etc/nginx/conf.d \
  --restart always \
  nginx:alpine

설정 파일 작성

# /etc/nginx/conf.d/default.conf

server {
    listen 80;
    server_name mydomain.com;

    # 1. 프론트엔드 요청 처리
    # "/" 로 들어오는 모든 요청은 프론트엔드로 보낸다.
    location / {
        proxy_pass http://localhost:3000;
    }

    # 2. 백엔드 API 요청 처리
    # "/api/" 로 시작하는 요청은 Spring Boot로 보낸다.
    location /api/ {
        proxy_pass http://localhost:8080;

        # 원본 요청 정보를 백엔드에 전달 (로깅, 보안용)
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

이 설정 하나로 세 가지 문제가 동시에 해결된다.

문제 해결
포트 번호 노출 사용자는 80번 포트(기본)만 접속. 내부 포트 숨김.
CORS 에러 프론트와 백엔드가 같은 도메인(mydomain.com)으로 통일. 출처가 같으니 CORS 없음.
서버 구조 노출 외부에서는 내부에 서버가 몇 대인지, 어떤 포트를 쓰는지 알 수 없음.
# 설정 파일 문법 검사
docker exec nginx nginx -t

# 설정 반영 (서비스 중단 없이!)
docker exec nginx nginx -s reload

nginx -s reload는 서비스를 끊지 않고 설정만 갱신한다. 사용자는 설정이 바뀐 줄도 모른다. 이것이 실무에서 Nginx가 사랑받는 이유 중 하나다.


HTTPS 적용: 보안 시리즈와의 연결

시리즈 3(Works on My Machine)의 보안 편에서 Let’s Encrypt로 HTTPS를 적용했던 것을 기억하는가? 그때 certbot --nginx 명령어를 썼는데, 사실 그게 바로 Nginx 설정 파일에 SSL 인증서를 자동으로 추가해주는 것이었다.

HTTPS까지 적용하면 설정이 이렇게 확장된다.

# HTTPS 설정 (certbot이 자동으로 생성해주는 부분)
server {
    listen 443 ssl;
    server_name mydomain.com;

    ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;

    location / {
        proxy_pass http://localhost:3000;
    }

    location /api/ {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# HTTP → HTTPS 자동 리다이렉트
server {
    listen 80;
    server_name mydomain.com;
    return 301 https://$host$request_uri;
}

http://로 접속해도 자동으로 https://로 전환된다. 사용자는 항상 암호화된 통신을 하게 되고, 브라우저에 자물쇠 아이콘이 표시된다.


한 걸음 더: 로드 밸런싱

지금은 서버가 1대라 필요 없지만, 나중에 트래픽이 늘어서 Spring Boot를 여러 대 돌려야 한다면? Nginx는 로드 밸런서 역할도 한다.

# Spring Boot가 2대일 때 (8080, 8081)
upstream backend {
    server localhost:8080;
    server localhost:8081;
}

server {
    listen 80;

    location /api/ {
        proxy_pass http://backend;  # 요청을 두 서버에 번갈아 분배
    }
}
분배 방식 설명
Round Robin (기본) 순서대로 번갈아 보냄
Least Connections 현재 연결이 적은 서버로 보냄
IP Hash 같은 IP는 항상 같은 서버로 보냄 (세션 유지)

1인 개발 단계에서는 로드 밸런싱은 거의 쓸 일이 없다. 하지만 Nginx를 앞에 세워두면, 나중에 서버를 늘릴 때 설정 몇 줄 추가로 확장이 가능하다는 것이 핵심이다.

Nginx 하나면 서버가 1대든 10대든, 사용자는 같은 주소로 접속한다.

실무 조언: Nginx를 쓰면서 겪은 실수들

1. proxy_pass 끝에 슬래시(/) 주의

# 이 두 설정은 결과가 다르다!
location /api/ {
    proxy_pass http://localhost:8080;    # /api/users → 8080/api/users
}

location /api/ {
    proxy_pass http://localhost:8080/;   # /api/users → 8080/users  (api가 잘림!)
}

슬래시 하나 차이로 경로가 완전히 달라진다. 설정 후 반드시 테스트하자.

2. 502 Bad Gateway

Nginx는 살아있는데 뒤쪽 서버(Spring Boot)가 죽었을 때 나오는 에러다. Nginx 자체의 문제가 아니라, proxy_pass가 가리키는 서버가 응답하지 않는 것이다. Spring Boot 로그를 확인하자.

3. 설정 변경 후 반드시 문법 검사

# 문법 검사 (실수 방지)
nginx -t

# 검사 통과 후에만 재로드
nginx -s reload

설정 파일에 오타가 있으면 nginx -s reload가 실패하면서 기존 설정도 날아갈 수 있다. 반드시 -t로 먼저 검증하자.


마치며: Nginx는 ‘시스템의 현관문’이다

Nginx를 도입하고 나니, 마침내 나의 서비스가 ‘프로처럼’ 보이기 시작했다.

  • 포트 번호 없이 깔끔한 URL
  • HTTPS 자물쇠 아이콘
  • CORS 에러 없는 쾌적한 프론트-백엔드 통신
  • 나중에 서버를 늘려도 URL 변경 없는 확장성

돌이켜보면, Nginx는 보안 편에서 다뤘던 방화벽(UFW)과 맞닿아 있다. UFW가 ‘어떤 포트를 열고 닫을지’ 결정한다면, Nginx는 ‘열린 포트(80, 443)로 들어온 요청을 어디로 보낼지’ 결정한다. 둘이 합쳐져서 비로소 서비스의 든든한 현관문이 완성되는 것이다.

이제 서비스는 빠르고(Redis), 요청도 잘 정리되고(Nginx) 있다. 하지만 아직 한 가지 빠진 게 있다. ‘지금 서비스가 잘 돌아가고 있는지’ 확인할 방법이 없다는 것이다. 서버가 죽었는데 고객 전화가 올 때까지 모르는 건 최악이다.

다음 시간, 시리즈의 마지막 편에서는 ‘로그 관리와 모니터링’에 대해 알아보자. 내 서비스가 지금 건강한지, 어디가 아픈지를 실시간으로 감시하는 방법이다.

댓글 남기기