[Spring] 프록시 패턴(Proxy Pattern)
스프링 프레임워크에서 프록시 패턴은 다양한 기능을 제공하는 핵심 매커니즘 중 하나입니다.
스프링에서는 프록시 객체를 통해 AOP(Aspect-Oriented Programming), 트랜잭션, 보안, 비동기 작업 등 다양한 부가 기능을 비즈니스 로직과 분리하여 쉽게 적용할 수 있습니다.
스프링의 프록시 패턴에 대해 알아보겠습니다. ✍️
프록시 패턴의 개념
프록시 패턴은 대리 객체(프록시)를 통해 실제 객체에 대한 접근을 제어하는 디자인 패턴입니다.
- 클라이언트는 실제 객체를 직접 호출하지 않고, 프록시 객체를 통해 호출합니다.
- 프록시 객체는 실제 객체에 대한 접근을 제어하며 중간에서 부가적인 작업(ex: 로깅, 권한 확인 등)을 수행할 수 있습니다.
스프링에서는 이 프록시를 통해 공통 기능을 실제 객체에 투명하게 적용할 수 있으며 특정 로직을 추가하거나 접근을 제한하는 등 다양한 부가 기능을 수행할 수 있습니다.
스프링에서의 프록시 사용 방식
스프링 프레임워크에서는 JDK 동적 프록시와 CGLIB 프록시의 두 가지 방식으로 프록시를 생성합니다.
- JDK 동적 프록시
- 인터페이스 기반 프록시 생성 방식입니다. `java.lang.reflect.Proxy`를 사용해 인터페이스를 구현하는 프록시 객체가 생성됩니다.
- 인터페이스가 존재하는 경우에만 사용할 수 있으며, 프록시가 인터페이스의 모든 메서드 호출을 가로채어 제어합니다.
- CGLIB(Code GenerationLibrary)
- 클래스 기반의 프록스를 생성하며 주로 인터페이스가 없을 때 사용됩니다.
- 바이트코드 조작을 통해 실제 클래스를 상속한 프록시 객체를 생성하며 `final`로 선언된 클래스나 메서드에는 사용할 수 없습니다.
- CGLIB 프록시는 JDK 동적 프록시에 비해 성능이 좋을 수 있지만 메모리 사용량이 더 높습니다.
스프링이 프록시를 사용할 때 인터페이스가 존재하면 JDK 동적 프록시를 인터페이스가 없으면 CGLIB을 기본으로 사용합니다.
스프링 프록시와 AOP
스프링의 AOP(Aspect-Oriented Progaramming) 기능은 프록시 패턴을 통해 구현됩니다.
AOP는 핵심 로직과 부가 기능(로깅, 트랜잭션, 보안 등)을 분리해 각기 독립적으로 관리할 수 있도록 돕습니다.
- Aspect: 부가 기능의 집합입니다. 스프링에서 `@Aspect`어노테이션을 사용해 정의합니다.
- Advice: 실제로 실행되는 부가 기능으로 메서드 실행 전후 또는 예외 발생 시 실행될 수 있습니다.
- Pointcut: 부가 기능이 적용될 지점을 정의하며 특정 메서드나 클래스에 적용됩니다.
이러한 AOP는 스프링이 메서드를 호출할 때마다 프록시를 통해 적용됩니다.
예를 들어 `@Transactional`이나 `@Async`등의 어노테이션이 붙은 메서드는 프록시로 감싸져서 메서드 실행 전후에 트랜잭션 관리나 비동기 처리가 이루어집니다.
스프링에서 프록시 패턴을 사용하는 주요 예시
트랜잭션 관리(@Transactional)
`@Transactional`은 스프링이 프록시를 통해 관리하는 대표적인 예시입니다. `@Transactional`이 붙은 메서드는 프록시로 감싸지며, 메서드가 실행되기 전 트랜잭션을 시작하고 메서드 실행이 완료되면 트랜잭션 커밋하거나 롤백합니다.
비동기 처리(@Async)
`@Async`어노테이션을 붙이면 메서드가 프록시로 감싸지고 프록시는 비동기 스레드 풀을 통해 메서드를 비동기로 실행합니다. 이로 인해 호출한 스레드는 메서드가 종료될 때까지 기다리지 않고 별도의 스레드에서 작업을 처리합니다.
보안(@PreAuthorize, @Secured)
스프링 시큐리티에서 특정 메서드에 대해 접근 권한을 제어할 때도 프록시가 사용됩니다. `@PreAuthorize`같은 어노테이션이 적용된 메서드는 프록시가 생성되어 사용자의 접근 권한을 검사하며, 권한이 없을 경우 접근을 막는 로직이 실행됩니다.
JDK 동적 프록시 예시
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyDemo {
public static void main(String[] args) {
MyInterface myInterface = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class[]{MyInterface.class},
new MyInvocationHandler(new MyInterfaceImpl())
);
myInterface.doSomething();
}
}
interface MyInterface {
void doSomething();
}
class MyInterfaceImpl implements MyInterface {
public void doSomething() {
System.out.println("Doing something in MyInterfaceImpl");
}
}
class MyInvocationHandler implements InvocationHandler {
private final Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method call");
Object result = method.invoke(target, args);
System.out.println("After method call");
return result;
}
}
위 코드에서 `MyInvocationHandler`는 프록시가 메서드 호출을 가로채고 호출 전후에 추가 작업을 수행하는 방식으로 동작합니다.
CGLIB 프록시 예시
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
public class CglibProxyDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyClass.class);
enhancer.setCallback(new MyMethodInterceptor());
MyClass myClassProxy = (MyClass) enhancer.create();
myClassProxy.doSomething();
}
}
class MyClass {
public void doSomething() {
System.out.println("Doing something in MyClass");
}
}
class MyMethodInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before method call");
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method call");
return result;
}
}
위 코드는 CGLIB을 사용해 `MyClass`클래스의 프록시 객체를 생성하고, 메서드 호출 전후에 부가 작업을 추가합니다.
프록시의 한계와 고려 사항
- private 메서드에는 적용되지 않음.
스프링 프록시는 public 메서드에 대해서만 동작합니다. 따라서, `@Async`나 `@Transactional`과 같은 어노테이션을 private 메서드에 적용해도 효과가 없습니다. - 자체 클래스 내부 호출 시 동작하지 않음.
프록시가 같은 클래스의 다른 메서드를 호출할 때는 AOP나 트랜잭션 등이 적용되지 않습니다.
이는 프록시 객체가 아닌 실제 객체가 호출되기 때문입니다. 해결 방법으로는 해당 메서드를 별도의 빈으로 분리하거나, 프록시를 주입하여 사용하는 방식이 있습니다. - 오버헤드
프록시로 인해 메서드호출 시 일부 성능 오버헤드가 발생할 수 있습니다. 그러나 이는 일반적으로 무시할 수 있는 수준이며, 대규모 트랜잭션이나 비동기 작업에서 효율성을 높일 수 있습니다.
💡스프링의 프록시 패턴은 핵심 로직을 수정하지 않고도 부가 기능을 유연하게 추가할 수 있는 강력한 방법입니다. 이를 통해 개발자는 트랜잭션, 비동기 처리, 보안 기능을 손쉽게 적용할 수 있으며, 코드의 유지보수성과 확장성을 높일 수 있습니다.