먼저 프록시(Proxy)란?
프록시란, 주로 실제 객체 앞에서 실행되는 대리 객체처럼 이해하면 된다.
다음 코드를 보면, RealService 객체에 대한 지연 초기화(Lazy Initialization)를 구현하고 있는 Proxy를 확인할 수 있다. 이는 실제 객체의 값을 캐싱하는 역할을 한다.
package proxy;
import lombok.extern.slf4j.Slf4j;
public class ProxyTest {
public static void main(String[] args) {
CertainService service = new CacheProxy();
System.out.println(service.call());
System.out.println(service.call());
}
}
@Slf4j
class CacheProxy implements CertainService {
private RealService service;
private String cachedResult;
@Override
public String call() {
if (cachedResult == null) {
if (service == null) {
service = new RealService();
}
cachedResult = service.call();
}
else {
log.info("Returning cached result.");
}
return cachedResult;
}
}
@Slf4j
class RealService implements CertainService {
@Override
public String call() {
log.info("HELLO, I'M REAL SERVICE.");
return "R E A L !! ";
}
}
interface CertainService {
String call();
}
프록시는 객체지향의 SOLID 원칙 중, SRP 과 OCP 를 잘 따를 수 있는 구조이다.
SRP, 즉 Single Responsibility Principle, 단일 책임원칙.
프록시에는 다양한 역할이 있다. 내가 상단에 작성한 코드는 단순히 값을 캐싱하는 용도로 작성했지만, 캐싱 용도 이외에도 다른 부가기능들에 대해서도 생성할 수가 있다. 또한 프록시는 연쇄적으로 연결하여 작성하는 것도 가능하다.
OCP, 즉 Open-Closed Principle, 개방-폐쇄 원칙.
개인적으로 내가 생각하는 프록시의 가장 강력한 존재 이유이다. 프록시는 기존 객체를 변경하지 않고 기능을 추가한다. 즉, 확장에는 열려있고 수정에는 닫혀있다.
그렇다. 프록시는 다양하게 확장적으로 존재할 수 있다.
그렇기 때문에 동적 프록시가 필요한 것이다. 프록시들은 정말 다양하고 많이 존재할 수 있기 때문이다.
동적 프록시를 사용하면 컴파일 시점에 프록시 객체를 만들지 않아도 된다.
동적 프록시는, 인터페이스만 알고 있다면, 런타임 시점에 다양한 프록시 로직을 적용할 수 있다.
InvocationHandler, 동적 프록시를 생성하는 방법.
InvocationHandler는 java.lang.reflect 패키지에 포함되어 있는 인터페이스이다.
그렇다. Java 리플랙션의 라이브러리에 포함되어 있다. 이 말의 뜻은, 메타데이터를 이용한다는 뜻이다.
(혹시 Java 리플랙션이 뭔지 모른다면 다음 포스팅을 참고)
https://jinn-o.tistory.com/292
Java 리플랙션(Reflection) 에 대하여
어떤 로직은 런타임 시점에 실행되어야 한다.컴파일과 동시에 확정되어 버리는 코드는, 가끔은 유연성과 확장성 관점에서 성능이 떨어질 수 있다. 일부 애플리케이션에서는 외부 설정 파일이나
jinn-o.tistory.com
package proxy;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyTest {
public static void main(String[] args) {
// 실제 서비스
CertainService realService = new RealService();
// 동적 프록시 - 캐스팅 해주어야함
CertainService proxyInstance = (CertainService) Proxy.newProxyInstance(
realService.getClass().getClassLoader(),
new Class<?>[]{CertainService.class},
new CacheProxy(realService)
);
proxyInstance.call();
}
}
@Slf4j
class CacheProxy implements InvocationHandler {
private final Object target;
public CacheProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("CacheProxy 실행");
long startTime = System.currentTimeMillis();
// 메인 로직
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("CacheProxy 종료 resultTime={}", resultTime);
return result;
}
}
@Slf4j
class RealService implements CertainService {
@Override
public String call() {
log.info("HELLO, I'M REAL SERVICE.");
return "R E A L !! ";
}
}
interface CertainService {
String call();
}
InvocationHandler 인터페이스는 invoke(...) 메서드를 통해서 로직을 가로챈다.
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
- proxy
- 현재 호출을 처리하는 프록시 객체 자체
- Proxy.newProxyInstance를 통해 생성된 객체
- 일반적으로 이 프록시 객체는 invoke 메서드 내에서 직접 사용되지 않지만, 재귀 호출이나 프록시 객체에 대한 참조가 필요한 경우 사용
- method: 호출된 메서드를 나타내는 Method 객체
- 프록시 객체에서 호출된 메서드를 나타내는 리플렉션 객체로, 호출된 메서드의 이름, 매개변수 타입, 리턴 타입 등의 메타데이터를 가지고 있다. 이를 통해, 어떤 메서드가 호출되었는지 확인하고, 특정 메서드에 대해 조건을 추가하거나 동작을 달리할 수 있다.
- method.invoke(target, args) 형태로 사용하면 프록시를 통해 실제 객체의 메서드를 호출
- args: 호출된 메서드의 인자 값들을 포함한 배열
- 호출된 메서드에 전달된 실제 인자 값들을 배열 형태로 받는다. (null 가능)
- 이를 활용해 인자 값에 따라 다른 로직을 수행하거나, 인자 값 자체를 수정하여 실제 메서드에 전달할 수 있다.
Proxy.newProxyIntance(...) 메서드에 대하여
프록시 객체를 생성하는 JDK 메서드이다. 이 메서드는 클래스 로더, 인터페이스, 호출 핸들러 의 세가지 파라미터를 받아 동적 프록시를 생성한다.
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
- ClassLoader loader:
- 프록시 클래스를 로드하는 데 사용될 클래스 로더
- 일반적으로 대상 객체의 클래스 로더(target.getClass().getClassLoader())를 전달
- 프록시 클래스가 어떤 클래스나 인터페이스의 이름을 참조할 때, 해당 클래스나 인터페이스가 어디에 있는지 이 클래스 로더를 통해 알아낸다.
- Class<?>[] interfaces:
- 프록시가 구현할 인터페이스 목록
- Proxy.newProxyInstance는 이 인터페이스 목록을 기반으로 프록시 객체의 구조를 정의한다. 즉, 프록시 객체는 여기서 지정된 인터페이스들에 선언된 메서드를 구현하는 것이다.
- Class<?>[] 배열이므로, 여러 인터페이스를 동시에 지정할 수 있다. 단, 프록시는 인터페이스를 기반으로 생성되므로 구현할 인터페이스가 반드시 필요(클래스를 직접 상속할 수는 없습니다).
- 예를 들어, new Class<?>[]{ServiceA.class, SomeOtherInterface.class}처럼 전달하면 프록시가 ServiceA와 SomeOtherInterface를 동시에 구현
- InvocationHandler h:
- 프록시 메서드 호출을 처리하는 핸들러 객체로, InvocationHandler 인터페이스를 구현한 객체
- 프록시의 모든 메서드 호출이 InvocationHandler의 invoke() 메서드로 전달되며, 이 메서드에서 메서드 호출 전후로 로직을 추가하거나 실제 메서드를 호출하는 작업을 처리
- 이 핸들러를 통해 프록시 메서드의 호출 흐름을 제어하고, 추가적인 기능(로깅, 트랜잭션 관리 등)을 수행
핵심은, 동적 프록시를 이용하면, 프록시들을 미리 구체화시키지 않아도 된다는 것이다.
스프링에서는 동적 프록시를 통해 AOP 기능을 구현하여, 로깅, 트랜잭션, 보안 등을 런타임에 자동으로 주입하고 있다. 이로써 개발자는 비즈니스 로직에 집중할 수 있으며, 프록시 로직을 미리 구현하지 않아도 다양한 부가 기능을 필요에 따라 적용할 수 있는 것이다.
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
Comparable VS Comparator (0) | 2025.04.10 |
---|---|
Java의 객체 참조 방식과 call by value + reference semantics (0) | 2025.04.10 |
Java 리플랙션(Reflection) 에 대하여 (0) | 2024.10.25 |
BufferedReader 와 Scanner 의 차이 (3) | 2024.10.05 |
Stack을 구현할 때 ArrayDeque를 사용하는 이유 (0) | 2024.05.06 |