구조 패턴(Structure Pattern)은 구조가 복잡한 시스템 개발에 도움을 줄 수 있다.
그 중 프록시 패턴은 접근이 어려운 객체에 접근할 수 있도록 인터페이스 역할을 수행 하고, 데코레이터 패턴은 클래스에 기능을 추가하기 위해 다른 객체를 덧붙이는 형태이다.
실무에서는 스프링 빈으로 등록할 클래스에 인터페이스가 있는 경우, 없는 경우, 스프링 빈을 수동으로 직접 등록하는 경우, 컴포넌트 스캔으로 자동 등록하는 경우가 있다. 이렇게 다양한 케이스에서 프록시를 어떻게 적용하는지 알아본다.
즉, 다음과 같은 요구사항이 추가되었다고 생각해보자.
- 원본 코드를 전혀 수정하지 않고, 로그 추적기를 사용한다.
- 특정 메서드는 보안상 로그를 출력하지 않는다.
- 위의 예와 같은 다양한 케이스에 기능을 적용한다.
대리자를 영어로 proxy라 한다.
클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 대리자를 통해 간접적으로 서버에 요청할 수 있다.
proxy의 장점
대리자가 중간에서 여러가지 일을 할 수 있다.
- 접근 제어, 캐싱
- 부가 기능 추가
- 프록시 체인. 이때, 클라이언트는 프록시를 통해 요청하고 이 후 과정은 모른다.
서버와 프록시 같은 인터페이스를 사용한다.
객체 세상에서 프록시가 되려면 클라이언트는 서버에게 요청을 한 것인지, 프록시에게 요청을 한 것인지 조차 몰라야 한다.
즉, 서버와 프록시는 같은 인터페이스를 사용해야 하고, DI를 사용해서 대체 가능해야 한다는 것이다. 그래야 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있다.
따라서 DI를 사용해 클라이언트의 코드 변경없이 유연하게 프록시를 주입할 수 있다.
proxy의 주요 기능
프록시 객체가 중간에 있으면 크게 접근 제어와 부가 기능 추가를 수행할 수 있다.
- 접근 제어
- 권한에 따른 접근 차단
- 캐싱
- 지연 로딩
- 부가 기능 추가
- 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
- 요청 값, 응답 값을 중간에 변형
- 로그 추적기 추가
- 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
Proxy Pattern, Decorator Pattern
둘 다 프록시를 사용하는 방법이지만 GOF 디자인 패턴에서는 이 둘을 의도(intent)에 따라 Proxy Pattern과 Decorator Pattern으로 구분한다.
- 프록시 패턴 : 접근 제어가 목적
- 데코레이터 패턴 : 새로운 기능 추가가 목적
Proxy Pattern
프록시 패턴의 핵심은 실제 객체 코드와 클라이언트 코드를 변경하지 않고, 프록시 패턴을 도입해서 접근 제어 한다는 점이다.
또한 클라이언트의 코드 변경 없이 프록시 객체를 넣고 뺄 수 있기 때문에, 실제 클라이언트의 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못한다는 것이다.
Decorator Pattern
인터페이스 기반 구조에 프록시 적용
즉, 프록시 적용 전에는 스프링 컨테이너에 실제 객체가 스프링 빈으로 등록되고, 프록시 적용 후에는 스프링 컨테이너에 프록시 객체가 등록되는 것이다. 따라서 스프링 컨테이너는 실제 객체가 아니라 프록시 객체를 스프링 빈으로 관리한다.
정리하면 이제부터는 실제 객체는 스프링 컨테이너와는 상관이 없는 것이다. 다만 실제 객체는 프록시 객체를 통해 참조될 뿐이다.
그러므로 프록시 객체는 스프링 컨테이너가 관리하고, 자바 힙 메모리에도 올라간다. 반면에 실제 객체는 자바 힙 메모리에는 올라가지만 스프링 컨테이너가 관리하지는 않는 것이다.
인터페이스가 없어도 프록시가 가능하다.
자바의 다형성은 인터페이스나 클래스를 구분하지 않고 모두 적용된다. 해당 타입과 그 타입의 하위 타입은 모두 다형성의 대상이 된다.
즉, ConcreteLogic이라는 참조 타입이 있을 때, concreteLogic이라는 참조 변수는 본인과 같은 타입인 concreteLogic 객체를 참조할 수 있고, 자식 타입인 timeProxy 객체를 참조할 수도 있다는 것이다.
클래스 기반 프록시의 단점
자바 기본 문법에서는 자식 클래스를 생성할 때는 항상 super()로 부모 클래스의 생성자를 호출해줘야 한다. 그리고 아래 예에서는 부모인 OrderService 클래스에서 기본 생성자가 없고, 파라미터 1개를 필수로 받는 생성자가 생성되어 있다. 따라서 자식 클래스는 파라미터를 넣어서 super()를 호출해줘야 하는 것이다.
하지만 프록시는 부모 객체의 기능을 사용하지는 않기 때문에, super(null)을 입력해도 된다.
public class OrderServiceConcreteProxy extends OrderService {
private final OrderService target;
private final LogTrace logTrace;
public OrderServiceConcreteProxy(OrderService target, LogTrace logTrace) {
super(null);
this.target = target;
this.logTrace = logTrace;
}
@Override
public void orderItem(String itemId) {
TraceStatus status = null;
try {
status = logTrace.begin("OrderService.orderItem()");
target.orderItem(itemId);
logTrace.end(status);
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
이를 포함한 클래스 기반 프록시의 단점을 나열해보면 다음과 같다.
- 클래스 기반 프록시는 해당 클래스에만 적용할 수 있다.
- 클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
- 부모 클래스의 생성자를 호출해야 한다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩할 수 없다.
반면에 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있으며, 이론적으로도 모든 객체에 인터페이스를 도입해서 역할과 구현을 나누는 것이 좋다.
클래스 기반 프록시와 인터페이스 기반 프록시 둘 다 사용할 줄 알아야 한다.
하지만 현실적으로 구현을 변경할 가능성이 거의없는 코드에 무작정 인터페이스를 사용하는 것보다는 구체 클래스를 바로 바로 사용하는 것이 좋을때도 있다.
'디자인 패턴 > GOF' 카테고리의 다른 글
Template Method Pattern 적용 (0) | 2023.09.22 |
---|---|
Strategy Pattern (0) | 2023.06.10 |
Template Method Pattern (0) | 2023.06.10 |