로컬에서 Spring Boot 애플리케이션을 잘 만들었어도, 그게 인터넷 어디에선가 다른 사람도 접속할 수 있는 서버 위에서 돌아가야 진짜 서비스가 된다. 이때 등장하는 게 EC2와 Nginx다.
EC2(Elastic Compute Cloud)는 AWS가 제공하는 가상 서버다. 물리적인 서버를 직접 사 와서 설치할 필요 없이, 필요한 사양의 컴퓨터를 클릭 몇 번으로 빌려서 인터넷에 연결된 서버로 쓸 수 있다.
Nginx는 그 서버 앞에서 들어오는 요청을 받아서 적절한 곳으로 전달해주는 웹 서버다. 정적 파일을 직접 서빙하거나, 우리 Spring Boot 애플리케이션으로 요청을 넘겨주는 리버스 프록시 역할을 한다.
둘을 합치면 이런 그림이 된다.
[인터넷] → [EC2 서버] → [Nginx] → [Spring Boot 애플리케이션 (Tomcat)]
서비스를 운영하려면 24시간 켜져 있는 컴퓨터가 필요하다. 직접 서버를 사서 데이터센터에 두는 건(On-Premise) 비용도 크고 관리도 복잡하다.
Spring Boot는 내장 Tomcat이 있어서 그 자체로도 HTTP 요청을 처리할 수 있다. 그런데도 앞에 Nginx를 두는 이유가 있다.
핵심은 Nginx가 "외부에 노출되는 창구"를 맡고, Spring Boot는 내부에서 비즈니스 로직만 처리하는 역할 분리다.
사용자가 https://myservice.com/api/users로 접속했을 때의 흐름을 보자.
[사용자 브라우저]
|
| https://myservice.com/api/users 요청
↓
[DNS]
|
| myservice.com → EC2의 고정 IP(Elastic IP)로 변환
↓
[AWS 보안그룹]
|
| 443(HTTPS), 80(HTTP) 포트만 외부에서 접근 허용
| 8080(Spring Boot)은 외부에서 직접 접근 불가 (내부 통신만 허용)
↓
[EC2 서버]
|
↓
[Nginx] (포트 443에서 대기 중)
|
| ① HTTPS 인증서로 SSL 처리 (TLS Handshake)
| ② 요청 경로 확인 → /api/* 는 백엔드로 전달하도록 설정되어 있음
| ③ proxy_pass로 내부 포트(8080)에 요청 전달
↓
[Spring Boot 애플리케이션 - 내장 Tomcat, 포트 8080]
|
| ④ DispatcherServlet → Controller → Service → DB 처리
↓
[Nginx]
|
| ⑤ 애플리케이션의 응답을 받아서 다시 클라이언트로 전달
↓
[사용자 브라우저]
JSON 응답 수신
사용자는 Nginx와만 직접 통신하고, Spring Boot 애플리케이션은 외부에 직접 노출되지 않는다. Nginx가 일종의 관문 역할을 하는 것이다.
실제로 실행되는 가상 서버 한 대를 의미한다.
CPU, 메모리, 스토리지 사양에 따라 인스턴스 타입(t3.micro, t3.medium 등)이 나뉜다.
서비스 초기에는 작은 인스턴스로 시작하고, 트래픽이 늘면 더 큰 사양으로 바꾸는 식으로 운영한다.
EC2 인스턴스에 대한 방화벽 역할을 한다.
어떤 포트를, 어떤 IP에서 오는 요청만 허용할지 정의한다.
일반적으로 80(HTTP), 443(HTTPS), 22(SSH) 정도만 외부에 열어두고,
애플리케이션 포트(8080)는 외부에서 직접 접근하지 못하게 막는다.
EC2 인스턴스를 재시작하면 기본 IP가 바뀔 수 있다. Elastic IP는 고정된 IP 주소로, 도메인을 이 IP에 연결해두면 인스턴스를 재시작해도 도메인 연결이 깨지지 않는다.
클라이언트의 요청을 받아서 내부 서버(애플리케이션)로 전달하고, 응답을 다시 클라이언트에게 돌려주는 역할이다. 일반적인 프록시는 클라이언트를 대신해서 외부로 요청을 보내지만, 리버스 프록시는 서버를 대신해서 클라이언트의 요청을 받아준다는 점에서 방향이 반대다.
백엔드 애플리케이션 서버가 여러 대 있을 때,
들어오는 요청을 여러 서버에 분산시켜주는 역할이다.
upstream 블록으로 여러 서버를 등록하고 라운드로빈 등의 방식으로 분배한다.
EC2 위에서 Spring Boot 애플리케이션과 Nginx가 어떻게 배치되는지 구조를 보자.
[EC2 인스턴스]
|
├── Nginx (시스템 서비스 또는 Docker 컨테이너)
│ - 포트 80, 443에서 대기
│ - SSL 인증서 보관
│ - /etc/nginx/sites-available/myservice.conf 설정 파일
│
└── Spring Boot 애플리케이션 (Docker 컨테이너 또는 JAR 직접 실행)
- 포트 8080에서 대기 (외부 비공개)
- 내장 Tomcat이 요청 처리
Nginx는 80/443으로 외부 요청을 받고, 내부적으로 8080에 있는 Spring Boot로 전달한다. 이 구조 덕분에 Spring Boot 애플리케이션 코드는 HTTPS나 외부 노출에 대해 전혀 신경 쓸 필요가 없다.
배포 흐름은 보통 이렇게 진행된다.
1. 로컬에서 빌드 (또는 CI에서 빌드) → JAR 또는 Docker 이미지 생성
2. EC2로 전송 (scp, Docker 레지스트리 pull 등)
3. EC2에서 애플리케이션 실행 (포트 8080)
4. Nginx는 이미 설정되어 있어서 별도 재설정 불필요
(단, 무중단 배포 시에는 Nginx 설정 reload가 필요할 수 있음)
# /etc/nginx/sites-available/myservice.conf
server {
listen 80;
server_name myservice.com;
# HTTP 요청을 HTTPS로 강제 리다이렉트
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name myservice.com;
# SSL 인증서 설정 (Let's Encrypt 등으로 발급)
ssl_certificate /etc/letsencrypt/live/myservice.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myservice.com/privkey.pem;
location / {
proxy_pass http://localhost:8080; # Spring Boot로 전달
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;
}
}
server {
listen 443 ssl;
server_name myservice.com;
# 정적 파일은 Nginx가 직접 처리 (Spring Boot로 안 넘김)
location /static/ {
alias /var/www/myservice/static/;
expires 30d;
}
# API 요청만 Spring Boot로 전달
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
}
}
upstream backend_servers {
server localhost:8080;
server localhost:8081;
server localhost:8082;
}
server {
listen 443 ssl;
server_name myservice.com;
location / {
proxy_pass http://backend_servers; # 세 서버에 분산
proxy_set_header Host $host;
}
}
# /etc/systemd/system/myapp.service
[Unit]
Description=My Spring Boot App
After=network.target
[Service]
User=ec2-user
ExecStart=/usr/bin/java -jar /home/ec2-user/app.jar
Restart=always
RestartSec=10
Environment="SPRING_PROFILES_ACTIVE=prod"
[Install]
WantedBy=multi-user.target
# 서비스 등록 및 시작
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
# 로그 확인
sudo journalctl -u myapp -f
# 설정 문법 오류 확인
sudo nginx -t
# 무중단으로 설정 다시 로드 (재시작 아님)
sudo systemctl reload nginx
EC2는 그 자체로 "가상 서버 한 대"다. 트래픽이 늘어도 자동으로 서버가 늘어나지 않는다. 자동 확장이 필요하면 Auto Scaling Group, 로드 밸런서(ALB) 같은 별도 구성이 필요하다. 초기 단계에서는 EC2 한 대 + Nginx로 시작하는 경우가 많지만, 이건 "확장 가능한 구조"가 아니라 "최소 구성"이라는 점을 알아야 한다.
이름이 헷갈리지만 "누구를 대신해서 동작하는가"로 구분하면 명확해진다. Nginx는 서버(우리 애플리케이션)를 대신해서 클라이언트 요청을 받아주는 것이다.
Tomcat은 Java 애플리케이션 코드를 실행하는 데 특화되어 있고, Nginx는 정적 파일 처리, SSL, 로드 밸런싱, 다중 도메인 처리에 특화되어 있다. 각자 잘하는 역할을 분리해서 맡기는 것이지, 같은 일을 중복으로 하는 게 아니다.
보안 그룹은 외부에서 들어오는 트래픽을 막는 것이다.
같은 EC2 인스턴스 안에서 localhost:8080으로 접근하는 건
보안 그룹과 무관하게 항상 가능하다.
Nginx가 proxy_pass http://localhost:8080으로 내부 접근하는 게 가능한 이유다.
필요한 포트만 정확히 열어야 한다.
일반적으로 80, 443은 모든 IP(0.0.0.0/0)에서 허용하고,
22(SSH)는 본인의 IP나 회사 VPN IP로 제한해야 한다.
8080처럼 애플리케이션이 직접 쓰는 포트는 외부에 열 필요가 없다.
# Certbot 설치 후 인증서 발급 및 Nginx 자동 설정
sudo certbot --nginx -d myservice.com
# 인증서는 90일마다 만료되므로 자동 갱신 설정 (보통 cron으로 등록됨)
sudo certbot renew --dry-run
새 버전을 다른 포트(예: 8081)로 띄운 뒤, 정상 동작을 확인하고
Nginx의 proxy_pass 대상을 바꿔서 트래픽을 전환하는 방식(Blue-Green 배포의 단순화 버전)을 쓸 수 있다.
# 기존: proxy_pass http://localhost:8080;
# 전환: proxy_pass http://localhost:8081;
# 변경 후 reload (재시작 없이 적용)
sudo systemctl reload nginx
Nginx의 액세스 로그(/var/log/nginx/access.log)와
애플리케이션 로그를 함께 보면 어느 요청이 어디서 느려지는지 추적할 수 있다.
실무에서는 CloudWatch 같은 모니터링 도구와 연계해서 EC2 자원 사용량, 애플리케이션 상태를 함께 관찰한다.
처음부터 큰 인스턴스를 쓸 필요는 없다. 작게 시작해서 CPU/메모리 사용률을 모니터링하며 필요에 따라 사이즈를 조정하는 것이 비용 효율적이다. AWS는 인스턴스 타입 변경이 비교적 쉬운 편이다.
reload로 무중단 설정 변경이 가능하다
로컬에서 localhost:8080으로만 보던 애플리케이션을
실제로 도메인을 연결해서 인터넷에 띄워보니까 신경 써야 할 게 생각보다 많았다.
보안그룹 설정 하나 놓쳐서 접속이 안 되거나, SSL 설정이 꼬여서 헤맨 적도 있었다.
가장 크게 느낀 건 Spring Boot 애플리케이션이 직접 인터넷과 마주하지 않는다는 점이다. Nginx가 앞에서 받아주고, 애플리케이션은 그 뒤에서 안전하게 자기 일만 한다. 이 역할 분리를 이해하고 나니 왜 포트를 숨기는지, 왜 SSL을 Nginx에서 처리하는지, 왜 정적 파일을 굳이 Nginx가 따로 처리하는지가 전부 하나의 그림으로 연결됐다.