전략패턴을 통해서 로깅을 적용해서 비지니스 로직을 담당하는 클래스와 로깅(부가기능)을 담당하는 클래스를 분리할 수 있었지만 여전히 메인 기능에 해당하는 코드를 변경해야 하는 단점이 있었다. 이 문제를 해결하기 위해서 프록시 패턴을 사용해서 메인 기능의 수정 없이 요청 시간 측정 로그를 적용해보자
옥스퍼드 사전에서 proxy의 뜻은 "the authority to represent someone else"으로 "누군가를 대신할 수 있는 권한"이다. 누군가를 대신해서 어떤일을 해주는것을 의미한다. 네트워크 쪽에서 프록시가 있다면, 클라이언트와 서버 사이에서 클라이언트의 요청을 대신 처리해주는 서버를 의미한다. 프로그램 안에서 프록시가 존재한다면 객체와 객체 간의 상호작용의 중간에서 함수를 대신 호출해주는 역할을 한다.
즉, 규모의 차이만 있을 뿐 프록시는 두개의 노드 사이에서 접근제어와 부가기능을 해주는 대리자이다.
프록시 vs 데코레이터
GOF(Gang Of Four)책에서는 프록시와 데코레이터를 의도의 차이로 설명한다.
어떤 노드에서 다른 노드로의 접근제어가 목적이면proxy
, 한 노드에서 다른 노드로 이동할때 중간에 부가적인 동작을 하고싶다면decorator
라고 한다.
지금 우리가 처한 문제는 비지니스 클래스를 변경하지 않고 로깅을 할수 있도록 하는 것이기 때문에, 로깅을 해주는 프록시클래스를 정의해주고, 프록시 객체에서 로깅 이후에 비지니스 로직을 수행하는 함수를 호출해주도록 하면 비지니스 로직의 코드 수정을 피할 수 있다.
우선 사진과 같이 기존에 있던 PostingController
의 인터페이스 IPostingController
를 만들고 프록시 클래스 PostingProxyController
가 이를 구현하도록 했다
@RestController
@RequiredArgsConstructor
public class PostingProxyController implements IPostingProxyController {
private final PostingController target;
private final LogTracer logTracer;
@Override
public ResponseEntity getPostings(Long userId, PostingSearch postingSearch, int offset, int limit) {
Long startTimeMs = System.currentTimeMillis(); // 시작
ResponseEntity result = target.getPostings(userId, postingSearch, offset, limit);
Long endTimeMs = System.currentTimeMillis(); // 끝
log.info("time - {}", endTimeMs - startTimeMs); // 걸린 시간
return result;
}
...... (생략)
}
그리고 @RestController
어노테이션을 통해서 Spring Bean을 등록할 때 PostingController
가 아닌 PostingProxyController
를 등록한다.(PostingController
는 그냥 @Component
로만 등록해서 di만 받을 수 있도록 한다)
이제 요청이 스프링 컨테이너 안으로 들어오면 사진과 같이 프록시 컨트롤러로 들어가서 시간측정 로깅 시작하고 프록시 객체 안에서 컴포지션 관계에 있는 진짜 컨트롤러를 호출해서 비지니스적인 기능을 실행한다.
꼭 인터페이스를 만들어야 할까?
상속을 사용해도 된다.PostingController
를PostingProxyController
가 상속받고 메서드를 오버라이드 해도 동일하게 동작한다. 인터페이스와 상속 모두 다형성을 사용할수 있다는 점에서는 동일하기 때문이다.
하지만 상속의 특성상PostingProxyController
입장에서 부모클래스를 생성자에서 super()를 통해서 초기화 해주어야 하기 때문에 언제나 super(null)를 넣어주어야 한다.PostingProxyController
가 부모클래스의 기능을 사용할 일이 없기 때문이다. (단순히 다형성을 위한 상속이기 때문)
프록시 클래스에서 하는일은 로깅을 하는것으로 정해져있으며, 메인 기능을 하는 함수 호출 전후로 시간을 찍어주는 것이다. 하지만 인터페이스 혹은 상속을 사용하기 때문에 PostingController
를 구현 혹은 오버라이드 해야하고, 모든 오버라이딩 된 메서드에는 중복된 시간 기록 코드가 들어간다.
즉, 프록시안에서 하는 일은 동일한데 적용대상만 다른 상황이다.
또한, 지금은 컨트롤러에서만 프록시를 적용했지만 모든 모듈에 전부 프록시를 적용한다면 인터페이스와 프록시 클래스의 개수가 n개로 늘어날 것이다.
다음 포스팅에서는 중복되는 코드 없이 필요할 때 마다 동적으로 프록시를 생성하는 방법에 대해 알아보자