파이썬/크롤링

VPN과 프록시 서버

코샵 2024. 11. 30. 10:21
반응형

소개

인터넷을 사용하다 보면 VPN과 프록시 서버라는 용어를 자주 접하게 됩니다. 이 둘은 비슷해 보이지만 작동 방식과 용도에 중요한 차이가 있습니다. 이번 글에서는 VPN과 프록시 서버의 차이점을 이해하고, Python의 aiohttp를 사용한 구현 방법까지 자세히 알아보겠습니다.

VPN(Virtual Private Network)의 이해

VPN은 공용 네트워크를 통해 사설 네트워크처럼 안전한 연결을 만드는 기술입니다.

 

VPN의 주요 특징

  1. 전체 네트워크 트래픽을 암호화
  2. 운영체제 수준에서 작동
  3. IP 주소 완전 변경
  4. 모든 애플리케이션에 적용

프록시 서버의 이해

프록시 서버는 클라이언트와 목적지 서버 사이에서 중계 역할을 하는 서버입니다.

프록시 서버의 종류

  1. Forward Proxy (포워드 프록시)
    • 클라이언트 -> 프록시 -> 목적지 서버 내부 네트워크에서 외부로 나가는 트래픽을 중계
  2. Reverse Proxy (리버스 프록시)
    • 클라이언트 <- 프록시 <- 목적지 서버 외부에서 들어오는 트래픽을 내부 서버로 분산 로드 밸런싱, 캐싱 등의 용도로 사용
  3. SOCKS Proxy
    • TCP/UDP 레벨에서 작동하는 프록시 더 낮은 레벨에서 동작하여 다양한 프로토콜 지원

VPN과 프록시의 주요 차이점

  VPN Proxy
보안 수준 전체 트래픽 암호화 특정 애플리케이션/프로토콜만 처리
적용 범위 시스템 전체 특정 애플리케이션 또는 프로토콜
속도 암호화로 인한 속도 저하 상대적으로 빠름
설정 복잡도 시스템 수준 설정 필요 애플리케이션별 간단 설정

aiohttp를 사용한 프록시 구현

기본 프록시 클라이언트

import aiohttp
import asyncio
from typing import Optional, Dict

class ProxyClient:
    def __init__(self, proxy_url: Optional[str] = None):
        """
        비동기 프록시 클라이언트

        Args:
            proxy_url: "http://proxy.example.com:8080" 형식의 프록시 주소
        """
        self.proxy_url = proxy_url
        self.session = None

    async def __aenter__(self):
        """비동기 컨텍스트 관리자 진입"""
        self.session = aiohttp.ClientSession()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """세션 정리"""
        if self.session:
            await self.session.close()

    async def get(self, url: str, headers: Optional[Dict] = None) -> str:
        """
        프록시를 통한 GET 요청

        Example:
            async with ProxyClient("http://proxy.example.com:8080") as client:
                response = await client.get("http://example.com")
        """
        if not self.session:
            raise RuntimeError("세션이 초기화되지 않았습니다")

        async with self.session.get(
            url,
            proxy=self.proxy_url,
            headers=headers
        ) as response:
            return await response.text()

 

프록시 서버 구현

from aiohttp import web
import aiohttp
from typing import Optional

class ProxyServer:
    def __init__(self, host: str = "0.0.0.0", port: int = 8080):
        """
        간단한 프록시 서버 구현
        """
        self.host = host
        self.port = port
        self.app = web.Application()
        self.app.router.add_route("*", "/{path:.*}", self.handle_request)

    async def handle_request(self, request: web.Request) -> web.Response:
        """
        모든 요청을 처리하는 핸들러
        """
        # 원본 요청 URL 재구성
        path = request.match_info["path"]
        target_url = f"http://{path}"

        # 클라이언트 헤더 복사
        headers = dict(request.headers)
        headers.pop("Host", None)

        try:
            async with aiohttp.ClientSession() as session:
                # 원본 요청 메서드와 동일한 메서드로 요청
                async with session.request(
                    request.method,
                    target_url,
                    headers=headers,
                    data=await request.read()
                ) as response:
                    # 응답 반환
                    return web.Response(
                        status=response.status,
                        body=await response.read(),
                        headers=response.headers
                    )
        except Exception as e:
            return web.Response(status=500, text=str(e))

    def run(self):
        """서버 실행"""
        web.run_app(self.app, host=self.host, port=self.port)

# 사용 예시
if __name__ == "__main__":
    proxy = ProxyServer()
    proxy.run()

 

SOCKS5 프록시 클라이언트

import aiohttp
from aiohttp_socks import ProxyConnector
from typing import Optional

class SOCKS5Client:
    def __init__(
        self,
        proxy_host: str,
        proxy_port: int,
        username: Optional[str] = None,
        password: Optional[str] = None
    ):
        """
        SOCKS5 프록시 클라이언트

        Args:
            proxy_host: 프록시 서버 주소
            proxy_port: 프록시 서버 포트
            username: 인증 사용자명 (선택)
            password: 인증 비밀번호 (선택)
        """
        self.proxy_host = proxy_host
        self.proxy_port = proxy_port
        self.username = username
        self.password = password
        self.session = None

    async def __aenter__(self):
        """프록시 연결 설정"""
        connector = ProxyConnector.from_url(
            f"socks5://{self.proxy_host}:{self.proxy_port}",
            username=self.username,
            password=self.password
        )
        self.session = aiohttp.ClientSession(connector=connector)
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """세션 정리"""
        if self.session:
            await self.session.close()

    async def request(self, method: str, url: str, **kwargs):
        """
        프록시를 통한 요청 전송

        Example:
            async with SOCKS5Client("proxy.example.com", 1080) as client:
                response = await client.request("GET", "http://example.com")
        """
        if not self.session:
            raise RuntimeError("세션이 초기화되지 않았습니다")

        async with self.session.request(method, url, **kwargs) as response:
            return await response.text()

보안 및 모니터링

프록시 로깅 미들웨어

import logging
from datetime import datetime

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@web.middleware
async def logging_middleware(request: web.Request, handler):
    """
    프록시 요청/응답 로깅
    """
    start_time = datetime.now()

    try:
        response = await handler(request)
        duration = datetime.now() - start_time

        logger.info(
            f"Request: {request.method} {request.path} "
            f"Status: {response.status} "
            f"Duration: {duration.total_seconds():.3f}s"
        )

        return response
    except Exception as e:
        logger.error(f"Error processing request: {str(e)}")
        raise

활용 예시

지역 제한 우회

async def access_geo_restricted_content(url: str, proxy_url: str):
    """
    지역 제한 콘텐츠 접근 예시
    """
    async with ProxyClient(proxy_url) as client:
        try:
            content = await client.get(
                url,
                headers={"User-Agent": "Mozilla/5.0 ..."}
            )
            return content
        except Exception as e:
            print(f"접근 실패: {str(e)}")
            return None

 

웹 스크래핑

async def scrape_with_rotating_proxies(
    urls: List[str],
    proxy_list: List[str]
):
    """
    여러 프록시를 번갈아 사용하는 스크래핑
    """
    results = []

    for url, proxy in zip(urls, cycle(proxy_list)):
        async with ProxyClient(proxy) as client:
            try:
                content = await client.get(url)
                results.append(content)
            except Exception as e:
                print(f"스크래핑 실패 ({proxy}): {str(e)}")

    return results

마치며

VPN과 프록시는 각각의 장단점이 있으며, 사용 목적에 따라 적절히 선택해야 합니다. aiohttp를 사용하면 Python에서 효율적인 프록시 클라이언트와 서버를 구현할 수 있으며, 비동기 처리를 통해 높은 성능을 얻을 수 있습니다.