Spring/Spring Core Advanced

프록시 패턴과 데코레이터 패턴

민철킹 2021. 11. 8. 21:45

2021.11.01 - [Spring/Spring Core Advanced] - 템플릿 메서드 패턴과 콜백 패턴

 

템플릿 메서드 패턴과 콜백 패턴

2021.10.29 - [Spring/Spring Core Advanced] - ThreadLocal ThreadLocal 2021.10.28 - [Spring/Spring Core Advanced] - 로그 추적기 로그 추적기 애플리케이션이 커질수록 모니터링과 운영이 중요해진다. 어떤..

minchul-son.tistory.com

앞서 템플릿 메서드 패턴, 전략 패턴, 콜백 패턴을 로그 추적기에 적용시켜보며 공통의 변하지 않는 코드와 변하는 코드를 분리해 로직을 좀 더 간단하게 변경해보았다.

 

이 방법 또한 원본 코드에 코드 수정이 많이 필요하다.

 

기존 애플리케이션에 위의 디자인 패턴을 사용한 로그 추적기를 적용시키려면?

 

로그를 남기는 모든 곳에서 해당 로직을 추가해주는 수많은 코드 수정이 일어나야한다.

 


로그 추적기 새로운 요구사항
  • 원본 코드를 전혀 수정하지 않고 로그 추적기를 적용하라
  • 특정 메서드는 로그를 출력하지 않는 기능을 추가하라
    • 보안상의 이유
  • 다양한 case에 적용될 수 있어야한다.
    • v1 - 인터페이스가 있는 구현 클래스(스프링 빈 수동 등록)
    • v2 - 인터페이스가 없는 구현 클래스(스프링 빈 수동 등록)
    • v3 - 컴포넌트 스캔(스프링빈 자동 등록)

 

 

어떻게하면 원본 코드의 수정없이 로그 추적기를 도입할 수 있을까?😕

 


프록시

 

넓은 의미에서 클라이언트와 서버를 생각해보자.

 

클라이언트는 의뢰인이고, 서버는 서비스를 제공하는 사람이 된다.

 

이를 객체에다 대입한다면?

 

클라이언트는 요청하는 객체이고 서버는 요청을 처리하는 객체이다.

 

이렇게 클라이언트가 서버에게 직접 요청을 전달하는 것을 직접 호출이라고 한다.

 

 

위와 같이 대리자인 프록시를 통해 요청을 대신 전달하는 것을 간접 호출이라고 한다.

 

 

또한, 중간에 프록시를 여러개 두는 것을 프록시 체인이라고 한다.

 

여기서 중요한 점은 클라이언트는 프록시 1에게 요청을 전달했을 뿐 그 뒤에 프록시가 몇개가 있는지 알지 못한다.

 

단지 요청한 것이 제대로 전달되기만 하면 되는 것이다.

 


객체에서 프록시

 

대체 가능

객체에서 프록시가 되려면 클라이언트는 요청을 서버에게 전달한 것인지 프록시에게 요청을 한 것인지조차 몰라야한다.

 

JPA에서 지연 로딩을 사용하는 객체는 프록시 객체로 가져와지고 실제 그 객체를 참조하려할 때 진짜 객체가 가져와진다.

 

이 과정에서 사용하는 개발자의 코드는 아무런 변경없이 동작한다.

 

즉, 서버와 프록시는 같은 인터페이스를 사용해야한다.

 

클라이언트의 서버 객체를 프록시 객체로 변경하여도 나머지 코드의 변경없이 정상 동작해야한다!

 

 

DI를 사용하여 유연하게 프록시를 주입할 수 있다.

 

주요 기능

접근 제어

  • 권한에 따른 접근 차단
  • 캐싱
  • 지연 로딩

부가 기능 추가

  • 원래 서버가 제공하는 기능에 더해서 부가 기능 수행
    • ex: 요청, 응답 값을 중간에서 변경, 실행 시간 측정해서 로그를 남김

 


 프록시 패턴, 데코레이터 패턴

 

두 패턴 모두 프록시를 사용하지만, GoF Design Pattern에서는 이를 의도에 따라 나누고 있다.

 

프록시 패턴 ==> 접근 제어가 목적

 

데코레이터 패턴 ==> 새로운 기능 추가가 목적

 

 


 

프록시 패턴

 

프록시 패턴을 코드로 구현해보자.

 

접근 제어의 한 종류인 캐싱을 하는 프록시를 예시로 살펴보자.

 

캐시 프록시를 사용 X

인터페이스를 정의하고 이를 구현한 실제 객체를 만들었다.

 

클라이언트는 이 실제 객체의 operation 메서드를 호출해 데이터를 조회한다고 가정한다.

 

따라서, 클라이언트는 Subject를 생성자 주입을 통해 주입받고 내부의 execute 메소드를 호출하면 실제 객체를 조회한다.

사용은 이런식으로 클라이언트에 실제 객체를 주입받아 사용한다.

 

실제 객체에서 Thread.sleep을 통해 1초의 지연을 주었으므로 해당 테스트가 완료되는데는 3초가 걸린다.

 

 

캐시 프록시 사용 O

동일한 데이터를 조회하는데 동일한 작업을 수행하기 때문에 이러한 시간을 줄이기 위해 사용하는 방법이 바로 캐시이다.

 

자주 사용하는 데이터를 저장해두고 필요할 때 바로 반환하기 때문에 빠르다는 장점이 있다.

 

위의 예시에 프록시 패턴을 통해 캐시 프록시를 적용시켜보자.

 

앞서 이야기했듯이 프록시 패턴에 사용되는 프록시는 서버와 같은 인터페이스를 구현하고 있다.

 

따라서 클라이언트의 코드 변경없이 정상적으로 동작해야한다.

서버와 같은 인터페이스를 구현하는데 이제는 프록시 객체가 실제 객체를 주입받는다.

 

클라이언트는 프록시 객체를 주입받게 되는데 모두 같은 인터페이스를 구현하고 있기 때문에 가능하다.

 

값을 저장할 cacheValue가 null(최초 호출)이라면 실제 객체에 접근해 데이터를 가져오고 그렇지 않다면 바로 cacheValue를 반환하는 방식으로 매우 간단하게 캐시 기능을 사용해보았다.

실제 객체를 한번만 호출하기 때문에 시간이 매우 단축된 것을 확인할 수 있다.

 

캐시 프록시를 적용하기 전에는 client가 실제 객체를 참조했지만 캐시 프록시를 적용하면서

 

"client -> cacheProxy ->  실제 객체" 런타임 객체 의존 관계가 완성되었다.

 

클라이언트는 인터페이스를 의존하고 있기 때문에 프록시가 주입되는지 실제 객체가 주입되는지를 모르고 아무런 코드의 수정도 필요없다.

 

이게 바로 프록시 패턴!!

 


데코레이터 패턴

 

새로운 기능 추가를 목적으로하는 데코레이터 패턴을 사용해보자.

 

기본적으로 구조는 위에서 구현한 프록시 패턴과 동일하다.

 

Component 인터페이스와 이를 구현한 클래스를 정의한다.

 

마찬가지 클라이언트는 해당 인터페이스를 주입받아 내부 execute 메소드를 통해 사용한다.

 

데코레이터 패턴을 적용하지 않는다면 코드는 아래와 같다.

 

데코레이터 패턴이든 프록시 패턴이든 중간에 한단계를 끼워준다고 생각하면 편할 것이다.

 

마치 웹에서 프록시가 중개 서버의 역할을 하는 것처럼 말이다.

데코레이터 패턴을 적용하면 위와 같이 구현할 수 있다. 마찬가지로 Component 인터페이스를 구현하면서 클라이언트가 접근하려하는 RealComponent를 주입받는다.

 

그 후 내부 메소드를 통해 RealComponent에 접근해 값을 얻어온 후, 값에 새로운 기능을 추가한 뒤에 클라이언트에게 반환해준다.

 

결국 핵심은 클라이언트의 요청을 대신 처리해준다는 것이다!

 

프록시 체인을 통해 여러가지 기능을 체인 형식으로 추가해 나갈 수 있다.

클라이언트 -> TimeDecorator -> MessageDecorator -> 서버 순으로 호출할 것이다.


인터페이스 기반 프록시

프록시를 사용하여 원본 코드를 전혀 수정하지 않고 로그 추적기를 도입해보자.

원본 코드는 위와 같다. 일반적인 흐름으로 client -> controller -> service -> repository의 흐름으로 동작한다.

 

이를 스프링 빈으로 수동 등록하여 사용하고 있는데 여기에 데코레이터 패턴을 적용하면 원본 코드의 수정없이 새로운 기능을 추가할 수 있다.

 

호출 순서는 다음과 같다.

clinet -> controllerProxy -> controller -> serviceProxy -> service -> repositoryProxy -> repository

 

중간에 끼어들어간 Proxy에서 로그를 남기는 부가 기능을 추가하면 된다.

 

여기서 핵심 아이디어는 인터페이스를 활용한 유연한 DI이다.

 

즉, controller와 같은 인터페이스를 구현하는 Proxy를 스프링 빈으로 등록한다.

 

Proxy는 내부에서 실제 객체인 controller를 주입받아 요청을 위임해주면 된다.

orderController라는 스프링 빈으로 Proxy 객체가 등록된 것이다.

 

비록 Proxy 클래스를 별도로 만들어야한다는 단점은 존재하지만 원본 코드를 전혀 수정하지 않고 로그 추적기를 도입할 수 있다는 장점이 존재한다.


구체클래스 기반 프록시

 

Java의 다형성은 인터페이스를 구현하든, 클래스를 상속하든 상위 타입만 맞으면 적용시킬 수 있다.

 

즉, 구체 클래스를 상속받아 새로운 부가 기능을 추가할 수 있다.

 

public class Client {
	private Parents parents;
    
    public Client(Parents parents) {
    	this.parents = parents;
    }
}

위와 같이 Client가 Parents를 주입받는다고 가정할 때

Client client = new Client(new Child());

Parents를 상속받는 Child를 주입할 수 있다는 의미이다.

 

이제 애플리케이션에 적용시켜보면,

 

실제 Controller를 상속받고 내부에서 주입을 받고 호출해준다.

 

언뜻봐서는 인터페이스 형식과 큰 차이가 없어보이지만 단점이 존재한다.

 

Java 기본 문법상 부모를 상속받은 자식의 생성자에서는 부모의 기본생성자가 호출된다.(super())

 

부모는 OrderServiceV2를 주입받는 생성자를 가지고 있기 때문에 super()가 호출되면 컴파일 에러가 발생한다.

 

물론 super(orderServiceV2)를 통해 부모의 생성자를 호출할 수 있지만, 여기서는 부모의 기능을 사용하지 않고 프록시의 기능만 수행한 후 부모에게 위임할 것이기 때문에 null을 넣어주었다.

 

마찬가지로 실제 객체를 주입받아 프록시를 스프링 빈으로 등록하여 사용한다.

 

이 또한 마찬가지로 원본 코드는 전혀 변경하지 않고 로그 추적기를 도입할 수 있었다.

 


장단점

인터페이스 기반 프록시

인터페이스 기반 프록시는 상속이라는 제약에서 자유롭다.

 

또한, 역할과 구현을 명확히 나누기 때문에 더 좋다.

 

하지만, 인터페이스가 없으면 프록시를 만들 수 없다는 단점이 존재한다.

 

인터페이스는 매우 유연하게 구현을 변경할 수 있기 때문에 변경할 가능성이 있을 때 매우 효과적이지만 변경 가능성이 없는 코드에 무지성으로 인터페이스를 사용하는 것은 번거롭고 비실용적이다.

 

구체 클래스 기반 프록시

인터페이스가 없더라도 프록시를 생성할 수 있다.

 

하지만 해당 클래스에만 적용할 수 있다는 단점이 존재한다.(인터페이스 기반은 인터페이스만 같다면 모두 적용 가능)

 

상속에 따른 제약이 존재한다.

  • 부모 클래스의 생성자를 호출해야함
  • 클래스에 final 키워드가 붙으면 상속 불가
  • 메서드에 final 키워드가 붙으면 Override 불가

 


ControllerProxy, ServiceProxy, RepositoryProxy든 공통적으로 로그 추적을 하고 대상 클래스만 다를 뿐이다.

 

즉, 대상 클래스가 10000개라면 프록시도 10000개 만들어야함.

 

프록시를 하나만 만들어서 모든 곳에 공통으로 적용시킬 수는 없을까??🤔
반응형

'Spring > Spring Core Advanced' 카테고리의 다른 글

Spring의 프록시 기술  (0) 2021.12.16
동적 프록시 기술  (0) 2021.11.18
템플릿 메서드 패턴과 콜백 패턴  (0) 2021.11.01
ThreadLocal  (0) 2021.10.29
로그 추적기  (0) 2021.10.28