부제 : DNS, Nginx, Reverse Proxy, SSL/TLS 이해하기
바이브코딩 및 AI 를 활용할 때 개인적으로 항상
개념숙지 -> 구현순으로 진행합니다. 개념숙지가 안된상태로 구현을 하게 되면 찝찝함이 많이 남는 편이라 그렇습니다. 아래 내용은 개념 숙지를 위해 관련된 내용을 AI에게 정리시킨 내용입니다.

백엔드 서버를 하나 만들었다고 해보자.
Node.js든 Fastify든 Spring이든 서버를 실행하면 보통 다음과 같은 형태로 동작한다.
http://localhost:3000
개발할 때는 이것만으로 충분하다.
하지만 실제 인터넷에서
https://mock-exchange.example.com
같은 주소로 서비스를 제공하려면 몇 가지 요소가 추가로 필요하다.
전체 구조부터 보면 생각보다 단순하다.
사용자
│
│ https://mock-exchange.example.com
▼
DNS
│
│ "이 도메인의 서버 IP는 1.2.3.4입니다"
▼
EC2
│
│ :443
▼
Nginx
│
│ http://127.0.0.1:3000
▼
내 백엔드 서버
여기에는 크게 네 가지 개념이 등장한다.
DNS → HTTP/HTTPS → SSL/TLS 인증서 → Nginx Reverse Proxy
하나씩 이해해보자.
HTTP는 클라이언트와 서버가 데이터를 주고받는 규칙이다.
예를 들어 브라우저나 다른 서버가 다음 요청을 보냈다고 하자.
GET /api/orderbook
Host: mock-exchange.example.com
서버에서는:
{
"bids": [],
"asks": []
}
같은 응답을 반환한다.
이게 기본적인 HTTP 통신이다.
구조를 단순화하면:
Client
│
│ HTTP Request
▼
Server
│
│ HTTP Response
▼
Client
HTTP의 기본 포트는 80이다.
따라서:
http://mock-exchange.example.com
은 기본적으로:
mock-exchange.example.com:80
으로 접속한다.
HTTP 자체에는 암호화가 없다.
예를 들어 클라이언트가 서버에 다음 데이터를 보낸다고 생각해보자.
{
"email": "user@example.com",
"password": "12345678"
}
HTTP만 사용한다면 네트워크를 통과하는 데이터가 암호화되지 않는다.
그래서 중간에서 트래픽을 관찰할 수 있는 공격자가 데이터를 읽거나 변조할 가능성이 생긴다.
이 문제를 해결하기 위해 HTTP에 TLS를 적용한 것이 HTTPS다.
TLS는 통신에 크게 세 가지 보안 속성을 제공한다.
HTTPS는 완전히 별개의 웹 프로토콜이라고 생각하기보다,
HTTP
+
TLS
라고 이해하면 쉽다.
즉:
HTTP
────────────────
평문 HTTP 통신
HTTPS
────────────────
HTTP
↓
TLS 암호화
↓
TCP
HTTPS의 기본 포트는 443이다.
그래서:
https://mock-exchange.example.com
이라고 하면 브라우저는 기본적으로:
mock-exchange.example.com:443
에 연결한다. (MDN Web Docs)
서버를 운영하다 보면 이런 표현을 매우 자주 본다.
SSL 인증서
SSL 설정
SSL certificate
HTTPS SSL
하지만 현대 웹에서 실제로 사용하는 기술은 TLS다.
SSL은 TLS의 전신이다.
대략:
SSL 2.0
↓
SSL 3.0
↓
TLS 1.0
↓
TLS 1.1
↓
TLS 1.2
↓
TLS 1.3
으로 발전했다.
현재 웹에서는 TLS 1.2와 TLS 1.3을 사용하며, SSL 및 오래된 TLS 버전은 사용하지 않는 것이 일반적이다. (MDN Web Docs)
그런데 관습적으로 아직도 사람들이:
SSL 인증서
라고 부른다.
따라서 실무에서는 대충:
SSL 인증서 ≒ TLS 인증서
라고 이해해도 된다.
여기서 중요한 문제가 하나 있다.
사용자가:
https://mock-exchange.example.com
에 접속했다고 해보자.
암호화를 하기 전에 먼저 확인해야 하는 것이 있다.
“내가 지금 통신하려는 서버가 정말 mock-exchange.example.com 서버가 맞나?”
공격자가 중간에서 자신을 진짜 서버인 것처럼 속이면 암호화 자체가 의미가 없어질 수 있기 때문이다.
그래서 서버는 자신의 신원을 증명하는 TLS Certificate를 제공한다.
인증서에는 해당 도메인과 공개키 등 서버의 신원을 검증하는 데 필요한 정보가 포함된다. 브라우저는 신뢰할 수 있는 인증기관(CA)이 발급한 인증서인지, 접속한 도메인과 인증서가 일치하는지 등을 확인한다. (MDN Web Docs)
CA는 인증서를 발급하고 신뢰를 보증하는 기관이다.
예를 들어:
사용자
│
│ "이 인증서 믿어도 돼?"
▼
브라우저
│
│ 신뢰하는 CA가 서명했는지 확인
▼
Certificate
│
▼
mock-exchange.example.com
대표적으로 Let’s Encrypt 같은 CA가 있다.
Let’s Encrypt는 무료 TLS 인증서를 제공하는 비영리 인증기관이다. (letsencrypt.org)
실제로 우리가 EC2를 구성할 때 사용할 수 있는 방법이:
Let's Encrypt
│
│ 인증서 발급
▼
Certbot
│
│ Nginx 설정
▼
Nginx
이다.
여기서 많이 헷갈리는 부분이다.
Let’s Encrypt와 Certbot은 같은 것이 아니다.
쉽게 구분하면:
Let's Encrypt
= 인증서를 발급하는 CA
Certbot
= 인증서 발급/설정/갱신을 자동화해주는 프로그램
예를 들어 서버에서:
sudo certbot --nginx -d mock-exchange.example.com
을 실행하면 Certbot이 Let’s Encrypt를 통해 인증서를 발급받고 Nginx에 적용하는 작업을 상당 부분 자동화할 수 있다.
결과적으로 Nginx가 다음과 같은 인증서 파일을 사용하게 된다.
/etc/letsencrypt/live/mock-exchange.example.com/
├── fullchain.pem
└── privkey.pem
개념적으로:
fullchain.pem
→ 클라이언트에게 보여줄 인증서 + 인증서 체인
privkey.pem
→ 서버의 Private Key
라고 보면 된다.
특히 privkey.pem은 절대 외부에 노출하면 안 된다.
사실 Node.js 서버 자체를 인터넷에 직접 노출할 수도 있다.
예를 들어 Node.js 서버가:
0.0.0.0:3000
으로 실행되고 EC2 Security Group에서 3000을 열어버리면:
http://1.2.3.4:3000
으로 직접 접근할 수 있다.
하지만 실제 서비스에서는 보통 이렇게 하지 않는다.
대신:
Internet
│
▼
Nginx
│
▼
Application
구조를 사용한다.
여기서 Nginx가 Reverse Proxy 역할을 한다.
Proxy라는 개념부터 보면 쉽다.
일반적인 Proxy는 중간에서 요청을 대신 전달하는 서버다.
예를 들어:
나
│
▼
Proxy
│
▼
Google
Google 입장에서는 요청을 Proxy가 보낸 것처럼 보일 수 있다.
이런 구조를 흔히 Forward Proxy라고 한다.
Reverse Proxy는 반대쪽에 있다.
사용자
│
▼
Reverse Proxy
│
▼
실제 서버
사용자는 실제 Application Server와 직접 통신하지 않는다.
Nginx와 통신한다.
그리고 Nginx가 요청을 실제 Application Server로 전달한다.
NGINX 공식 문서에서도 reverse proxy를 클라이언트 요청을 proxied server에 전달하고, 그 응답을 다시 클라이언트에게 전달하는 구조로 설명한다. (docs.nginx.com)
우리가 만들 구조가 정확히 이것이다.
예를 들어 EC2의 Public IP가:
3.35.100.100
이고 Node.js 서버가:
127.0.0.1:3000
에서 실행된다고 해보자.
우리가 원하는 것은:
https://mock-exchange.example.com
으로 요청하면 Node.js의:
http://127.0.0.1:3000
으로 전달되는 것이다.
전체 흐름은:
Internet
User
│
│ https://mock-exchange.example.com
│
▼
DNS
│
│ mock-exchange.example.com
│ ↓
│ 3.35.100.100
│
▼
EC2
│
│ :443
▼
┌─────────────────────┐
│ Nginx │
│ │
│ HTTPS / TLS 처리 │
│ Reverse Proxy │
└──────────┬──────────┘
│
│ HTTP
│ 127.0.0.1:3000
▼
┌─────────────────────┐
│ Node.js Server │
│ │
│ Fastify / Express │
└─────────────────────┘
이 구조가 이번 작업에서 가장 중요하다.
도메인은 결국 사람이 읽기 편한 이름이다.
컴퓨터가 실제 서버에 연결하려면 IP 주소가 필요하다.
그래서:
mock-exchange.example.com
을:
3.35.100.100
으로 변환해야 한다.
그 역할을 DNS가 한다.
도메인 관리 서비스에서 A Record를 다음처럼 설정할 수 있다.
Type Name Value
A mock-exchange 3.35.100.100
그러면:
mock-exchange.example.com
│
│ DNS Lookup
▼
3.35.100.100
이 된다.
중요한 것은 DNS는 요청을 Node.js로 전달하는 Reverse Proxy가 아니라는 것이다.
DNS가 하는 핵심 역할은:
Domain
↓
IP Address
를 알려주는 것이다.
따라서 전체 관계를 이렇게 기억하면 된다.
DNS
"어느 서버로 갈까?"
│
▼
EC2 Public IP
Nginx
"이 서버에 들어온 요청을
어느 Application으로 보낼까?"
│
▼
127.0.0.1:3000
Node.js
"실제 API 로직을 실행"
각자의 역할이 완전히 다르다.
Nginx에는 이런 설정이 들어간다.
server {
listen 80;
server_name mock-exchange.example.com;
}
여기서:
listen 80;
은
80번 포트로 들어오는 HTTP 연결을 받겠다.
는 의미다.
그리고:
server_name mock-exchange.example.com;
은
mock-exchange.example.com을 대상으로 들어온 요청은 이 server block에서 처리하겠다.
는 의미다.
이 덕분에 EC2 하나에서 여러 도메인을 운영할 수도 있다.
예를 들어:
EC2
│
│ 3.35.100.100
│
├── api.example.com
│
├── mock-exchange.example.com
│
└── admin.example.com
이 세 도메인이 전부 같은 IP를 가리켜도 Nginx가 요청의 Host를 보고 서로 다른 서버로 전달할 수 있다.
api.example.com
│
▼
127.0.0.1:3000
mock-exchange.example.com
│
▼
127.0.0.1:4000
admin.example.com
│
▼
127.0.0.1:5000
이것이 기존 EC2에 이미 Nginx가 있는데도 새로운 서비스를 추가할 수 있는 이유다.
Nginx Reverse Proxy 설정에서 가장 중요한 부분은:
location / {
proxy_pass http://127.0.0.1:3000;
}
이다.
의미는 간단하다.
Nginx로 들어온
/
요청을
127.0.0.1:3000
으로 전달한다.
예를 들어:
GET https://mock-exchange.example.com/api/orderbook
요청이 들어왔다고 하자.
Nginx가 받아서 내부적으로:
GET http://127.0.0.1:3000/api/orderbook
으로 전달한다.
그리고 Node.js 응답을 다시 사용자에게 전달한다. proxy_pass는 NGINX에서 이런 upstream 전달을 지정하는 핵심 directive다. (nginx.org)
따라서 사용자는 Node.js가 3000 포트를 사용하는지 알 필요도 없다.
여기에는 보안상의 장점도 있다.
Node.js가:
0.0.0.0:3000
에서 실행되고 Security Group도 3000을 열어놓으면 인터넷에서:
EC2:3000
으로 직접 접근할 가능성이 생긴다.
그러면:
User ───────────────→ Node.js :3000
이라는 Nginx를 우회하는 경로가 존재한다.
반대로 Node.js를:
127.0.0.1:3000
으로만 listen하게 하면 외부에서 직접 접근할 수 없다.
Internet
│
X
127.0.0.1:3000
같은 EC2 내부의 Nginx만 접근할 수 있다.
Internet
│
▼
Nginx
│
▼
127.0.0.1:3000
따라서 Security Group도 일반적으로:
80 → Public
443 → Public
3000 → Public에 열지 않음
으로 구성할 수 있다.
여기서 아주 중요한 개념이 하나 나온다.
우리 구조에서는 Node.js가 HTTPS를 직접 처리하지 않는다.
Nginx가 처리한다.
User
│
│ HTTPS
│ encrypted
▼
Nginx
│
│ HTTP
│ localhost
▼
Node.js
즉 TLS 연결은:
User ↔ Nginx
사이에서 끝난다.
이를 흔히 TLS Termination이라고 한다.
그래서 Node.js 코드는 그냥:
app.listen({
port: 3000,
host: "127.0.0.1"
});
처럼 HTTP 서버로 실행해도 된다.
Nginx가:
HTTPS
↓
decrypt
↓
HTTP
↓
Node.js
를 담당한다.
개념적으로 HTTPS용 Nginx 설정은 다음과 같다.
server {
listen 443 ssl;
server_name mock-exchange.example.com;
ssl_certificate /etc/letsencrypt/live/mock-exchange.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mock-exchange.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
여기서:
listen 443 ssl;
은 HTTPS 연결을 받겠다는 뜻이다.
그리고:
ssl_certificate
와:
ssl_certificate_key
가 TLS 연결에 필요한 인증서와 Private Key를 지정한다.
다음처럼 구성할 수도 있다.
HTTP :80
HTTPS :443
하지만 일반적인 웹 서비스에서는 HTTP 요청을 그대로 서비스하기보다 HTTPS로 보내버린다.
예:
server {
listen 80;
server_name mock-exchange.example.com;
return 301 https://$host$request_uri;
}
그러면 사용자가:
http://mock-exchange.example.com/api/orderbook
으로 접속해도 Nginx가:
301 Redirect
를 반환해서:
https://mock-exchange.example.com/api/orderbook
으로 이동시킨다.
HTTPS 사이트가 HTTP 요청을 받아 HTTPS로 redirect하는 것은 일반적인 구성이다. (MDN Web Docs)
따라서 최종 구조는:
HTTP :80
│
│ 301 Redirect
▼
HTTPS :443
│
│ TLS
▼
Nginx
│
│ Reverse Proxy
▼
Node.js :3000
이 된다.
실제 설정에서는 보통 다음도 넣는다.
location / {
proxy_pass http://127.0.0.1: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_set_header X-Forwarded-Proto $scheme;
}
왜 필요할까?
Reverse Proxy가 있기 때문이다.
원래:
Client
↓
Node.js
였다면 Node.js가 직접 클라이언트 정보를 알 수 있다.
하지만 지금은:
Client
↓
Nginx
↓
Node.js
이다.
Node.js 입장에서는 연결한 상대가 Nginx다.
그래서 Nginx가 원래 요청 정보를 Header에 담아 전달한다. NGINX 공식 문서에서도 proxy_set_header를 이용해 proxied server로 전달하는 Host 등의 헤더를 변경하도록 지원한다. (docs.nginx.com)
대표적으로:
Host
→ 사용자가 요청한 도메인
X-Real-IP
→ 실제 Client IP
X-Forwarded-For
→ Proxy를 거친 Client IP 정보
X-Forwarded-Proto
→ 원래 HTTP인지 HTTPS인지
를 전달한다.
여기까지 이해하면 Security Group도 연결된다.
EC2 앞에는 AWS Security Group이라는 방화벽이 있다.
Internet
│
▼
┌─────────────────────┐
│ Security Group │
│ │
│ 80 → Allow │
│ 443 → Allow │
│ 3000 → Deny │
└──────────┬──────────┘
│
▼
EC2
│
▼
Nginx
│
▼
Node.js
따라서 이번 서버에서는 보통 외부에:
TCP 80
TCP 443
을 허용하면 된다.
애플리케이션 포트:
3000
은 외부에 열 필요가 없다.
사용자가 브라우저에:
https://mock-exchange.example.com/api/orderbook
을 입력했다고 하자.
브라우저가 묻는다.
mock-exchange.example.com의 IP가 뭐야?
DNS가:
3.35.100.100
이라고 알려준다.
HTTPS이므로:
3.35.100.100:443
으로 연결한다.
Nginx가 인증서를 제공한다.
Certificate
Domain:
mock-exchange.example.com
Issuer:
Let's Encrypt
브라우저가 인증서를 검증한다.
TLS handshake 과정에서는 TLS 버전과 암호화 방식 등을 협상하고, 이후 실제 HTTP 데이터가 암호화되어 전달된다. (MDN Web Docs)
암호화된 연결이 만들어지면:
GET /api/orderbook
Host: mock-exchange.example.com
요청을 보낸다.
Nginx는:
server_name mock-exchange.example.com;
과 일치하는 server block을 찾는다.
그리고:
proxy_pass http://127.0.0.1:3000;
을 확인한다.
Nginx가 내부적으로:
http://127.0.0.1:3000/api/orderbook
으로 요청한다.
Node.js가:
{
"bids": [],
"asks": []
}
을 반환한다.
다시:
Node.js
│
▼
Nginx
│
│ TLS encryption
▼
Internet
│
▼
User
순서로 응답이 돌아간다.
이번에 우리가 만들 시스템을 전부 합치면 다음과 같다.
┌─────────────────┐
│ DNS │
│ │
│ mock-exchange │
│ ↓ │
│ EC2 Public IP │
└────────┬────────┘
│
▼
Internet
│
│
HTTPS TCP :443
│
▼
┌─────────────────────────┐
│ AWS Security Group │
│ │
│ :80 ALLOW │
│ :443 ALLOW │
│ :3000 BLOCK │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Nginx │
│ │
│ TLS Certificate │
│ TLS Termination │
│ Reverse Proxy │
│ │
│ mock-exchange... │
│ ↓ │
│ 127.0.0.1:3000 │
└────────────┬────────────┘
│
│ HTTP
│
▼
┌─────────────────────────┐
│ Application │
│ │
│ Node.js / Fastify │
│ │
│ 127.0.0.1:3000 │
└─────────────────────────┘
이론을 실제 작업으로 바꾸면 순서는 다음과 같다.
① EC2 준비
↓
② Node.js 서버 실행
↓
③ localhost에서 서버 정상 동작 확인
↓
④ Nginx 설치/기존 설정 확인
↓
⑤ mock-exchange용 server block 추가
↓
⑥ Nginx → Node.js Reverse Proxy
↓
⑦ DNS A Record → EC2 IP
↓
⑧ HTTP 접속 확인
↓
⑨ Let's Encrypt 인증서 발급
↓
⑩ Nginx HTTPS 설정
↓
⑪ HTTP → HTTPS redirect
↓
⑫ HTTPS 접속 확인
이 순서가 중요한 이유는 문제 발생 시 원인을 한 단계씩 분리할 수 있기 때문이다.
이 구조를 이해하면 장애 원인을 찾기도 쉬워진다.
예를 들어:
https://mock-exchange.example.com
이 안 열린다고 하자.
무작정 Nginx를 수정할 필요가 없다.
먼저:
curl http://127.0.0.1:3000
을 실행한다.
이것부터 실패 : Node.js 문제
다음:
curl http://localhost
또는 Host를 지정해서 Nginx를 테스트한다.
여기서 실패한다면: Nginx 문제
다음:
dig mock-exchange.example.com
결과가 EC2 IP가 아니면: DNS 문제
443 연결 자체가 안 된다면:
Security Group
Firewall
Nginx listen
등을 확인한다.
인증서 오류라면:
TLS Certificate
Domain
Certbot
Nginx SSL configuration
을 확인한다.
즉 구조를 알고 있으면:
"사이트가 안 돼요"
가 아니라:
DNS?
↓
Network?
↓
Nginx?
↓
TLS?
↓
Application?
순서로 문제를 좁힐 수 있다.
DNS
Domain → IP
Nginx
Request → Application
HTTP → 80
HTTPS → 443
현재 실제 사용하는 것은 TLS지만 여전히 관습적으로 “SSL 인증서”라는 표현을 많이 사용한다.
SSL Certificate
≈ TLS Certificate
라고 이해하면 된다.
Let's Encrypt
→ 인증서를 발급하는 CA
Certbot
→ 인증서 발급/갱신/설정을 자동화하는 Client
Nginx
→ 외부 요청 접수
→ HTTPS 처리
→ Reverse Proxy
Node.js
→ 실제 Business Logic/API 처리
Public IP
→ 실제 서버 주소
Domain
→ 사람이 사용하기 쉬운 이름
DNS
→ Domain과 IP를 연결
모든 내용을 암기할 필요는 없다.
아래 그림이 머릿속에 있으면 된다.
DNS
│
mock-exchange.example.com
│
▼
EC2 Public IP
│
│
┌─────────┴─────────┐
│ │
HTTP HTTPS
:80 :443
│ │
│ TLS Certificate
│ │
└───────► Nginx ◄───┘
│
│ Reverse Proxy
│
▼
127.0.0.1:3000
│
▼
Node.js API
그리고 각각 한 문장으로 설명할 수 있으면 충분하다.
DNS :
도메인을 서버의 IP 주소와 연결한다.
HTTP :
클라이언트와 서버가 웹 데이터를 주고받는 프로토콜이다.
HTTPS :
HTTP 통신을 TLS로 보호한 것이다.
TLS :
통신을 암호화하고 무결성을 보호하며 서버의 신원을 인증한다.
TLS Certificate :
해당 서버가 특정 도메인의 서버임을 검증하는 데 사용되는 디지털 인증서다.
Let’s Encrypt :
무료 TLS 인증서를 발급해주는 CA다.
Certbot :
Let’s Encrypt 등의 인증서 발급과 갱신, 웹 서버 설정을 자동화하는 데 사용하는 도구다.
Nginx :
웹 서버이면서 Reverse Proxy 등 다양한 역할을 할 수 있는 소프트웨어다.
Reverse Proxy :
외부 요청을 대신 받아 내부 Application Server로 전달하는 구조다.
TLS Termination :
HTTPS/TLS 처리를 Nginx에서 끝내고 내부 Application에는 HTTP로 전달하는 구조다.
Security Group :
EC2 앞에서 어떤 네트워크 트래픽을 허용할지 제어하는 AWS의 방화벽이다.
이번에 구축하려는 서버를 한 문장으로 표현하면 다음과 같다.
DNS로 mock-exchange.example.com을 EC2에 연결하고, Nginx가 80/443 포트의 HTTP/HTTPS 요청을 받은 뒤 TLS를 처리하고, Reverse Proxy를 통해 localhost에서 실행 중인 자체 서버로 요청을 전달한다.
그래서 실제 애플리케이션은 인터넷이나 TLS 인증서를 직접 신경 쓰지 않고:
127.0.0.1:3000
에서 자신의 API만 제공하면 된다.
외부 세계와 Application 사이의 경계를 Nginx가 담당하는 것이다.
External
─────────────────────────────────
DNS
↓
AWS Security Group
↓
Nginx
├─ HTTP
├─ HTTPS
├─ TLS Certificate
└─ Reverse Proxy
─────────────────────────────────
Internal
↓
127.0.0.1:3000
↓
Application
이 그림을 이해했다면, 이제 실제 Nginx 설정에서 listen, server_name, location, proxy_pass, ssl_certificate가 왜 존재하는지도 자연스럽게 연결된다.