기업 Java 애플리케이션은 외부 API에 정기적으로 접근하거나 데이터를 파싱하거나 격리된 기업 네트워크에서 작업합니다. 이러한 경우에는 언젠가 트래픽을 프록시 서버를 통해 라우팅해야 하는 문제에 직면하게 됩니다. Java에서 프록시 설정은 간단한 작업처럼 보이지만, RestTemplate, OkHttp 및 WebClient가 서로 다르게 작동하고 JVM의 시스템 속성이 모든 곳에서 작동하지 않는 경우에 직면하게 됩니다. 이 가이드에서는 구체적인 코드 예제, 숨겨진 문제 설명 및 다양한 작업에 맞는 프록시 유형 선택에 대한 권장 사항을 제공합니다.
Java 애플리케이션에 프록시가 필요한 이유
Java는 여전히 기업 개발의 주요 언어 중 하나입니다. Spring Boot 서비스, 마이크로서비스 아키텍처, 배치 작업, ETL 파이프라인 등은 모두 외부 세계에 HTTP 요청을 정기적으로 수행합니다. 이러한 모든 시나리오에서 프록시는 특정 작업을 해결합니다:
- 프록시 게이트웨이가 필수인 기업 네트워크. 많은 회사에서 모든 아웃바운드 트래픽은 기업 프록시(Squid, Zscaler, BlueCoat)를 통해 흐릅니다. 이를 설정하지 않으면 애플리케이션이 인터넷에 접속할 수 없습니다.
- 데이터 파싱 및 수집. 외부 사이트에서 데이터를 수집하는 서비스(가격, 인용, 뉴스)는 IP에 대한 제한 및 차단을 우회하기 위해 프록시를 사용합니다.
- 지리적 테스트. QA 팀은 API 또는 웹사이트가 다양한 지역에서 어떻게 작동하는지 확인합니다. 프록시는 필요한 국가에서의 요청을 에뮬레이션할 수 있게 해줍니다.
- 대상 API의 제한 우회. 일부 외부 API는 하나의 IP에서 요청 수에 제한을 두고 있습니다. 프록시 회전은 부하를 분산시킬 수 있습니다.
- 보안 및 익명화. 외부 서비스에 접근할 때 애플리케이션의 실제 IP를 숨기는 것은 민감한 통합을 위한 표준 관행입니다.
작업에 따라 Java에서 프록시 설정 접근 방식은 크게 다릅니다. JVM의 전역 시스템 속성은 가장 간단한 옵션이지만 일부 HTTP 클라이언트와는 작동하지 않습니다. OkHttp 및 WebClient는 명시적인 구성이 필요합니다. 각 경우를 차례로 살펴보겠습니다.
JVM 시스템 속성을 통한 전역 설정
Java 애플리케이션의 모든 HTTP 트래픽을 프록시를 통해 라우팅하는 가장 빠른 방법은 JVM의 시스템 속성을 사용하는 것입니다. 이는 표준 HttpURLConnection 및 이를 내부적으로 사용하는 많은 라이브러리에 대해 작동합니다.
프록시는 여러 방법으로 설정할 수 있습니다:
1. JVM 인수로 시작할 때:
java -Dhttp.proxyHost=proxy.example.com \
-Dhttp.proxyPort=8080 \
-Dhttps.proxyHost=proxy.example.com \
-Dhttps.proxyPort=8080 \
-Dhttp.nonProxyHosts="localhost|127.0.0.1|*.internal.corp" \
-jar myapp.jar
2. 애플리케이션 코드에서 (예: main 메서드나 @PostConstruct에서):
System.setProperty("http.proxyHost", "proxy.example.com");
System.setProperty("http.proxyPort", "8080");
System.setProperty("https.proxyHost", "proxy.example.com");
System.setProperty("https.proxyPort", "8080");
// 예외 — 프록시를 통과하지 않아야 하는 호스트
System.setProperty("http.nonProxyHosts", "localhost|127.0.0.1|*.internal.corp");
3. Spring Boot의 application.properties 파일을 통해 (그 후 @PostConstruct를 통해 적용):
# application.properties proxy.host=proxy.example.com proxy.port=8080 proxy.username=user proxy.password=secret
⚠️ 알아두어야 할 사항
JVM 시스템 속성은 HttpURLConnection에만 작동합니다. OkHttp, Apache HttpClient 및 Reactor Netty (WebClient)는 이러한 속성을 무시합니다. 이들은 아래에 설명된 명시적인 구성이 필요합니다.
프록시가 인증을 요구하는 경우 Authenticator를 사용하세요:
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (getRequestorType() == RequestorType.PROXY) {
return new PasswordAuthentication("username", "password".toCharArray());
}
return null;
}
});
Spring Boot의 프록시: RestTemplate 및 Apache HttpClient
RestTemplate은 Spring의 표준 동기 HTTP 클라이언트입니다. 기본적으로 SimpleClientHttpRequestFactory를 사용하며, 이는 HttpURLConnection에 의존하고 JVM의 시스템 속성을 읽습니다. 그러나 더 유연한 설정(타임아웃, 연결 풀, 인증이 있는 프록시)을 위해 Apache HttpClient를 백엔드로 사용하는 것이 좋습니다.
Maven 의존성:
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>
Apache HttpClient 5를 통한 프록시 설정이 포함된 RestTemplate 구성:
@Configuration
public class RestTemplateConfig {
@Value("${proxy.host}")
private String proxyHost;
@Value("${proxy.port}")
private int proxyPort;
@Value("${proxy.username:}")
private String proxyUsername;
@Value("${proxy.password:}")
private String proxyPassword;
@Bean
public RestTemplate restTemplate() {
HttpHost proxy = new HttpHost("http", proxyHost, proxyPort);
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
if (!proxyUsername.isEmpty()) {
credentialsProvider.setCredentials(
new AuthScope(proxyHost, proxyPort),
new UsernamePasswordCredentials(proxyUsername, proxyPassword.toCharArray())
);
}
CloseableHttpClient httpClient = HttpClients.custom()
.setProxy(proxy)
.setDefaultCredentialsProvider(credentialsProvider)
.build();
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory(httpClient);
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
return new RestTemplate(factory);
}
}
이러한 구성은 프록시 매개변수를 application.properties 또는 환경 변수로 외부화할 수 있게 해주며, 이는 서로 다른 프록시를 가진 prod/staging/dev 환경에 매우 중요합니다.
일부 요청은 프록시를 통해 전송하고 다른 요청은 직접 전송해야 하는 경우, 서로 다른 이름의 두 개의 별도 RestTemplate 빈을 생성하고 @Qualifier를 사용하여 필요한 것을 주입합니다.
Spring WebClient의 프록시 (반응형 스택)
WebClient는 Reactor Netty 기반의 반응형 HTTP 클라이언트입니다. 이는 프록시를 위해 JVM의 시스템 속성을 읽지 않으며, 클라이언트를 생성할 때 ProxyProvider를 통해 설정합니다.
의존성 (spring-boot-starter-webflux에 포함):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
HTTP 프록시가 포함된 WebClient 구성:
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
HttpClient httpClient = HttpClient.create()
.proxy(proxy -> proxy
.type(ProxyProvider.Proxy.HTTP)
.host("proxy.example.com")
.port(8080)
.username("user")
.password(user -> "password")
);
ClientHttpConnector connector =
new ReactorClientHttpConnector(httpClient);
return WebClient.builder()
.clientConnector(connector)
.build();
}
}
WebClient에서 SOCKS5 프록시를 위한:
HttpClient httpClient = HttpClient.create()
.proxy(proxy -> proxy
.type(ProxyProvider.Proxy.SOCKS5)
.host("socks5.proxy.example.com")
.port(1080)
.username("user")
.password(user -> "password")
);
💡 구성 팁
반응형 애플리케이션에서는 종종 서로 다른 외부 서비스에 대해 서로 다른 프록시를 가진 여러 개의 WebClient 인스턴스를 생성해야 합니다. WebClient.Builder를 프로토타입 빈으로 사용하고 .mutate()를 통해 설정을 재정의하세요.
OkHttp에서 프록시 설정
OkHttp는 많은 Java 및 Android 애플리케이션에서 사용되는 인기 있는 HTTP 라이브러리이며, 일부 Spring 통합(예: OkHttp 백엔드를 사용하는 Feign)에서도 사용됩니다. 이 라이브러리는 자체 프록시 설정 메커니즘을 가지고 있으며, JVM의 시스템 속성을 읽지 않습니다.
OkHttp에서 HTTP/HTTPS 프록시의 기본 설정:
import okhttp3.*;
import java.net.InetSocketAddress;
import java.net.Proxy;
// 프록시 생성
Proxy proxy = new Proxy(
Proxy.Type.HTTP,
new InetSocketAddress("proxy.example.com", 8080)
);
// 프록시 인증
Authenticator proxyAuthenticator = (route, response) -> {
String credential = Credentials.basic("username", "password");
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
};
// 클라이언트 구성
OkHttpClient client = new OkHttpClient.Builder()
.proxy(proxy)
.proxyAuthenticator(proxyAuthenticator)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
// 요청 예제
Request request = new Request.Builder()
.url("https://api.example.com/data")
.build();
try (Response response = client.newCall(request).execute()) {
System.out.println(response.body().string());
}
Spring Boot와의 OkHttp 통합을 위한 빈:
@Configuration
@ConditionalOnProperty(name = "proxy.enabled", havingValue = "true")
public class OkHttpConfig {
@Value("${proxy.host}")
private String proxyHost;
@Value("${proxy.port}")
private int proxyPort;
@Bean
public OkHttpClient okHttpClient() {
Proxy proxy = new Proxy(
Proxy.Type.HTTP,
new InetSocketAddress(proxyHost, proxyPort)
);
return new OkHttpClient.Builder()
.proxy(proxy)
.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
.build();
}
}
@ConditionalOnProperty에 유의하세요. 이는 코드 변경 없이 구성에 따라 필요한 환경에서만 프록시를 활성화할 수 있게 해줍니다.
OkHttp를 주거용 프록시와 함께 데이터 파싱에 사용하는 경우, 연결 풀을 올바르게 설정하는 것이 중요합니다. 주거용 프록시는 데이터 센터 프록시보다 대개 더 높은 대기 시간을 가지므로, 타임아웃을 늘리고 풀 크기를 줄여 사용 가능한 연결을 소진하지 않도록 하세요.
Java의 SOCKS5 프록시: 특징 및 예제
SOCKS5는 HTTP 프록시보다 더 낮은 수준의 프로토콜입니다. 이는 전송 계층에서 작동하며 모든 프로토콜(HTTP, HTTPS, FTP 등) 및 프록시 측의 DNS 해석을 지원합니다. Java 애플리케이션에서 SOCKS5는 HTTP뿐만 아니라 다른 TCP 연결을 프록시할 때 특히 유용합니다.
JVM 시스템 속성을 통한 SOCKS5 설정:
System.setProperty("socksProxyHost", "socks5.proxy.example.com");
System.setProperty("socksProxyPort", "1080");
System.setProperty("java.net.socks.username", "user");
System.setProperty("java.net.socks.password", "password");
OkHttp에서 SOCKS5 설정 (java.net.Proxy를 통해):
Proxy socks5Proxy = new Proxy(
Proxy.Type.SOCKS,
new InetSocketAddress("socks5.proxy.example.com", 1080)
);
OkHttpClient client = new OkHttpClient.Builder()
.proxy(socks5Proxy)
.build();
SOCKS5 및 DNS의 중요한 특징: 기본적으로 Java는 로컬에서 DNS를 해석하고 프록시를 통해 IP 주소를 전달합니다. 이는 실제 DNS 요청을 노출할 수 있습니다. DNS 해석도 프록시를 통해 이루어지도록 하려면, 다음과 같은 접근 방식을 OkHttp와 함께 사용하세요:
// SOCKS5를 통해 해석을 강제하는 커스텀 DNS
OkHttpClient client = new OkHttpClient.Builder()
.proxy(socks5Proxy)
.dns(hostname -> {
// 해석되지 않은 호스트를 반환 — OkHttp가 이를 프록시로 전달합니다.
return Collections.singletonList(InetAddress.getByName(hostname));
})
.build();
SOCKS5와 HTTP 프록시 선택 시기
| 기준 | HTTP 프록시 | SOCKS5 프록시 |
|---|---|---|
| 프로토콜 | HTTP/HTTPS | 모든 TCP/UDP |
| 프록시를 통한 DNS | 구현에 따라 다름 | 지원됨 |
| Java와의 호환성 | 네이티브 | 네이티브 (java.net.Proxy.Type.SOCKS) |
| 사용 용도 | 기업 게이트웨이, 웹 파싱 | 익명화, 비표준 프로토콜 |
Java 애플리케이션에서 프록시 회전
애플리케이션이 하나의 리소스에 대해 많은 요청을 하는 경우, 하나의 프록시 IP를 사용하는 것은 빠르게 차단으로 이어질 수 있습니다. 프록시 회전은 서로 다른 IP 주소 간의 전환으로 이 문제를 해결합니다. Java에서는 이를 여러 가지 방법으로 구현할 수 있습니다.
옵션 1: 회전 프록시 엔드포인트를 통한 회전
가장 간단한 접근 방식은 요청 시마다 또는 설정된 간격에 따라 IP를 자동으로 변경하는 단일 엔드포인트를 가진 프록시 공급자를 사용하는 것입니다. 하나의 호스트와 포트를 지정하면 회전은 공급자 측에서 이루어집니다. 회전을 위한 코드를 작성할 필요가 없습니다.
옵션 2: 프록시 목록을 통한 애플리케이션 측 회전
@Component
public class ProxyRotator {
private final List<ProxyConfig> proxies;
private final AtomicInteger counter = new AtomicInteger(0);
public ProxyRotator(@Value("${proxy.list}") List<String> proxyList) {
this.proxies = proxyList.stream()
.map(this::parseProxy)
.collect(Collectors.toList());
}
public OkHttpClient getClientWithNextProxy() {
int index = counter.getAndIncrement() % proxies.size();
ProxyConfig config = proxies.get(index);
Proxy proxy = new Proxy(
Proxy.Type.HTTP,
new InetSocketAddress(config.getHost(), config.getPort())
);
return new OkHttpClient.Builder()
.proxy(proxy)
.build();
}
private ProxyConfig parseProxy(String proxyStr) {
// "host:port" 또는 "user:pass@host:port" 형식의 문자열 파싱
String[] parts = proxyStr.split(":");
return new ProxyConfig(parts[0], Integer.parseInt(parts[1]));
}
}
옵션 3: ProxySelector를 통한 회전
Java는 ProxySelector라는 내장 메커니즘을 제공하여 대상 URL에 따라 동적으로 프록시를 선택할 수 있게 해줍니다. 이는 HttpURLConnection 및 Apache HttpClient와 함께 작동합니다:
ProxySelector.setDefault(new ProxySelector() {
private final List<Proxy> proxyPool = Arrays.asList(
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy1.example.com", 8080)),
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy2.example.com", 8080)),
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy3.example.com", 8080))
);
private final AtomicInteger idx = new AtomicInteger(0);
@Override
public List<Proxy> select(URI uri) {
// 도메인에 따라 선택 로직 추가 가능
int i = idx.getAndIncrement() % proxyPool.size();
return Collections.singletonList(proxyPool.get(i));
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
// 사용할 수 없는 프록시에 대한 처리 로직
log.warn("프록시 실패: {}", sa);
}
});
Java 작업에 적합한 프록시 유형 선택
프록시 유형 선택은 특정 작업에 따라 다릅니다. 기업 Java 애플리케이션에서는 일반적으로 세 가지 주요 유형을 고려합니다:
| 프록시 유형 | 속도 | 익명성 | 최적 사용 용도 |
|---|---|---|---|
| 데이터 센터 프록시 | 높음 | 중간 | 공개 데이터 파싱, API 테스트, 높은 부하 |
| 주거용 프록시 | 중간 | 높음 | 보안 사이트 파싱, 지리적 테스트, 차단 우회 |
| 모바일 프록시 | 중간 | 최대 | 모바일 API 통합, 강력한 방지 시스템을 가진 서비스 |
작업 유형에 대한 실용적인 권장 사항:
- 기업 게이트웨이 (Squid, Zscaler): 프록시 유형은 IT 부서에서 결정합니다. Apache HttpClient 또는 JVM 시스템 속성을 통해 인증이 있는 HTTP 프록시를 사용하세요.
- 공개 데이터 파싱 (뉴스, 인용, 공개 API): 데이터 센터 프록시가 속도와 비용의 최적 비율을 제공합니다.
- 전자상거래 파싱 (Wildberries, Ozon, Amazon): 회전하는 주거용 프록시. 이러한 플랫폼은 데이터 센터 IP를 적극적으로 차단합니다.
- 지리적 테스트 (현지화 확인, 지역별 가격): 국가/도시 선택이 가능한 주거용 프록시를 사용하세요.
- 모바일 서비스 및 애플리케이션 통합: 모바일 프록시는 실제 모바일 네트워크의 IP를 사용하여 실제 사용자를 가장 잘 모방합니다.
Java에서 프록시 설정 시 자주 발생하는 오류 및 해결 방법
경험이 풍부한 개발자조차도 Java에서 프록시 설정 시 동일한 문제에 직면하게 됩니다. 가장 일반적인 문제를 살펴보겠습니다.
1. 프록시가 설정되었지만 트래픽이 여전히 직접 전송됨
원인: JVM 시스템 속성을 설정했지만 OkHttp 또는 Reactor Netty (WebClient)를 사용하고 있어 이를 무시합니다.
해결 방법: 위의 섹션에서 설명한 대로 특정 HTTP 클라이언트의 구성에서 프록시를 명시적으로 설정하세요.
2. HTTPS를 통한 프록시에서 javax.net.ssl.SSLHandshakeException 발생
원인: 기업 프록시가 SSL 검사(MITM)를 수행하여 인증서를 변경합니다. Java는 기업 CA를 신뢰하지 않습니다.
해결 방법: 기업 인증서를 Java truststore에 가져옵니다:
keytool -import -trustcacerts \ -alias corporate-ca \ -file corporate-ca.crt \ -keystore $JAVA_HOME/lib/security/cacerts \ -storepass changeit
3. java.net.ConnectException: 연결 거부됨
원인: 잘못된 프록시 호스트 또는 포트; 프록시 서버가 사용 불가능함; 방화벽이 연결을 차단함.
진단:
# 프록시 접근 가능성 확인 curl -x http://proxy.example.com:8080 https://httpbin.org/ip # 또는 telnet을 통해 telnet proxy.example.com 8080
4. 프록시는 HTTP에서는 작동하지만 HTTPS에서는 작동하지 않음
원인: http.proxyHost/Port 속성만 설정되었고 https.proxyHost/Port는 설정되지 않았습니다.
해결 방법: 두 세트의 속성을 모두 설정하세요. HTTPS를 통한 HTTP 프록시는 CONNECT 터널링 방법을 사용하므로, 프록시가 이를 지원하는지 확인하세요.
5. 407 프록시 인증 필요
원인: 프록시가 인증을 요구하지만 설정되지 않았습니다.
해결 방법: JVM 시스템 속성을 위한 Authenticator를 추가하거나 Apache HttpClient / proxyAuthenticator를 위한 CredentialsProvider를 설정하세요 (위의 예제 참조).
6. 프록시 회전 시 메모리 누수 발생
원인: 각 요청마다 새로운 OkHttpClient 인스턴스가 생성됩니다. 각 클라이언트는 자신의 스레드 및 연결 풀을 유지합니다.
해결 방법: 클라이언트를 재사용하세요. 여러 클라이언트(프록시당 하나)를 만들어 풀을 구성하고 새 클라이언트를 생성하는 대신 회전하세요.
7. 진단을 위한 디버그 로깅 활성화
# application.properties — 프록시 진단을 위한 로깅 활성화 logging.level.org.apache.http=DEBUG logging.level.org.apache.http.wire=DEBUG logging.level.reactor.netty.http.client=DEBUG # JVM을 위한 시스템 속성 -Djava.net.debug=all
결론
Java 애플리케이션에서 프록시를 설정하는 것은 사용 중인 HTTP 클라이언트를 이해해야 합니다. JVM의 시스템 속성은 HttpURLConnection에 대한 빠른 시작이지만, OkHttp, WebClient 및 Apache HttpClient는 명시적인 구성이 필요합니다. 기업 애플리케이션의 경우 프록시 매개변수를 application.properties로 외부화하고 서로 다른 환경을 위한 Spring 프로파일을 사용하는 것이 좋습니다. 프록시 회전은 회전 엔드포인트를 가진 공급자에게 위임하는 것이 가장 쉽고 신뢰할 수 있습니다.
귀하의 Java 애플리케이션이 보안 플랫폼에서 데이터를 파싱하거나 외부 요청 시 높은 익명성이 필요한 경우, 주거용 프록시를 고려하는 것이 좋습니다. 이들은 실제 가정 사용자 IP를 가지고 있어 차단 시스템의 차단 가능성을 크게 줄입니다. 고부하 공개 데이터 파싱의 경우 데이터 센터 프록시가 최적입니다. 이들은 더 낮은 트래픽 비용으로 최대 속도를 제공합니다.
```