새로 투입된 공공기관 프로젝트에서 배포 직전에 문제가 생겼습니다. 앱 서버에서 SMS 발송 API를 호출하는데 연결이 안 됩니다. 개발 환경에서는 잘 됐는데 운영 환경은 "망 분리 구성"이라고 합니다. "Proxy 서버를 통해서 연결해야 해요"라는 말을 들었지만 — 환경변수를 설정해도 Java 앱은 여전히 실패하고, curl은 된다고 하는데 이유를 모르겠습니다.
이 모듈에서는 폐쇄망 구조와 Proxy/NAT의 동작 원리를 이해하고, Squid Proxy 설정과 Java 포함 다양한 클라이언트에서 Proxy를 사용하는 방법을 다룹니다.
- 1Forward Proxy와 Reverse Proxy의 방향 차이를 구분하고 설명할 수 있다
- 2Squid Proxy의 기본 설정(http_port, acl, http_access)을 작성하고 동작을 확인할 수 있다
- 3curl, Java JVM, 시스템 환경변수 각각에서 Proxy를 설정하는 방법을 적용할 수 있다
- 4SNAT/DNAT 차이를 이해하고 iptables로 포트 포워딩을 설정할 수 있다
- 5NO_PROXY 설정으로 내부 통신이 Proxy를 우회하도록 구성할 수 있다
망 분리 환경의 통신 구조
폐쇄망에서 외부로 나가는 트래픽 흐름
보안 요구가 높은 공공기관이나 금융 환경에서는 내부 서버가 인터넷에 직접 접속할 수 없도록 망을 분리합니다. 이 구조에서 외부 API를 호출하려면 허가된 Proxy 서버를 통해야만 트래픽이 흘러나갈 수 있습니다. 내부망, DMZ, 인터넷 구간 사이의 흐름을 이해하지 못하면 방화벽 오픈 요청도, Proxy 설정도 어디서 막혔는지 진단조차 하기 어렵습니다.

공공기관 운영 서버에서 SMS API를 호출했더니 "연결 거부"가 납니다. 개발 서버에서는 됐는데 운영만 안 됩니다. 방화벽을 확인했더니 "직접 외부 통신은 차단, Proxy를 통해야 한다"고 합니다. 망 분리 환경을 처음 접하는 순간입니다. 어떤 경로로 트래픽이 흘러야 하는지를 먼저 이해해야 Proxy 설정도, 방화벽 오픈 요청도 제대로 할 수 있습니다.
망 분리는 보안 요구사항이 높은 환경(금융, 공공기관, 병원 등)에서 내부 업무망과 인터넷 구간을 물리적 또는 논리적으로 분리하는 구성입니다. 내부 서버는 인터넷에 직접 접속할 수 없고, 허가된 경로(Proxy 서버)를 통해서만 외부 통신이 가능합니다.
3계층 망 분리 구조:
[내부망] [DMZ] [인터넷/외부기관]
내부 WAS 서버 Proxy 서버 SMS API 서버
192.168.10.x ───► 10.10.1.100 ───► 203.x.x.x:443
(Squid 3128)
방화벽 규칙:
- 내부→DMZ Proxy: ALLOW 10.10.1.100:3128
- DMZ→외부: ALLOW 특정 IP:443만 허용
- 내부→외부: 직접 연결 DENY
Proxy 서버가 내부 서버 대신 외부 API를 호출합니다. 외부 API 서버 입장에서는 Proxy 서버의 IP만 보이므로 Proxy IP를 외부 기관에 IP 화이트리스트로 등록합니다.
Squid Proxy 설치와 설정
Squid — Forward Proxy의 대표 소프트웨어
폐쇄망 환경에서 내부 서버가 특정 외부 API에만 접근하도록 제어하고 감사 로그를 남겨야 할 때, 가장 빠르게 구축할 수 있는 선택이 Squid입니다. ACL(Access Control List)로 허용 목적지를 세밀하게 제한하고, 어떤 서버가 어느 URL을 호출했는지 실시간으로 추적할 수 있어 보안 감사 요구사항을 충족하기에 적합합니다. 오픈소스이면서도 운영 환경에서 충분히 검증된 소프트웨어로, 국내 공공·금융 프로젝트에서 표준적으로 사용됩니다.

Proxy 서버를 구축해달라는 요청을 받았습니다. 폐쇄망 내부 서버들이 특정 외부 기관 API에만 접근하도록 제어해야 하고, 어떤 서버가 어느 URL을 얼마나 호출하는지 감사 로그도 남겨야 합니다. 이 요구사항을 가장 빠르고 실용적으로 구현하는 도구가 Squid입니다.
Squid는 Linux에서 가장 널리 쓰이는 Forward Proxy 소프트웨어입니다. HTTP/HTTPS 요청을 대신 수행하고, ACL(Access Control List)로 어떤 클라이언트가 어떤 목적지에 접근할 수 있는지 제어합니다. 캐싱 기능도 있어 동일 외부 리소스 반복 요청 시 성능을 높입니다.
Squid 설치:
# RHEL/CentOS
sudo dnf install -y squid
# Ubuntu/Debian
sudo apt-get install -y squid
# 서비스 시작 및 자동 시작 등록
sudo systemctl enable --now squid
# 버전 및 상태 확인
squid -v
sudo systemctl status squid
기본 설정 파일 (/etc/squid/squid.conf):
# 핵심 설정 항목만 추출
grep -v '^#' /etc/squid/squid.conf | grep -v '^$'
운영 환경에 맞는 squid.conf 설정:
# /etc/squid/squid.conf
# ── Proxy 수신 포트 ────────────────────────────────
http_port 3128
# ── ACL 정의 ──────────────────────────────────────
# 내부 네트워크 대역 정의
acl localnet src 192.168.0.0/16
acl localnet src 10.0.0.0/8
acl localnet src 172.16.0.0/12
# 허용할 목적지 도메인/IP (외부 기관 API 서버)
acl allowed_destinations dstdomain .sms-gateway.example.com
acl allowed_destinations dstdomain .cert-authority.go.kr
acl allowed_destinations dst 203.x.x.x/32 # 특정 IP만 허용 시
# HTTP/HTTPS 포트
acl SSL_ports port 443
acl Safe_ports port 80
acl Safe_ports port 443
# ── 접근 제어 규칙 ────────────────────────────────
# 위에서 아래로 순서대로 평가됨 (첫 매칭에서 결정)
http_access deny !Safe_ports
http_access allow localnet allowed_destinations
http_access deny all # 나머지는 모두 차단
# ── HTTPS CONNECT 터널 허용 ───────────────────────
# HTTPS 요청 시 CONNECT 메서드로 터널 요청이 옴
http_access allow CONNECT SSL_ports
# ── 로그 ──────────────────────────────────────────
access_log /var/log/squid/access.log squid
cache_log /var/log/squid/cache.log
# 캐시 비활성화 (API 서버 연동 시 캐시 불필요한 경우)
no_cache deny all
# Proxy 헤더에 내부 IP 노출 방지
forwarded_for delete
via off
# 설정 파일 문법 검증
sudo squid -k parse
# 설정 재로드 (서비스 중단 없이)
sudo squid -k reconfigure
# 또는
sudo systemctl reload squid
클라이언트별 Proxy 설정
curl, Java, Python — 각각 다르게 설정한다
export http_proxy=...를 설정했더니 curl은 Proxy를 타는데 Java 앱은 여전히 실패합니다. 환경변수를 설정하면 다 되는 줄 알았는데 Java JVM은 다르게 동작합니다. 같은 서버에서도 클라이언트 종류마다 Proxy 설정 방법이 다르다는 것을 모르면 이 문제를 몇 시간 동안 헤맬 수 있습니다.
Proxy를 설정하는 방법은 클라이언트마다 다릅니다. OS 환경변수를 자동으로 읽는 것도 있고, 별도 설정이 필요한 것도 있습니다. 특히 Java JVM은 OS 환경변수를 무시하는 경우가 많아 별도 처리가 필요합니다.
1. 환경변수 방식 (curl, wget, Python requests 자동 적용):
# HTTP/HTTPS Proxy 설정
export http_proxy=http://10.10.1.100:3128
export https_proxy=http://10.10.1.100:3128
export HTTP_PROXY=http://10.10.1.100:3128 # 대문자도 설정 (앱마다 다름)
export HTTPS_PROXY=http://10.10.1.100:3128
# 내부 통신은 Proxy 우회 (매우 중요)
export no_proxy=localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,.internal.company.com
export NO_PROXY=$no_proxy
# 영구 적용 (시스템 전체)
echo 'http_proxy=http://10.10.1.100:3128' | sudo tee -a /etc/environment
echo 'https_proxy=http://10.10.1.100:3128' | sudo tee -a /etc/environment
echo 'no_proxy=localhost,127.0.0.1,10.0.0.0/8' | sudo tee -a /etc/environment
2. curl — 명시적 Proxy 옵션:
# -x 옵션으로 Proxy 지정 (환경변수 무관하게 동작)
curl -x http://10.10.1.100:3128 -v https://sms-gateway.example.com/health
# Proxy 인증이 있는 경우
curl -x http://10.10.1.100:3128 -U proxyuser:proxypass https://api.example.com/
# Proxy 경유 시 연결 과정 상세 확인
curl -x http://10.10.1.100:3128 --trace-ascii /dev/stderr https://api.example.com/ 2>&1 | head -30
3. Java JVM — 환경변수가 아닌 시스템 프로퍼티로 설정:
# WAS 기동 스크립트에 JVM 옵션 추가
JAVA_OPTS="$JAVA_OPTS \
-Dhttp.proxyHost=10.10.1.100 \
-Dhttp.proxyPort=3128 \
-Dhttps.proxyHost=10.10.1.100 \
-Dhttps.proxyPort=3128 \
-Dhttp.nonProxyHosts=localhost|127.0.0.1|10.*|*.internal.company.com"
# Tomcat의 경우 setenv.sh에 추가
# /opt/tomcat/bin/setenv.sh
export CATALINA_OPTS="$CATALINA_OPTS -Dhttp.proxyHost=10.10.1.100 -Dhttp.proxyPort=3128"
# Spring Boot 내장 서버의 경우
java -Dhttp.proxyHost=10.10.1.100 -Dhttp.proxyPort=3128 -jar app.jar
4. Python requests — 코드 또는 환경변수:
import requests
# 코드에서 직접 설정
proxies = {
'http': 'http://10.10.1.100:3128',
'https': 'http://10.10.1.100:3128',
}
response = requests.get('https://api.example.com/', proxies=proxies, timeout=10)
# 또는 환경변수 http_proxy, https_proxy 설정 시 자동 적용
NAT — 주소 변환과 포트 포워딩
SNAT/DNAT와 iptables 포트 포워딩
사설 IP를 쓰는 내부 서버가 인터넷과 통신하는 원리가 궁금할 때, 또는 외부에서 특정 포트로 들어오는 트래픽을 내부 서버로 보내야 할 때 NAT를 만납니다. "왜 내부 서버가 외부 API를 호출할 수 있는 거지?" 또는 "공인 IP 8080으로 들어오는 요청을 내부 WAS로 전달하려면 어떻게 하지?" — 이 두 가지 질문이 SNAT와 DNAT입니다.
NAT(Network Address Translation)는 IP 패킷의 주소를 변환하는 기술입니다. 사설 IP를 쓰는 내부 서버가 인터넷과 통신할 때(SNAT), 또는 외부에서 들어오는 트래픽을 내부 특정 서버로 포워딩할 때(DNAT) 사용합니다.
SNAT — 내부 → 외부 (출발지 변환):
# 내부 서버(192.168.10.0/24)가 eth0(공인 IP)을 통해 외부로 나갈 때
# 출발지 IP를 eth0 IP로 변환 (MASQUERADE: 동적 IP에 적합)
sudo iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -o eth0 -j MASQUERADE
# 정적 공인 IP가 있는 경우 SNAT 명시
sudo iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -o eth0 -j SNAT --to-source 203.x.x.x
# IP 포워딩 활성화 (라우터/NAT 서버에서 필수)
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
# 영구 적용
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-ip-forward.conf
sudo sysctl -p
DNAT — 외부 → 내부 포트 포워딩 (목적지 변환):
# 외부에서 이 서버의 8080 포트로 들어오는 트래픽을 내부 WAS로 포워딩
sudo iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.10.11:8080
# 특정 IP에서 오는 트래픽만 포워딩
sudo iptables -t nat -A PREROUTING -s 203.x.x.x -p tcp --dport 443 -j DNAT --to-destination 192.168.10.11:443
# 현재 NAT 규칙 확인
sudo iptables -t nat -L -n -v
# 규칙 영구 저장 (RHEL/CentOS)
sudo service iptables save
# Ubuntu
sudo netfilter-persistent save
실습 — Proxy 경유 외부 통신 확인
Proxy 환경변수를 설정하고 curl로 외부 API에 접속합니다. -v 옵션으로 Proxy와의 CONNECT 터널 수립 과정을 확인할 수 있습니다.
# Proxy 환경변수 설정
export http_proxy=http://10.10.1.100:3128
export https_proxy=http://10.10.1.100:3128
export no_proxy=localhost,127.0.0.1,10.0.0.0/8
# Proxy 경유 접속 테스트
curl -v https://external-api.example.com/health
# CONNECT 터널 수립 확인 (HTTPS 요청 시)
# 출력에서 다음 라인이 보여야 정상:
# * Connected to 10.10.1.100 (10.10.1.100) port 3128 (#0)
# * Establish HTTP proxy tunnel to external-api.example.com:443
# * Proxy replied 200 to CONNECT request
# Proxy 없이 직접 접속 시도 (차단되어야 정상)
unset http_proxy https_proxy
curl -m 5 https://external-api.example.com/health
# Expected: curl: (28) Connection timed out
export http_proxy=http://proxy-server:3128 && curl -v https://external-api.example.com/health- curl -v 출력에 'Connected to 10.10.1.100 port 3128'이 보이는가 (Proxy 경유 확인)
- HTTPS 요청 시 'Proxy replied 200 to CONNECT request'가 나오는가
- 최종 응답이 200 OK인가
- Squid access.log에 해당 요청이 기록됐는가 (tail -f /var/log/squid/access.log)
Squid 로그에서 각 요청의 상태를 확인합니다. TCP_MISS는 캐시 미스(실제 외부 요청 수행), TCP_DENIED는 ACL에 의해 차단된 요청입니다.
# Squid 액세스 로그 실시간 확인
tail -f /var/log/squid/access.log
# 로그 형식:
# 타임스탬프 응답시간 클라이언트IP 결과코드/HTTP상태 바이트 메서드 URL 계층정보
# 1748556000.123 342 192.168.10.11 TCP_MISS/200 1234 CONNECT external-api.example.com:443 - DIRECT/203.x.x.x -
# 차단된 요청 필터링
grep TCP_DENIED /var/log/squid/access.log | tail -20
# 특정 클라이언트 요청 추적
grep 192.168.10.11 /var/log/squid/access.log | tail -20
tail -f /var/log/squid/access.log- Squid 로그에 TCP_MISS/200 항목이 생겼는가 (Proxy가 외부 요청 성공)
- 차단된 목적지로의 요청에 TCP_DENIED가 찍히는가
- 로그에서 클라이언트 IP, 목적지 URL, 응답 코드가 모두 확인되는가
- no_proxy에 지정한 내부 IP는 Squid 로그에 남지 않는가
트러블슈팅
원인: Java JVM은 OS 수준의 http_proxy 환경변수를 자동으로 읽지 않습니다. export http_proxy=...는 Shell 환경변수로 curl 같은 일반 프로그램에는 적용되지만, Java 프로세스는 JVM 시스템 프로퍼티(-Dhttp.proxyHost 등)로 Proxy를 설정해야 합니다.
# Java 앱의 실제 JVM 옵션 확인
ps aux | grep java | grep -v grep
# 출력에 -Dhttp.proxyHost 가 없으면 Proxy 미설정 상태
# 확인: curl은 되는가?
curl -x http://10.10.1.100:3128 https://external-api.example.com/health
# → 200 OK이면 Proxy 자체는 정상, Java 설정 문제
# 해결: JVM 옵션 추가
# Tomcat setenv.sh 또는 systemd unit의 Environment= 에 추가
JAVA_OPTS="$JAVA_OPTS \
-Dhttp.proxyHost=10.10.1.100 \
-Dhttp.proxyPort=3128 \
-Dhttps.proxyHost=10.10.1.100 \
-Dhttps.proxyPort=3128 \
-Dhttp.nonProxyHosts=localhost|127.0.0.1|10.*"
# WAS 재시작 후 확인
sudo systemctl restart tomcat
# 앱 로그에서 외부 API 연결 성공 여부 확인
원인: http_proxy를 설정했지만 no_proxy(또는 NO_PROXY)를 설정하지 않아 내부 서버(DB, 캐시 서버, 다른 WAS)로의 요청도 Proxy를 경유합니다. 내부 트래픽이 Proxy→내부 서버로 우회하면서 불필요한 지연이 발생합니다.
# 현재 Proxy 관련 환경변수 확인
env | grep -iE "proxy|no_proxy"
# no_proxy가 없거나 내부 대역이 누락된 경우
# 내부 통신 대역 전체를 no_proxy에 추가
export no_proxy=localhost,127.0.0.1,::1,10.0.0.0/8,192.168.0.0/16,172.16.0.0/12,.internal.company.com
export NO_PROXY=$no_proxy
# Java의 경우 nonProxyHosts (파이프로 구분, CIDR 미지원)
-Dhttp.nonProxyHosts="localhost|127.0.0.1|10.*|192.168.*|*.internal.company.com"
# 확인: 내부 서버로 curl 시 Proxy 미경유 확인
curl -v http://192.168.10.11:8080/health 2>&1 | grep "Connected to"
# 기대 출력: * Connected to 192.168.10.11 (192.168.10.11) port 8080
# (Proxy IP가 아닌 실제 서버 IP로 연결)
실제 업무에서 이 지식이 쓰이는 상황:
공공기관이나 금융 프로젝트에서 배포 직전 또는 운영 중에 외부 연계 오류가 발생하는 경우가 많습니다. 대부분은 Proxy 설정 누락이나 방화벽 미오픈 문제입니다.
외부 API 연계 불가 시 점검 순서:
# 1단계: 네트워크 경로 확인
ping -c 3 external-api.example.com # 내부 → 외부 ICMP (보통 차단)
nc -zv external-api.example.com 443 # TCP 포트 열렸는지
# 2단계: Proxy 경유 테스트
curl -x http://10.10.1.100:3128 -v https://external-api.example.com/health
# 3단계: Squid 로그에서 차단 여부 확인
grep external-api.example.com /var/log/squid/access.log | tail -5
# 4단계: Java 앱 JVM 옵션 확인
ps aux | grep java | tr ' ' '\n' | grep proxy
Proxy Chain — Proxy를 통해 또 다른 Proxy로:
# 내부 Proxy → 상위 Proxy → 인터넷 구조가 필요한 경우
# /etc/squid/squid.conf
cache_peer 10.10.0.1 parent 3128 0 no-query default
never_direct allow all
새로운 외부 연계 서비스가 추가될 때마다 Squid ACL에 목적지를 추가하고, Java 앱의 nonProxyHosts에서 내부 서버 대역이 빠지지 않도록 관리하는 것이 Proxy 운영의 핵심입니다. 다음 모듈에서는 API Gateway의 역할과 Webhook을 통한 이벤트 기반 외부 연계 구조를 다룹니다.