본문 바로가기

BackEnd

[Java, Spring] 프록시, 프록시 패턴, 데코레이터 패턴

반응형

해당 게시물은 김영한 강사님의 스프링 핵심원리 고급편 을 학습 후 정리한 내용이다.

 

스프링 핵심 원리 - 고급편 - 인프런 | 강의

스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

탬플릿 메서드 패턴 : 변하지 않는 로직(부가 기능)과 변하는 로직(핵심 기능)의 분리

템플릿 메서드 디자인 패턴의 목적은 다음과 같습니다.
"작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기합니다.
템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의할 수 있습니다.
[GOF]
프로그래밍에서 콜백(callback) 또는 콜애프터 함수(call-after function)는
다른 코드의 인수로서 넘겨주는 실행 가능한 코드를 말한다.
콜백을 넘겨받는 코드는 이 콜백을 필요에 따라 즉시 실행할 수도 있고, 아니면 나중에 실행할 수도 있다.
(위키백과)

탬플릿 메서드 패턴, 탬플릿 콜백 패턴을 사용하면,

변하는 부분의 로직을 익명 클래스 또는 전략으로 구현하고,

변하지 않는 부분의 로직을 추상 클래스로 구현하여, 두 코드를 분리해 결합도를 낮추고 재사용성을 낮출 수 있었다.

하지만 이 방법의 문제점은, 로직을 구현하는 쪽에서 전략 패턴의 컨텍스트(탬플릿)를 사용해야 한다는 것이다.

이 컨텍스트를 숨길 수 있는 방법은 프록시를 사용하는 것이다.

프록시

클라이언트와 서버

클라이언트는 서버에 필요한 것을 요청하는 모든 것이고, 서버는 클라이언트의 요청을 처리하는 모든 것이다.

이 개념을 우리가 익숙한 컴퓨터 네트워크에 도입하면 클라이언트는 웹 브라우저가 되고, 요청을 처리하는 서버는 웹 서버가 된다.

이 개념을 객체에 도입하면, 요청하는 객체는 클라이언트가 되고, 요청을 처리하는 객체는 서버가 된다.

직접 호출과 간접 호출

클라이언트가 서버를 직접 호출하면 직접 호출이다.

클라이언트는 어떤 대리자를 통해 결과를 서버에 간접 요청할 수 있다.

어떤 일을 직접 할 수도 있지만, 시킬 수도 있는 것이다.

대리자를 영어로(Proxy)라 한다.

이 프록시를 이용하면 클라이언트는 중간에 몇 단계의 업무가 있는지 전혀 몰라도 된다.

(접근제어, 캐싱, 부가기능 추가, 프록시 체인)

프록시 체인

아무 객체나 프록시가 될 수 있는것은 아니다.

클라이언트는 서버에게 요청하던, 프록시에 요청하던 동일한 인터페이스를 사용할 수 있어야 한다.(뭔지 몰라야 한다.)

즉, 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 한다.

클래스 의존관계 

클라이언트는 서버 인터페이스( ServerInterface )에만 의존한다. 그리고 서버와 프록시가 같은 인터페이스를 사용한다

서버와 프록시가 같은 인터페이스 사용

런타임 객체 의존관계

런타임(애플리케이션 실행 시점)에 클라이언트 객체에 DI를 사용하여,
Client -> Server 에서 Client -> Proxy 로 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 된다.
클라이언트 입장에서는 변경 사실 조차 모른다.
DI를 사용하면 클라이언트 코드의 변경 없이 유연하게 프록시를 주입할 수 있다.

프록시 런타임 객체 의존관계

프록시의 주요 기능

프록시 객체가 중간에 있으면 크게 접근 제어부가 기능 추가를 수행할 수 있다. (2가지 기능)

  • 접근 제어
    • 권한에 따른 접근 차단
    • 캐싱
    • 지연 로딩
  • 부가 기능 추가
    • 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
      • ) 요청 값이나, 응답 값을 중간에 변형한다.
      • ) 실행 시간을 측정해서 추가 로그를 남긴다.

프록시를 사용하는 GOF 디자인 패턴 : 프록시 페턴과 데코레이터 패턴

프록시 패턴과 데코레이터 패턴은 그 모양이 거의 같고, 상황에 따라 정말 똑같을 때도 있다.
둘다 프록시를 사용하지만, 의도가 다르다는 점이 핵심이다.
디자인 패턴에서 중요한 것은 해당 패턴의 겉모양이 아니라 그 패턴을 만든 의도가 더 중요하다. 따라서 의도에 따라 패턴을 구분한다.

  • 프록시 패턴: 접근 제어가 목적
  • 데코레이터 패턴: 새로운 기능 추가가 목적
데코레이터 패턴

잘 생각해보면 꾸며주는 역할을 하는 Decorator 들은 스스로 존재할 수 없다. 항상 꾸며줄 대상이 있어야 한다.
따라서 내부에 호출 대상인 component 를 가지고 있어야 한다. 그리고 component 를 항상 호출해야 한다.
이런 중복을 제거하기 위해 component 를 속성으로 가지고 있는 Decorator 라는 추상 클래스를 만드는 방법도 고민할 수 있다. 

GOF 데코레이터 패턴

 

이렇게 하면 추가로 클래스 다이어그램에서 어떤 것이 실제 컴포넌트 인지, 데코레이터인지 명확하게 구분할 수 있다.

여기까지 고민한 것이 바로 GOF에서 설명하는 데코레이터 패턴의 기본 예제이다.

인터페이스 기반 프록시

이전 게시글의 클래스 의존 관계 및 런타임 의존 관계는 다음과 같다.

https://itchallenger.tistory.com/960

 

[Java, Spring] 쓰레드 로컬과 전략 패턴, 탬플릿 메서드 패턴

해당 게시물은 김영한 강사님의 스프링 핵심원리 고급편 을 학습 후 정리한 내용이다. 스프링 핵심 원리 - 고급편 - 인프런 | 강의 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프

itchallenger.tistory.com

여기에 로그 추적용 프록시를 추가하면 다음과 같다.

레포지토리는 생략. 유사함
V1 프록시 런타임 객체 의존 관계

그리고 애플리케이션 실행 시점에 프록시를 사용하도록 의존 관계를 주입한다.

 

아래와 같이 Service를 구현하는 프록시 객체를 만든다.

이는 실제 사용할 OrderServiceImpl과 별개로, OrderServiceImpl에 핵심 로직을 구현한다.

여기에는 부가로직만 넣는다.

@RequiredArgsConstructor
public class OrderServiceInterfaceProxy implements OrderServiceV1 {
    private final OrderServiceV1 target;
    private final LogTrace logTrace;

    @Override
    public void orderItem(String itemId) {
        TraceStatus status = null;
        try {
            status = logTrace.begin("OrderService.orderItem()"); // target 호출
            target.orderItem(itemId);
            logTrace.end(status);
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

아래와 같이 프록시를 빈으로 등록한다.
프록시를 실제 스프링 빈 대신 등록한다. 실제 객체는 스프링 빈으로 등록하지 않는다.

  • 프록시는 내부에 실제 객체를 참조하고 있다. 예를 들어서 OrderServiceInterfaceProxy 는 내부에 실제 대상 객체인 OrderServiceV1Impl 을 가지고 있다.

정리하면 다음과 같은 의존 관계를 가지고 있다.

  • proxy -> target
  • orderServiceInterfaceProxy -> orderServiceV1Impl

스프링 빈으로 실제 객체 대신에 프록시 객체를 등록했기 때문에, 스프링 빈을 주입 받으면 실제 객체 대신에 프록시 객체가 주입된다.

실제 객체가 스프링 빈으로 등록되지 않는다고 해서 사라지는 것은 아니다.

프록시 객체가 실제 객체를 참조하기 때문에 프록시를 통해서 실제 객체를 호출할 수 있다. 쉽게 이야기해서 프록시 객체 안에 실제 객체가 있는 것이다.

@Configuration
public class InterfaceProxyConfig {
    @Bean
    public OrderControllerV1 orderController(LogTrace logTrace) {
        OrderControllerV1Impl controllerImpl = new OrderControllerV1Impl(orderService(logTrace));
        return new OrderControllerInterfaceProxy(controllerImpl, logTrace);
    }

    @Bean
    public OrderServiceV1 orderService(LogTrace logTrace) {
        OrderServiceV1Impl serviceImpl = new OrderServiceV1Impl(orderRepository(logTrace));
        return new OrderServiceInterfaceProxy(serviceImpl, logTrace);
    }

    @Bean
    public OrderRepositoryV1 orderRepository(LogTrace logTrace) {
        OrderRepositoryV1Impl repositoryImpl = new OrderRepositoryV1Impl();
        return new OrderRepositoryInterfaceProxy(repositoryImpl, logTrace);
    }
}

프록시 적용 전 스프링 컨테이너
프록시 적용 후 스프링 컨테이너 - 실제 객체를 참조하는 프록시 객체가 빈으로 등록

프록시 객체는 스프링 컨테이너가 관리하고 자바 힙 메모리에도 올라간다.
반면에 실제 객체는 자바 힙 메모리에는 올라가지만 스프링 컨테이너가 관리하지는 않는다.

구체 클래스 기반 프록시

지금까지는 인터페이스가 있는 클래스에 프록시를 적용하였다.
구체 클래스 기반 프록시는 클래스 기반 상속을 이용해 구현한다는것 빼고는 거의 다 비슷하다.

자바 언어에서 다형성은 인터페이스나 클래스를 구분하지 않고 모두 적용된다.
해당 타입과 그 타입의 하위 타입은 모두 다형성의 대상이 된다.
public class OrderRepositoryConcreteProxy extends OrderRepositoryV2 {
    private final OrderRepositoryV2 target;
    private final LogTrace logTrace;

    public OrderRepositoryConcreteProxy(OrderRepositoryV2 target, LogTrace logTrace) {
        this.target = target;
        this.logTrace = logTrace;
    }

    @Override
    public void save(String itemId) {
        TraceStatus status = null;
        try {
            status = logTrace.begin("OrderRepository.save()"); // target 호출
            target.save(itemId);
            logTrace.end(status);
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

클래스 기반 프록시의 단점

  • super(null) :
    • 자바 기본 문법에 의해 자식 클래스를 생성할 때는 항상 super() 로 부모 클래스의 생성자를 호출해야 한다.
    • 이 부분을 생략하면 기본 생성자가 호출된다.
    • 부모 클래스인 OrderServiceV2 는 기본 생성자가 없고, 생성자에서 파라미터 1개를 필수로 받는다.
      • 따라서 파라미터를 넣어서 super(..) 를 호출해야 한다.
      • 프록시는 부모 객체의 기능을 사용하지 않기 때문에 super(null) 을 입력해도 된다.

인터페이스 기반 프록시는 이런 고민을 하지 않아도 된다. 

빈 등록도 똑같이 수행한다.

@Configuration
public class ConcreteProxyConfig {
    @Bean
    public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
        OrderControllerV2 controllerImpl = new OrderControllerV2(orderServiceV2(logTrace));
        return new OrderControllerConcreteProxy(controllerImpl, logTrace);
    }

    @Bean
    public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
        OrderServiceV2 serviceImpl = new OrderServiceV2(orderRepositoryV2(logTrace));
        return new OrderServiceConcreteProxy(serviceImpl, logTrace);
    }

    @Bean
    public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
        OrderRepositoryV2 repositoryImpl = new OrderRepositoryV2();
        return new OrderRepositoryConcreteProxy(repositoryImpl, logTrace);
    }
}

정리

탬플릿 메서드 패턴을 통해 핵심 로직과 부가기능을 분리하여 독립적으로 유지보수 할 수 있도록 했다.

하지만 문제점은 핵심 로직에서 탬플릿을 사용하는 코드를 매번 작성해야 한다는 것이었다.

이를 해결하기 위해, 이 부분을 별도의 코드(프록시)로 분리한 뒤, 프록시의 존재 여부와 무관하게 서버를 사용할 수 있도록 했다.

프록시를 사용한 덕분에 원본 코드를 전혀 변경하지 않고, V1, V2 애플리케이션에 LogTrace 기능을 적용할 수 있었다.
  • 인터페이스가 없어도 클래스 기반으로 프록시를 생성할 수 있다.
  • 클래스 기반 프록시는 해당 클래스에만 적용할 수 있다. 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
  • 클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
  • 부모 클래스의 생성자를 호출해야 한다.(앞서 본 예제)
  • 클래스에 final 키워드가 붙으면 상속이 불가능하다.
  • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.

이렇게 보면 인터페이스 기반의 프록시가 더 좋아보인다.
맞다. 인터페이스 기반의 프록시는 상속이라는 제약에서 자유롭다.
프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다.
인터페이스 기반 프록시의 단점은 인터페이스가 필요하다는 그 자체이다.

인터페이스가 없으면 인터페이스 기반 프록시를 만들 수 없다.
인터페이스 기반 프록시는 캐스팅 관련해서 단점이 있다. (구체 클래스로의 캐스팅 불가)

이론적으로는 모든 객체에 인터페이스를 도입해서 역할과 구현을 나누는 것이 좋다.

이렇게 하면 역할과 구현을 나누어서 구현체를 매우 편리하게 변경할 수 있다.

 

하지만 실제로는 구현을 거의 변경할 일이 없는 클래스도 많다.

인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적이다.

구현을 변경할 가능성이 거의 없는 코드에 무작정 인터페이스를 사용하는 것은 번거롭고 그렇게 실용적이지 않다.

이런곳에는 실용적인 관점에서 인터페이스를 사용하지 않고 구체 클래스를 바로 사용하는 것이 좋다 생각한다.

물론 인터페이스를 도입하는 다양한 이유가 있다. 여기서 핵심은 인터페이스가 항상 필요하지는 않다는 것이다.

이제 생각해볼 문제점은, 프록시를 적용할 클래스가 100개면 프록시 클래스도 100개를 만들어야 한다는 것이다.
이를 해결하기 위한 기술이 동적 프록시 기술이다.

반응형