본문 바로가기

CS

프록시와 리버스 프록시, 그리고 자바 스프링에서의 프록시 활용 정리

개발자로서 한 번쯤은 들어봤을 프록시라는 단어. 그런데 이 단어는 너무 많은 맥락에서 쓰이다 보니 헷갈릴 때가 많습니다. 프록시 패턴, 스프링 AOP의 프록시, 리버스 프록시, JDK Proxy, CGLIB Proxy 등. 이 글에서는 프록시와 관련된 개념들을 하나씩 풀어가며, 프로그래밍 관점에서부터 서버 인프라 관점까지 통합적으로 이해할 수 있도록 설명해보겠습니다.


프록시란?

 

프록시는 말 그대로 '대리인'입니다. 진짜 객체에 직접 접근하지 않고, 중간에서 대신 일을 처리해주는 가짜 객체를 말합니다. 비유하자면, 스타 연예인에게 바로 연락할 수 없고, 매니저를 통해 인터뷰 요청을 넣는 것과 같습니다.

 

 


개발에서는 주로 다음과 같은 상황에서 프록시가 사용됩니다.

객체 접근을 제어해야 할 때 (보안, 인증 등)
호출 전후로 부가 로직을 삽입할 때 (로깅, 트랜잭션 등)
객체 생성 비용이 비쌀 때 (지연 초기화, Lazy Loading)

 

 


리버스 프록시란?

 

리버스 프록시는 프록시와 구조는 비슷하지만 서버 쪽에 위치한 프록시입니다. 클라이언트는 서버에 요청을 보내지만, 실제로는 리버스 프록시가 요청을 받아 내부 서버로 전달합니다. 사용자는 내부 서버의 존재를 알지 못합니다.

 

비유하자면, 대기업 대표 전화로 전화를 걸면 담당 부서로 자동 연결되듯, 리버스 프록시는 사용자의 요청을 적절한 내부 서버로 분산해줍니다.

 

 


리버스 프록시의 역할은 다음과 같습니다

내부 서버 IP 숨기기 (보안 강화)
로드 밸런싱
정적 리소스 캐싱
SSL 종료

 

server {
    listen 80;
    server_name myapp.local;

    location / {
        proxy_pass http://localhost:8080;  # Spring Boot 서버
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

 

위 설정은 클라이언트가 http://myapp.local로 요청했을 때, 내부의 localhost:8080 Spring 서버로 요청을 전달하는 구조입니다.

 


 

로드 밸런싱

 

리버스 프록시는 단순히 요청을 전달하는 것을 넘어서 트래픽을 분산하는 로드 밸런서 역할도 수행할 수 있습니다. Nginx는 대표적인 리버스 프록시로, 내부적으로 여러 백엔드 서버에 요청을 분산시켜 고가용성과 확장성을 확보합니다. 마치 음식점에서 손님이 무엇을 주문하든, 매니저가 셰프 여러 명에게 요청을 적절히 분산시키는 것과 같습니다.

 

upstream backend {
    server localhost:8081;
    server localhost:8082;
}

server {
    listen 80;
    server_name myapp.local;

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

upstream 블록에 백엔드 서버를 등록하고, 클라이언트의 요청을 자동으로 분산시킵니다.

 


 

스프링 AOP

 

AOP는 한 마디로 말하면, 공통적인 로직을 흩어지지 않게 모아서 처리하는 방법입니다. 예를 들어 어떤 애플리케이션이 있다고 해봅시다. 사용자가 로그인을 하거나, 게시글을 쓰거나, 댓글을 남길 때마다 개발자는 이런 생각을 하게 됩니다. "이 기능마다 실행 시간을 로그로 남기고 싶은데, 매번 System.out.println() 넣기도 그렇고, 코드가 지저분해지네?"

 

이럴 때 사용하는 것이 AOP입니다. 핵심 로직은 그대로 두고, 그 앞뒤로 실행시간 측정, 트랜잭션 처리, 보안 검사 같은 부가 작업을 프록시를 통해 자동으로 끼워넣는 방식입니다.

 


JDK Dynamic Proxy란?

 

스프링 AOP는 내부적으로 프록시를 사용합니다. 그중 하나가 JDK Dynamic Proxy입니다. 이 방식은 인터페이스 기반으로 프록시 객체를 만들 수 있는 방식입니다.

 

즉, 마치 인터페이스는 역할만 정의된 설계도입니다. JDK Dynamic Proxy는 이 설계도만 가지고, 마치 실제 객체인 척 연기할 수 있는 대리인을 만들어주는 도구입니다. 참고로 java.lang.reflect.Proxy와 InvocationHandler를 활용해 런타임에 프록시 객체를 생성합니다.

 

비유하자면, 콜센터 매뉴얼을 보고 새로 들어온 상담원이 기존 직원처럼 응대하는 것과 같아요. 인터페이스(매뉴얼)만 있으면, 실제 객체를 대신해서 프록시가 중간에서 일을 처리할 수 있는 거죠. 아래 이 코드는 Service라는 인터페이스만 있으면, RealService 대신 프록시 객체가 모든 호출을 감싸줄 수 있다는 걸 보여줍니다.

 

interface Service {
    void execute();
}

class RealService implements Service {
    public void execute() {
        System.out.println("Real service executed");
    }
}

class ServiceInvocationHandler implements InvocationHandler {
    private final Service target;

    public ServiceInvocationHandler(Service target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before");
        Object result = method.invoke(target, args);
        System.out.println("After");
        return result;
    }
}

// 사용 예
Service real = new RealService();
Service proxy = (Service) Proxy.newProxyInstance(
    Service.class.getClassLoader(),
    new Class[]{Service.class},
    new ServiceInvocationHandler(real)
);
proxy.execute();

 


CGLIB Proxy

 

CGLIB Proxy는 인터페이스가 없을 때 사용됩니다. 클래스를 상속해서 프록시 객체를 만드는 방식입니다. 스프링에서는 클래스에만 의존할 경우 자동으로 CGLIB을 사용합니다.

 

마치 원본 책을 복사해서, 복사본에만 메모를 덧붙이는 것과 같아요. 원본 내용을 바꾸지 않고도, 복사본에서만 부가적인 처리를 할 수 있는 거죠. 그런데 중요한 제한이 하나 있어요. final로 선언된 클래스나 메서드는 복사해서 덧붙일 수 없기 때문에, 프록시로 만들 수 없습니다.

 

아래 코드는 CoffeeService라는 클래스를 상속받아서 프록시를 만들고, order() 메서드 앞뒤로 부가 로직을 삽입한 구조입니다.

class CoffeeService {
    public void order() {
        System.out.println("커피 주문 처리");
    }
}

// CGLIB 사용 (Spring 없이 직접 할 경우 Enhancer 필요)
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(CoffeeService.class);
enhancer.setCallback(new MethodInterceptor() {
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("주문 전 결제 확인");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("주문 후 알림 전송");
        return result;
    }
});

CoffeeService proxy = (CoffeeService) enhancer.create();
proxy.order();

 


 

스프링 AOP는 바로 이 두 프록시 방식 중 하나를 사용해서 동작합니다.

 

  • 인터페이스가 있으면 → JDK Dynamic Proxy 사용
  • 인터페이스가 없으면 → CGLIB Proxy 사용

스프링은 우리가 직접 프록시를 만들지 않아도, AOP를 설정하는 순간 자동으로 판단해서 적절한 방식으로 프록시를 생성합니다. 우리가 @Transactional, @Async, @Around 같은 애너테이션만 붙이면 프록시가 생성되고, 그 앞뒤로 로직이 실행되는 구조입니다.


마무리

AOP의 핵심은 프록시이고, 그 프록시는 상황에 따라 JDK Dynamic Proxy 또는 CGLIB Proxy로 생성됩니다. 이 둘을 정확히 이해하면, 트랜잭션이 왜 잘 안 먹히는지, 프록시가 왜 생성되지 않는지 같은 문제 상황도 훨씬 쉽게 디버깅할 수 있습니다.