Kotlin 프록시란 무엇이고 왜 중요한가
Kotlin 앱이나 백엔드에서 Kotlin 프록시를 사용한다는 것은, HTTP 요청을 대상 서버가 아닌 중계 서버로 먼저 보내어 출발 IP를 바꾸는 패턴을 말합니다. 이는 단순한 우회가 아니라, 대상 사이트가 클라이언트의 출발 ASN을 보고 차단하는 현대적인 안티봇 환경에서 거의 필수가 되었습니다. 특히 소셜 플랫폼, 이커머스 가격 페이지, SERP 엔드포인트는 데이터센터 IP 대역을 정규 표현식이나 ASN 피드로 즉시 거르는 경우가 많습니다.
이 가이드는 Ktor client proxy와 OkHttp proxy authentication을 모두 다루며, ProxyHat의 gate.proxyhat.com:8080 게이트웨이를 기준으로 실제 동작하는 코드를 제공합니다. Android와 서버 모두에서 검증 가능한 설정을 목표로 합니다. Kotlin web scraping 작업을 병렬화하고 싶거나, 지역별 응답 차이를 테스트해야 하는 경우에도 동일한 패턴이 적용됩니다.
핵심 맥락을 한 줄로 요약하면: 프록시는 단순히 IP를 바꾸는 도구가 아니라, 대상 서비스의 ASN 기반 차단 회피, 지역별 콘텐츠 접근, 세션 일관성 유지를 위한 네트워크 계층의 추상화입니다. Kotlin 생태계에서는 Ktor와 OkHttp가 이 추상화를 엔진 단위로 다르게 처리하기 때문에, 두 클라이언트의 동작 차이를 정확히 이해하는 것이 이 가이드의 핵심입니다.
기술적 배경: 왜 residential 프록시가 필요한가
데이터센터 프록시는 AWS, Google Cloud, DigitalOcean 같은 호스팅 제공자의 ASN에 속합니다. Cloudflare와 유사한 안티봇 서비스는 Cloudflare의 IP 지능 피드를 통해 이러한 ASN을 실시간으로 분류하고, 데이터센터 출발 요청에 대해 무조건 CAPTCHA나 403을 반환하는 규칙을 적용합니다. 반면 residential 프록시는 실제 ISP 가입자에게 할당된 IP를 사용하므로, 브라우저 기반 사용자 트래픽과 거의 구별되지 않습니다.
소셜 미디어 플랫폼이나 티켓팅 사이트는 더 공격적입니다. 단일 데이터센터 IP에서 짧은 시간에 여러 요청이 오면 자동으로 ASN 전체를 일시 차단하는 경우가 있습니다. 이런 환경에서는 residential IP 풀을 로테이션하면서 요청당 다른 출발 IP를 부여하는 전략이 필요합니다. ProxyHat은 이를 위해 게이트웨이 단에서 IP 로테이션을 처리하므로, 클라이언트 코드는 단일 엔드포인트만 바라보면 됩니다.
법적 측면에서도 주의가 필요합니다. 미국에서는 Computer Fraud and Abuse Act (CFAA)가 무단 접근을 규제하며, EU에서는 GDPR이 공개된 개인정보라도 수집 목적에 따라 처리 근거를 요구합니다. 따라서 공개 데이터에 한정하고, 대상 서비스의 robots.txt와 ToS를 확인하며, 가능하면 공식 API를 우선 사용하는 것이 원칙입니다.
프로젝트 설정: Ktor 3와 OkHttp 기준
Ktor 3.x는 Kotlin 2.x 기반이며, CIO 엔진과 OkHttp 엔진을 모두 지원합니다. Gradle 설정에서 두 엔진을 함께 포함하면 런타임에 선택할 수 있습니다. 다음은 build.gradle.kts의 핵심 의존성입니다.
dependencies {
implementation("io.ktor:ktor-client-core:3.0.3")
implementation("io.ktor:ktor-client-cio:3.0.3")
implementation("io.ktor:ktor-client-okhttp:3.0.3")
implementation("io.ktor:ktor-client-content-negotiation:3.0.3")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.3")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}
OkHttp 기준선을 먼저 보면, OkHttpClient.Builder().proxy(java.net.Proxy(...))로 프록시를 지정할 수 있습니다. 하지만 ProxyHat처럼 사용자 인증이 필요한 게이트웨이에서는 Authenticator 인터페이스로 407 challenge에 대응해야 합니다. Ktor는 엔진에 따라 프록시 인증 처리 방식이 다르기 때문에, 이 차이를 명확히 이해하는 것이 이 가이드의 핵심입니다.
ProxyHat 게이트웨이 라우팅: HTTP 기본
ProxyHat의 HTTP 게이트웨이는 gate.proxyhat.com:8080입니다. 사용자 이름에 지역 타겟팅과 스티키 세션을 인코딩하는 것이 핵심 패턴입니다. 예를 들어 독일 베를린 IP를 스티키 세션 abc123으로 사용하려면 사용자 이름을 user-country-DE-city-berlin-session-abc123으로 설정합니다.
OkHttp에서 가장 간단한 형태는 다음과 같습니다.
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Authenticator
import okhttp3.Route
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64
val proxyUser = "user-country-DE-city-berlin-session-abc123"
val proxyPass = System.getenv("PROXYHAT_PASS") ?: "YOUR_PASS"
val client = OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
.proxyAuthenticator(object : Authenticator {
override fun authenticate(route: Route?, response: okhttp3.Response): Request? {
val credential = Credentials.basic(proxyUser, proxyPass)
return response.request.newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
})
.build()
val request = Request.Builder()
.url("https://httpbin.org/ip")
.build()
client.newCall(request).execute().use { resp ->
println(resp.body?.string())
}
이 예제에서 Credentials.basic(proxyUser, proxyPass)는 Proxy-Authorization: Basic ... 헤더를 생성합니다. OkHttp는 407 응답을 받으면 proxyAuthenticator를 호출하므로, 첫 요청에 미리 헤더를 넣지 않아도 됩니다. 하지만 프록시가 사전 인증을 요구하는 환경에서는 첫 요청부터 헤더를 포함하는 것이 왕복을 줄이는 방법입니다.
Ktor에서는 엔진이 OkHttp일 때와 CIO일 때 프록시 인증 처리가 다릅니다. CIO 엔진은 현재 프록시 인증을 기본적으로 지원하지 않으므로, defaultRequest에서 Proxy-Authorization 헤더를 직접 추가하는 방식을 사용합니다.
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64
import kotlinx.serialization.json.Json
val proxyUser = "user-country-DE-city-berlin-session-abc123"
val proxyPass = System.getenv("PROXYHAT_PASS") ?: "YOUR_PASS"
val basic = Base64.getEncoder().encodeToString("$proxyUser:$proxyPass".toByteArray())
val ktorClient = HttpClient(OkHttp) {
engine {
config {
proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
}
}
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
defaultRequest {
url("https://httpbin.org/")
header("Proxy-Authorization", "Basic $basic")
}
}
suspend fun main() = ktorClient.use { c ->
val body: String = c.get("ip").body()
println(body)
}
주의할 점은 Proxy-Authorization 헤더가 프록시로 향하는 CONNECT 요청에 붙어야 한다는 것입니다. OkHttp 엔진을 쓸 때는 엔진이 이를 자동 처리하지만, CIO 엔진에서는 헤더 주입이 엔진 계층까지 도달하지 않을 수 있어, 인증이 필요한 환경에서는 OkHttp 엔진을 권장합니다.
지역 타겟팅과 스티키 세션 인코딩
ProxyHat의 사용자 이름 스키마는 플래그를 -로 연결하는 구조입니다. 다음은 자주 쓰는 조합입니다.
user-country-US— 미국 전체 풀에서 라운드로빈user-country-DE-city-berlin— 베를린 도시 단위 타겟팅user-country-JP-session-tokyo1— 일본 IP를 세션 ID로 고정user-country-GB-session-r-42— 영국 IP 고정, 재시도 시에도 동일 IP 유지
스티키 세션은 동일 사용자 이름으로 보내는 모든 요청이 동일 출발 IP로 나가도록 보장합니다. 로그인 후 상태를 유지해야 하는 소셜 플랫폼 스크래핑이나, 다단계 결제 플로우를 테스트하는 QA 시나리오에서 필수적입니다. 세션을 바꾸면 새 IP가 할당되므로, IP 로테이션이 필요할 때는 세션 ID를 난수로 교체하면 됩니다.
ProxyHat SDK는 이 패턴을 미러링합니다. ProxyHat 공식 문서에 따르면, SDK는 사용자 이름 빌더를 제공하여 플래그 조합을 타입 안전하게 구성할 수 있습니다. 다음은 Kotlin에서 동일한 로직을 직접 구현한 헬퍼입니다.
data class ProxyOptions(
val country: String? = null,
val city: String? = null,
val session: String? = null
) {
fun toUsername(base: String = "user"): String = buildString {
append(base)
country?.let { append("-country-$it") }
city?.let { append("-city-$it") }
session?.let { append("-session-$it") }
}
}
fun makeUrl(opts: ProxyOptions, pass: String): String {
val u = opts.toUsername()
return "http://$u:$pass@gate.proxyhat.com:8080"
}
fun main() {
val url = makeUrl(ProxyOptions("DE", "berlin", "abc123"), "secret")
println(url) // http://user-country-DE-city-berlin-session-abc123:secret@gate.proxyhat.com:8080
}
이 헬퍼는 URL 기반 프록시 설정을 지원하는 라이브러리에서 바로 사용할 수 있습니다. 다만 Ktor와 OkHttp 모두 java.net.Proxy 객체를 사용할 때는 URL에 사용자 정보를 포함할 수 없으므로, 인증은 별도 헤더 또는 Authenticator로 처리해야 합니다.
SOCKS5 설정: 포트 1080
ProxyHat은 SOCKS5 게이트웨이도 gate.proxyhat.com:1080으로 제공합니다. SOCKS5는 HTTP보다 낮은 계층에서 동작하므로, JVM에서는 시스템 속성을 통해 사용자 인증을 전달하는 것이 일반적입니다. java.net.socks.username과 java.net.socks.password가 그 키입니다.
import java.net.InetSocketAddress
import java.net.Proxy
import okhttp3.OkHttpClient
import okhttp3.Request
fun socksExample() {
System.setProperty("java.net.socks.username", "user-country-DE-city-berlin")
System.setProperty("java.net.socks.password", System.getenv("PROXYHAT_PASS") ?: "YOUR_PASS")
val client = OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("gate.proxyhat.com", 1080)))
.build()
val request = Request.Builder().url("https://httpbin.org/ip").build()
client.newCall(request).execute().use { r ->
println(r.body?.string())
}
}
주의: 시스템 속성은 JVM 전역에 적용되므로, 멀티테넌트 서비스에서는 서로 다른 SOCKS5 자격증명을 동시에 사용하기 어렵습니다. 이 경우 각 요청마다 별도 Proxy 객체와 커스텀 Authenticator를 사용하는 HTTP 게이트웨이가 더 적합합니다. SOCKS5는 주로 TCP 레벨 라우팅이 필요한 비-HTTP 프로토콜이나, HTTP CONNECT보다 낮은 오버헤드가 중요한 대량 병렬 작업에서 선택합니다.
코루틴 동시 요청 팬아웃: Semaphore로 속도 제어
residential 프록시로 대량 스크래핑을 할 때는 동시성 제어가 필수입니다. 무제한 async를 띄우면 프록시 게이트웨이가 속도 제한을 걸거나, 대상 사이트가 순간 트래픽 스파이크를 탐지해 차단합니다. Semaphore로 동시 실행 수를 묶고, awaitAll로 결과를 모으는 패턴이 Kotlin에서 가장 자연스럽습니다.
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64
suspend fun fanOut(urls: List<String>, concurrency: Int = 20) {
val proxyUser = "user-country-US"
val proxyPass = System.getenv("PROXYHAT_PASS") ?: "YOUR_PASS"
val basic = Base64.getEncoder().encodeToString("$proxyUser:$proxyPass".toByteArray())
val sem = Semaphore(concurrency)
val client = HttpClient(OkHttp) {
engine {
config {
proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
retryOnConnectionFailure(true)
connectTimeout(10_000, java.util.concurrent.TimeUnit.MILLISECONDS)
readTimeout(30_000, java.util.concurrent.TimeUnit.MILLISECONDS)
}
}
}
coroutineScope {
val jobs = urls.map { url ->
async(Dispatchers.IO) {
sem.withPermit {
try {
val resp: HttpResponse = client.get(url) {
header("Proxy-Authorization", "Basic $basic")
}
resp.status.value to resp.bodyAsText().take(200)
} catch (e: Exception) {
-1 to (e.message ?: "error")
}
}
}
}
jobs.awaitAll().forEachIndexed { i, (status, snippet) ->
println("[$i] $status :: $snippet")
}
}
client.close()
}
fun main() = runBlocking {
val urls = (1..100).map { "https://httpbin.org/anything?id=$it" }
fanOut(urls, concurrency = 25)
}
여기서 concurrency = 25는 프록시 게이트웨이에 가해지는 동시 부하의 상한입니다. residential 풀은 일반적으로 초당 수백 요청을 처리하지만, 대상 사이트의 안티봏이 50ms 이내에 동일 IP에서 여러 요청을 감지하면 차단하므로, IP 로테이션 주기와 동시성을 함께 조율해야 합니다. 세션 ID를 요청마다 난수로 바꾸면 매 요청 새 IP가 할당되므로, 동일 IP 부하를 줄일 수 있습니다.
실패 처리는 단순 try/catch 이상이 필요합니다. 403/429가 연속되면 해당 세션을 일시 중단하고, 일정 횟수 이상 실패하면 서킷 브레이커로 전체 작업을 멈추는 것이 프로덕션에서 권장됩니다. Kotlin에서는 Flow.retryWhen과 지수 백오프를 조합해 이를 구현합니다.
프로덕션 하드닝: 407, 재시도, 커넥션 풀, TLS
OkHttp의 Authenticator는 407 Proxy Authentication Required에 대해 자동으로 재시도 헤더를 붙여주지만, 무한 루프를 방지하기 위해 응답 횟수를 확인해야 합니다. 다음은 안전한 Authenticator 구현입니다.
import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
class ProxyAuth(private val user: String, private val pass: String) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
if (response.code == 407 && response.priorResponse == null) {
val cred = Credentials.basic(user, pass)
return response.request.newBuilder()
.header("Proxy-Authorization", cred)
.build()
}
return null // 더 이상 재시도하지 않음
}
}
priorResponse == null 검사는 이미 한 번 인증 시도한 요청이 다시 407을 받은 경우 재시도를 중단합니다. 이 검사가 없으면 프록시가 계속 407을 반환할 때 OkHttp가 무한 호출될 수 있습니다.
커넥션 풀과 타임아웃은 대량 병렬 작업에서 메모리와 파일 디스크립터 사용량에 직접 영향을 줍니다. 다음은 권장 설정입니다.
import okhttp3.ConnectionPool
import java.util.concurrent.TimeUnit
val pool = ConnectionPool(
maxIdleConnections = 50,
keepAliveDuration = 5,
TimeUnit.MINUTES
)
val hardened = OkHttpClient.Builder()
.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080)))
.proxyAuthenticator(ProxyAuth("user-country-US", System.getenv("PROXYHAT_PASS")!!))
.connectionPool(pool)
.connectTimeout(8, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.callTimeout(60, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
TLS 설정에서는 최신 Android와 서버 모두 TLS 1.2 이상을 요구합니다. OkHttp는 기본적으로 Conscrypt 또는 플랫폼 SSLSocketFactory를 사용하므로, 별도 구성 없이도 TLS 1.3을 협상합니다. 다만 Android 7.0 미만에서는 Conscrypt를 명시적으로 추가하거나, NetworkSecurityConfig에서 cleartext 트래픽을 허용하지 않도록 주의해야 합니다. AndroidManifest.xml에 android:networkSecurityConfig를 지정하고, 프록시 도메인에 대해 인증서 고정을 하지 않는 것이 좋습니다. 프록시는 TLS 종단이 아니므로 고정 대상이 아닙니다.
로깅은 프로덕션에서 필수입니다. OkHttp HttpLoggingInterceptor를 BASIC 레벨로만 사용하고, 헤더에 인증 정보가 포함되므로 BODY 레벨은 운영 환경에서 피합니다. Ktor는 Logging 플러그인의 LogLevel.HEADERS를 사용하되, sanitizeHeader로 Proxy-Authorization을 마스킹합니다.
프록시 유형 비교: residential vs datacenter vs mobile
작업 성격에 따라 적합한 프록시 유형이 다릅니다. 다음 표는 대표적인 세 유형의 특성을 비교합니다.
| 특성 | Residential | Datacenter | Mobile |
|---|---|---|---|
| 출발 ASN | ISP 가입자 | 호스팅 제공자 | 이동통신사 |
| 차단 확률 | 낮음 | 높음 | 매우 낮음 |
| 평균 지연 | 200–800ms | 20–100ms | 300–1500ms |
| 단가 수준 | 중간 | 낮음 | 높음 |
| 적합 용도 | 소셜, SERP, 이커머스 | 대량 단순 수집 | 앱스토어, 모바일 전용 타겟 |
ProxyHat은 세 유형 모두 제공하므로, 작업별로 게이트웨이 사용자 이름 플래그만 바꿔서 전환할 수 있습니다. 예산이 민감한 대량 수집은 datacenter로 시작하고, 차단이 잦은 타겟은 residential으로 전환하는 전략이 효율적입니다. 자세한 가격 정보는 ProxyHat 가격 페이지를 참조하세요.
윤리적 스크래핑과 법적 고려
프록시를 쓴다고 해서 대상 서비스의 이용약관이나 법적 제약이 사라지지 않습니다. 미국에서는 CFAA가 무단 접근과 초과 사용을 규제하며, EU에서는 GDPR이 공개된 개인정보라도 처리 목적과 근거를 요구합니다. 다음 원칙을 권장합니다.
- 공개 데이터에 한정하고, 로그인이 필요한 페이지는 가능하면 공식 API를 사용합니다.
- robots.txt를 존중하고,
Crawl-delay를 준수합니다. - 개인 식별 정보는 수집하지 않거나 즉시 익명화합니다.
- 요청 속도는 대상 서비스의 트래픽에 눈에 띄지 않도록 조절합니다.
공식 API가 있는 경우, 예를 들어 SERP 데이터가 필요하다면 SERP 추적 유스케이스에서 설명한 것처럼 API 우선 접근이 더 안정적이고 법적 리스크가 낮습니다. 웹 스크래핑이 불가피한 경우에는 웹 스크래핑 유스케이스의 가이드라인을 따르세요.
ProxyHat 전용 설정 요약
ProxyHat 게이트웨이는 단일 엔드포인트 gate.proxyhat.com에 HTTP 8080, SOCKS5 1080을 제공합니다. 사용자 이름에 모든 제어 플래그를 인코딩하므로, 클라이언트 코드는 인증 헤더 또는 시스템 속성만 바꾸면 지역, 세션, 로테이션을 모두 제어할 수 있습니다. 지원 위치 목록을 확인하여 국가/도시 코드를 정확히 사용하세요.
ProxyHat SDK는 동일한 패턴을 타입 안전 빌더로 감싸며, 세션 ID 생성, 실패 재시도, 메트릭 수집을 기본 제공합니다. Kotlin에서는 이 가이드의 헬퍼와 SDK를 함께 사용하면, 설정 코드를 중복 없이 유지할 수 있습니다.
핵심 요약
Key Takeaways
- Ktor에서 프록시 인증은 엔진마다 다르다. OkHttp 엔진 +
Proxy-Authorization헤더가 가장 안정적이다.- ProxyHat 사용자 이름에
-country-XX-city-yy-session-zzz를 인코딩하여 지역과 세션을 제어한다.- SOCKS5는 포트 1080, 시스템 속성
java.net.socks.username/password로 인증한다.- 동시 요청은
Semaphore로 묶고, 세션 ID를 난수로 바꿔 IP 로테이션을 유도한다.- 407 challenge는
Authenticator에서priorResponse검사로 무한 루프를 막는다.- 공개 데이터만 수집하고, robots.txt와 ToS를 준수하며, 공식 API를 우선한다.
이 가이드의 코드는 그대로 복사해 실행할 수 있습니다. ProxyHat 계정이 없다면 가격 페이지에서 플랜을 확인하고, 환경 변수 PROXYHAT_PASS만 설정하면 됩니다. Kotlin 프록시 작업의 첫 단계로, 단일 요청부터 시작해 점진적으로 동시성을 높이는 것을 권장합니다.






