프록시
객체에서 프록시가 되려면, 클라이언트는 서버에게 요청을 한 것인지 프록시에게 요청을 한 것인지 몰라야한다.
서버와 프록시는 같은 인터페이스를 사용해야하고, 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야한다.
프록시의 주요 기능
1. 접근제어
- 권한에 따른 접근 차단
- 캐싱
- 지연로딩
2. 부가 기능 추가
- 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
- 예) 요청값이나 응답값을 중간에 변경한다.
- 예) 실행시간을 측정해서 추가 로그를 남긴다.
GOF 디자인패턴
프록시 패턴 : 접근 제어가 목적
데코레이터 패턴 : 새로운 기능 추가가 목적
둘 다 프록시를 사용하지만 의도만 다름
프록시 패턴
- 목적 : 접근제어 (캐시도 접근제어 중 하나이다.)
클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근제어를 하는 것이 프록시 패턴의 핵심이다. 실제 클라이언트 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못한다.
데코레이터 패턴
- 목적 : 새로운 기능 추가
인터페이스 기반 프록시 vs 클래스 기반 프록시
- 인터페이스가 없어도 클래스 기반으로 프록시를 생성할 수 있다.
- 클래스 기반 프록시는 해당 클래스에만 적용할 수 있다. 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
- 클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
- 부모 클래스의 생성자를 호출해야 한다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
인터페이스 기반의 프록시는 상속이라는 제약으로 부터 더 자유로워서 더 좋아보이지만, 인터페이스가 필요하다는 그 자체가 단점이다. (캐스팅 관련)
이론적으로는 모든 객체에 인터페이스를 도입해서 역할과 구현을 나누는 것이 좋다. 이렇게 하면 역할과 구현을 나누어서 구현체를 매우 편리하게 변경할 수 있다. 하지만 실제로는 구현을 거의 변경할 일이 없는 클래스도 많다.
인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을때 효과적인데, 구현을 변경할 가능성이 거의 없는 코드에 무작정 인터페이스를 사용하는 것은 번거롭고 그렇게 실용적이지 않다.
동적 프록시
리플렉션
void reflection() throws Exception {
//클래스 정보
Class classHello = Class.forName("hello.proxy.jdkdynamic.RelfectionTest$Hello");
Hello target = new Hello();
Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
private void dynamicCall(Method method, Object target) throws Exception {
log.info("start");
Object result = method.invoke(target);
log.info("result={}", result);
}
정적인 target.callA();와 target.callB()코드를 리플렉션을 사용해서 Method라는 메타정보로 추상화했다. 덕분에 공통로직을 만들었다.
애플리케이션을 동적으로 유연하게 만들었지만, 런타임에 동작하기 때문에 컴파일 시점에 리플렉션 관련 오류는 잡을 수 없다.
따라서 리플렉션은 일반적으로 사용하지않는 것을 권장하기 때문에 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야한다.