김영한의 스프링 핵심 원리(고급편) - 프록시 패턴, 데코레이터 패턴

2025. 2. 21. 00:17·김영한의 스프링 핵심 원리 - 고급편

Question

  • 프록시란?
  • 서버와 프록시는 왜 같은 인터페이스를 구현해야 하는가?
  • 프록시 패턴과 데코레이터 패턴의 차이는
  • 프록시 패턴 구현 방법은?
  • 데코레이터 패턴 구현 방법은?
  • 인터페이스가 없는 구현체에 프록시를 적용하는 방법은?
  • 단순 프록시 기술만 사용했을 때 여러 클래스마다 프록시를 중복해서 생성해줘야 하는 문제는 어떻게 개선 가능한가

 

 

프록시

  • Clinet -> Server로 요청을 하려고 할 때 중간에서 Proxy(대리자)를 통해서 요청을 보낼 수 있음
  • 이는 직접 호출이 아니라 간접 호출을 하게 되는데, 이러면 중간에 대리자가 기존 기능 외에 여러 기능을 추가할 수 있음
    • 캐싱을 통해 이미 데이터가 존재하면 Server로 요청을 하지 않고 Proxy가 바로 데이터를 반환하고 데이터가 없을 경우에만 Server로 요청
    • 기존에 처리 해야 하는 요청 외에 Proxy가 추가적인 처리를 더 해줄 수도 있음
  • Client는 자신이 호출한게 Proxy든 Server든 상관이 없고 자신이 원하는 요청만 처리되면 됨

 

프록시와 인터페이스

  • 프록시를 사용하면 Client는 서버에게 요청한 것인지 프록시에게 요청한 것인지 모르지만 그 요청한 결과는 받아야 함
  • 서버인지 프록시인지 몰라야 하는 조건을 만족하려면 이는 서버와 프록시가 같은 인터페이스를 사용해야 한다는 의미
  • 이렇게 같은 인터페이스를 사용하면 DI와 같은 기능을 통해 기존 Server에서 Proxy로 바꿔치기 할 수 있음

 

프록시의 주요 기능

  • 접근 제어
    • 권한에 따라 접근 차단
    • 캐싱
    • 지연 로딩
  • 부가 기능 추가
    • 기존의 기능에 추가적인 기능 처리
    • 응답값 변형 혹은 로깅 추가 등

 

GOF 디자인 패턴

  • GOF 디자인 패턴에서는 프록시를 사용하는 패턴이 2가지인데, 둘 다 프록시의 기능을 사용하지만 사용 의도에 따라 2가지로 분류
    • 프록시 패턴: 접근 제어가 목적
    • 데코레이터 패턴: 새로운 기능 추가가 목적

 

프록시 패턴 - 캐싱기능

public interface Subject {
	String operation();
}


@Slf4j
public class RealSubject implements Subject {
    @Override
    public String operation() {
        log.info("실제 객체 호출");
        sleep(1000);
        return "data";
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            log.error("에러 발생", e);
            Thread.currentThread().interrupt();
        }
    }
}

@Slf4j
public class CacheProxy implements Subject {
    private Subject target;
    private String cacheValue;

    public CacheProxy(Subject target) {
        this.target = target;
    }

    @Override
    public String operation() {
        log.info("프록시 호출");

        if (cacheValue == null) {
            cacheValue = target.operation();
        }

        return cacheValue;
    }
}



public class ProxyPatternClient {
    private Subject subject;

    public ProxyPatternClient(Subject subject) {
        this.subject = subject;
    }

    public void execute() {
        subject.operation();
    }
}



public class ProxyPatternTest {

    @Test
    void cacheProxyTest() {
        Subject realSubject = new RealSubject();
        Subject cacheProxy = new CacheProxy(realSubject);
        ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
        client.execute(); // 첫 번째 호출 (실제 객체 호출 및 캐싱)
        client.execute(); // 두 번째 호출 (캐시된 값 반환)
        client.execute(); // 세 번째 호출 (캐시된 값 반환)
    }
}
  • 기존에 Client -> Subject로 요청해서 데이터를 받아요는 케이스가 있었음
  • 이 때는 RealSubject라는 구현체가 실제로 데이터를 반환
  • 하지만, Subject를 구현하는 새로운 CacheProxy클래스를 생성
  • Client는 기존처럼 Subject로 요청하지만 설정을 변경하여 구현클래스를 RealSubject가 아닌 CacheProxy로 변경
  • CacheProxy는 Client의 요청을 받아 저장하고 있는 데이터가 없으면 RealSubject로 요청을 넘기고, 만약 본인이 저장하고 있는 데이터가 있으면 RealSubject로 요청하지 않고 바로 Client로 반환
  • Client 입장에서는 변했는지 전혀 알 수 없고, 코드 변화도 없음
  • RealSubject에도 코드의 변화가 없으며 넣어주는 객체만 바꿔주면 됨

 

데코레이터 패턴 - 기능 추가

 

 

 

@Slf4j
public class TimeDecorator implements Component {
    private Component component;

    public TimeDecorator(Component component) {
        this.component = component;
    }

    @Override
    public String operation() {
        log.info("TimeDecorator 실행");

        long startTime = System.currentTimeMillis();
        String result = component.operation();
        long endTime = System.currentTimeMillis();

        long resultTime = endTime - startTime;
        log.info("TimeDecorator 종료 resultTime={}ms", resultTime);

        return result;
    }
}
  • 데코레이터 패턴도 프록시 패턴과 방법은 유사함
  • Client -> Component의 operation() 함수를 호출
  • 기존에는 RealComponent 라는 구현체가 operation() 함수를 사용했음
  • 하지만, Component 클래스를 상속하는 TimeDecorator, MessageDecorator를 추가
  • Client는 기존처럼 Component의 operation() 함수를 호출
  • 하지만, 실제 구현체를 TimeDecorator로 바꾸고, TimeDecorator -> MessageDecorator -> RealComponent 이런식으로 요청을 넘겨줌
  • 프록시는 2개 이상 적용해서 활용할 수 있으며 이를 프록시 체인이라고 함
  • TimeDecorator에서는 시간을 출력해주고, MessageDecorator에서는 메시지를 추가해 줌
  • Client는 기존과 동일하게 호출하고 원하는 요청을 응답받지만 시간 출력, 메시지 출력 기능을 기존 변경 없이 추가 가능

 

데코레이터 패턴 개선

  • 데코레이터 패턴에서 여러 프록시를 사용하다 보면 중복 문제가 발생함
  • 예를 들어서, 데코레이터 패턴은 혼자만은 존재할 수 없고 무조건 실제 RealComponent의 operation() 함수를 호출이 존재 해야함
  • 이는 여러 Decorator에 동일하게 적용되는 부분이고 따라서, 여러 데코레이터는 이러한 Component에 대한 지정과 operation()함수 호출이 반복됨
  • 따라서, GOF에서는 Decorator는 상위 추상 클래스를 만들고 그 아래 여러 데코리어터가 상속하는 방식을 사용

 

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

  • 둘 다 구현되는 모양은 거의 동일
  • 디자인 패턴에서 중요한 것은 패턴의 모양이 아니라 패턴을 만든 의도
  • 따라서 의도를 잘 판단해야 함
  • 프록시 패턴의 의도: 다른 개체에 대한 접근을 제어하기 위해 대리자를 제공
  • 데코레이터 패턴의 의도: 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공

 

프록시 적용 - 로그 트레이스 예제

    •  

 

@RequiredArgsConstructor
public class OrderControllerInterfaceProxy implements OrderControllerV1 {
    private final OrderControllerV1 target;
    private final LogTrace logTrace;

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

    @Override
    public String noLog() {
        return target.noLog();
    }
}


@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);
    }

    @Bean
    public LogTrace logTrace() {
        return new ConsoleLogTrace();
    }
}
  • 이전에 사용했던 로그 트레이스를 Controller -> Service -> Repository 예시에 적용하려고 함
  • 요건은 Controller, Service, Repository 코드를 변경하지 않고 각각에 로깅만 추가 해야 함
  • 예시는 Controller에만 적용하는 예시
  • 방법은 Controller 구현체를 구현하는 ProxyController 생성
  • ProxyController에서 로깅 수행하는 작업 추가해주고 진짜 Controller를 부르게 하면 됨
  • 중요한 점은 Config를 수정해 줘야 하는데, Config에서 Bean을 등록할 때 Controller의 Parameter로는 진짜 Controller를 넣어주고, Bean은 Proxy를 리턴해 줘야 한다는 것

 

프록시 적용 - 인터페이스가 없는 경우

@Slf4j
public class TimeProxy extends ConcreteLogic {
    private final ConcreteLogic realLogic;

    public TimeProxy(ConcreteLogic realLogic) {
        this.realLogic = realLogic;
    }

    @Override
    public String operation() {
        log.info("TimeDecorator 실행");

        long startTime = System.currentTimeMillis();
        String result = realLogic.operation();
        long endTime = System.currentTimeMillis();

        long resultTime = endTime - startTime;
        log.info("TimeDecorator 종료 resultTime={}ms", resultTime);

        return result;
    }
}


@Test
void addProxy() {
    ConcreteLogic concreteLogic = new ConcreteLogic();
    TimeProxy timeProxy = new TimeProxy(concreteLogic);
    ConcreteClient client = new ConcreteClient(timeProxy);
    client.execute();
}
  • 인터페이스가 없으면 어떻게 기존 코드를 변경하지 않고 진짜 클래스가 아닌 프록시 클래스로 바꿔치기 할 수 있을까?
  • 사실 중요한 것은 진짜 클래스와 프록시 클래스가 동일한 상속관계 안에만 있으면 됨
  • 예를 들어, 인터페이스를 쓴다는 것도 진짜 클래스와 프록시 클래스가 같은 인터페이스를 구현하므로 동일한 상속 관계에 있는 것
  • 이와 마찬가지로 만약에 프록시 클래스가 진짜 클래스를 상속해버린다면, 이 또한 동일한 상속관계에 있다고 볼 수 있음
  • 따라서, 동일한 상속 관계라면 빈을 주입해줄 때 기존에 진짜 클래스를 빈으로 주입해주던걸 프록시 클래스 주입으로 변경해줄 수 있음

 

위 프록시 기술을 사용했을 때의 한계

  • 원하는 클래스에 적용하고자 했을 때 적용할 때 마다 프록시를 새로 생성해줘야 함
  • 예를 들어서 OrderControllerInterfaceProxy, OrderSerivceInterfaceProxy, OrderRepositoryInterfaceProxy 이런식으로 같은 코드를 사용하지만 여러 프록시를 생성해줘야 하는 중복의 문제가 있음
  • 이는 뒤에서 설명할 동적 프록시로 해결 가능

'김영한의 스프링 핵심 원리 - 고급편' 카테고리의 다른 글

김영한의 스프링 핵심 원리(고급편) - 빈 후처리기  (0) 2025.02.22
김영한의 스프링 핵심 원리(고급편) - 프록시 팩토리  (0) 2025.02.22
김영한의 스프링 핵심 원리(고급편) - 동적 프록시 기술  (0) 2025.02.21
김영한의 스프링 핵심 원리(고급편) - 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴  (0) 2025.02.20
김영한의 스프링 핵심 원리(고급편) - (Log Trace, ThreadLocal)  (0) 2025.02.18
'김영한의 스프링 핵심 원리 - 고급편' 카테고리의 다른 글
  • 김영한의 스프링 핵심 원리(고급편) - 프록시 팩토리
  • 김영한의 스프링 핵심 원리(고급편) - 동적 프록시 기술
  • 김영한의 스프링 핵심 원리(고급편) - 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴
  • 김영한의 스프링 핵심 원리(고급편) - (Log Trace, ThreadLocal)
5jyan5
5jyan5
  • 5jyan5
    jyan
    5jyan5
  • 전체
    오늘
    어제
    • 분류 전체보기 (242)
      • 김영한의 스프링 핵심 원리(기본편) (8)
      • 김영한의 스프링 핵심 원리 - 고급편 (11)
      • 김영한의 스프링 MVC 1편 (1)
      • 김영한의 스프링 DB 1편 (3)
      • 김영한의 스프링 MVC 2편 (3)
      • 김영한의 ORM 표준 JPA 프로그래밍(기본편) (9)
      • 김영한의 스프링 부트와 JPA 활용2 (2)
      • 김영한의 실전 자바 - 중급 1편 (1)
      • 김영한의 실전 자바 - 고급 1편 (9)
      • 김영한의 실전 자바 - 고급 2편 (9)
      • Readable Code: 읽기 좋은 코드를 작성.. (2)
      • 김영한의 실전 자바 - 고급 3편 (9)
      • CKA (118)
      • 개발 (37)
      • 경제 (4)
      • 리뷰 (1)
      • 정보 (2)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      Target
      단방향 맵핑
      typequery
      @args
      reentarantlock
      gesingleresult
      hibernate5module
      조회 성능 최적화
      프록시 팩토리
      김영한
      스레드
      requset scope
      WAS
      cglib
      자바
      jpq
      락
      @discriminatorcolumn
      log trace
      버퍼
      @within
      Thread
      페치 조인
      프록시
      JPQL
      빈 후처리기
      @discriminatorvalue
      고급
      양방향 맵핑
      jdk 동적 프록시
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.2
    5jyan5
    김영한의 스프링 핵심 원리(고급편) - 프록시 패턴, 데코레이터 패턴
    상단으로

    티스토리툴바