ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Core - 구체 클래스 기반 프록시(예제1, 예제2,적용)
    FrameWork/Spring&Spring-boot 2024. 3. 12. 12:45

    구체 클래스 기반 프록시 - 예제1

    이번에는 구체 클래스에 프록시를 적용하는 방법을 학습해보자

     

    다음 보이는 ConcreteLogic은 인터페이스가 없고 구체클래스만 있다. 이렇게 인터페이스가 없어도 프록시를 적용할 수 있을까? 먼저 프록시를 도이바기 전에 기본 코드를 작성해보자.

     

    ConcreteLogic

    package com.core.springproxy.pureproxy.concreteproxy.code;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    public class ConcreteLogic {
    
        public String operation(){
            log.info("ConcreteLogic 실행");
            return "data";
        }
    }
    

     

    ConcreateLoic은 인터페이스가 없고, 구체 클래스만 있다. 여기에 프록시를 도입해야 한다.

    ConcreteClient

    package com.core.springproxy.pureproxy.concreteproxy.code;
    
    public class ConcreteClient {
        private ConcreteLogic concreteLogic;
    
        public ConcreteClient(ConcreteLogic concreteLogic) {
            this.concreteLogic = concreteLogic;
        }
    
        public void execute(){
            concreteLogic.operation();
        }
    }

     

     

    ConcreteProxyTest

    public class ConcreteProxyTest {
    
        @Test
        void noProxy(){
            ConcreteLogic concreteLogic = new ConcreteLogic();
            ConcreteClient client = new ConcreteClient(concreteLogic);
            client.execute();
        }
    }

     

     

    실행결과


    구체 클래스 기반 프록시 - 예제2

    클래스 기반 프록시 도입

    이전에는 인터페이스를 기반으로 프록시를 도입했다. 그런데 자바의 다형성은 인터페이스를 구현하든, 아니면 클래스를 상속하든 상위 타입만 맞으면 다형성이 적용된다. 쉽게 얘기해서 인터페이스가 없어도 프록시를 만들 수 있다는 뜻이다. 그래서 이번에는 인터페이스가 아니라 클래스를 기반으로 상속을 받아서 프록시를 만들어 본다.

     

     

    TimeProxy

    package com.core.springproxy.pureproxy.concreteproxy.code;
    
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    public class TimeProxy extends ConcreteLogic{
        private ConcreteLogic concreteLogic;
    
        public TimeProxy(ConcreteLogic concreteLogic) {
            this.concreteLogic = concreteLogic;
        }
    
        @Override
        public String operation() {
            log.info("TimeDecorator 실행");
            long startTime = System.currentTimeMillis();
    
            String result = concreteLogic.operation();
    
            long endTime = System.currentTimeMillis();
            long resultTime = endTime - startTime;
            log.info("TimeDecorator 종료 resulttime= {}ms",resultTime);
    
            return result;
        }
    
    }
    

     

    TimeProxy 프록시는 시간을 측정하는 부가 기능을 제공한다. 그리고 인터페이스가 아니라 클래스인 ConcreteLogic을 상속 받아서 만든다.

     

     

    ConcreteProxyTest - addProxy()추가

    package com.core.springproxy.pureproxy.concreteproxy;
    
    import com.core.springproxy.pureproxy.concreteproxy.code.ConcreteClient;
    import com.core.springproxy.pureproxy.concreteproxy.code.ConcreteLogic;
    import com.core.springproxy.pureproxy.concreteproxy.code.TimeProxy;
    import org.junit.jupiter.api.Test;
    
    public class ConcreteProxyTest {
    
        @Test
        void noProxy(){
            ConcreteLogic concreteLogic = new ConcreteLogic();
            ConcreteClient client = new ConcreteClient(concreteLogic);
            client.execute();
        }
    
        @Test
        void addProxy(){
            ConcreteLogic concreteLogic = new ConcreteLogic();
            TimeProxy proxy = new TimeProxy(concreteLogic);
            ConcreteClient client = new ConcreteClient(proxy);
            client.execute();
        }
    }

     

    여기서 핵심은 ConcreteClient의 생성자에 concreteLoic이 아니라 timeProxy를 주입하는 부분이다. 

    ConcreteClient는 ConcreteLogic을 의존하는데, 다형성에 의해 ConcreteLogic에 ConcreteLogic도 들어갈 수 있고, timeProxy도 들어갈 수 있다. 

     

     

    ConcreteLogic에 할당할 수 있는 객체

    • ConcreteLogic = concreteLogic (본인과 같은 타입을 할당)
    • ConcreteLogic = timeProxy (자식 타입을 할당)

     

     

    ConcreateClient 참고 

    package com.core.springproxy.pureproxy.concreteproxy.code;
    
    import lombok.extern.slf4j.Slf4j;
    
    
    public class ConcreteClient {
        private ConcreteLogic concreteLogic; //ConcreteLoic, TimeProxy 모두 주입 가능
    
        public ConcreteClient(ConcreteLogic concreteLogic) {
            this.concreteLogic = concreteLogic;
        }
    
        public void execute(){
            concreteLogic.operation();
        }
    }
    

     

    ConcreteClient를 상속받은 자식 Class의 객체 인스턴스는 모두 주입 될 수 있다. 

     

    실행 결과

     

    실행 결과를 보면 인터페이스가 없어도 클래스 기반의 프록시가 잘 적용된 것을 확인할 수 있다.

     

    ➕참고
    자바 언어에서 다형성은 인터페이스나 클래스 구분하지 않고 모두 적용된다. 해단 타입과 그 타입의 하위 타입은 모두 다형성의 대상이 된다. 자바 언어의 너무 기본적인 내용일 수 있지만, 구체 클래스를 사용해도 프록시가 가능하다는 것을 집고 넘어갈 필요가 있었다. 

     


    구체 클래스 기반 프록시 - 적용

    OrderRepositoryConcreteProxy

    package com.core.springproxy.config.v1_proxy.concrete_poroxy;
    
    import com.core.springproxy.app.v2.OrderControllerV2;
    import com.core.springproxy.app.v2.OrderServiceV2;
    import com.core.springproxy.trace.TraceStatus;
    import com.core.springproxy.trace.logTrace.LogTrace;
    import lombok.RequiredArgsConstructor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.web.bind.annotation.GetMapping;
    
    
    public class OrderControllerConcreteProxy extends OrderControllerV2{
    
    
        private final OrderControllerV2 target;
        private final LogTrace logTrace;
    
        public OrderControllerConcreteProxy(
        @Qualifier("orderControllerV2") OrderControllerV2 target,
        LogTrace logTrace
        ) {
            super(null);
            this.target = target;
            this.logTrace = logTrace;
        }
    
        @Override
        @GetMapping("/v2/order")
        public String createOrder(String itemId){
            TraceStatus status = null;
            try {
                status = logTrace.begin("OrderCreateConcreteProxy.createOrder()");
                target.createOrder(itemId);
                logTrace.end(status);
                return "ok";
            }catch (Exception e){
                logTrace.exception(status,e);
                throw e;
            }
    
    
        }
    }

     


     

    OrderServiceConcreteProxy

    package com.core.springproxy.config.v1_proxy.concrete_poroxy;
    
    import com.core.springproxy.app.v2.OrderRepositoryV2;
    import com.core.springproxy.app.v2.OrderServiceV2;
    import com.core.springproxy.trace.TraceStatus;
    import com.core.springproxy.trace.logTrace.LogTrace;
    
    public class OrderServiceConcreteProxy extends OrderServiceV2{
    
        private final OrderServiceV2 orderService;
        private final LogTrace logTrace;
    
        public OrderServiceConcreteProxy( OrderServiceV2 orderService, LogTrace logTrace) {
            super(null);
            this.orderService = orderService;
            this.logTrace = logTrace;
        }
        @Override
        public void orderItem(String itemId) {
            TraceStatus status = null;
            try {
                status = logTrace.begin("OrderServiceConcreteProxy.orderItem()");
                orderService.orderItem(itemId);
                logTrace.end(status);
            }catch (Exception e){
                logTrace.exception(status,e);
                throw e;
            }
        }
    
    }
    

     

    인터페이스를 구현하는 것과 다르게 클래스를 상속하는 경우는 부모 클래스에 생성자가 존재하면  자식클래스에서 생성자를 호출해야 한다.

    • 인터페이스가 아닌 OrderServiceV2 클래스를 상속 받아서 프록시를 만든다.

     

    클래스 기반 프록시의 단점

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

     

    OrderServiceV2의 생성자 - 참고

     public OrderServiceV2(OrderRepositoryV2 orderRepository) {
            this.orderRepository = orderRepository;
        }

    OrderControllerConcreteProxy

    package com.core.springproxy.config.v1_proxy.concrete_poroxy;
    
    import com.core.springproxy.app.v2.OrderRepositoryV2;
    import com.core.springproxy.app.v2.OrderServiceV2;
    import com.core.springproxy.trace.TraceStatus;
    import com.core.springproxy.trace.logTrace.LogTrace;
    
    public class OrderServiceConcreteProxy extends OrderServiceV2{
    
        private final OrderServiceV2 orderService;
        private final LogTrace logTrace;
    
        public OrderServiceConcreteProxy( OrderServiceV2 orderService, LogTrace logTrace) {
            super(null);
            this.orderService = orderService;
            this.logTrace = logTrace;
        }
        @Override
        public void orderItem(String itemId) {
            TraceStatus status = null;
            try {
                status = logTrace.begin("OrderServiceConcreteProxy.orderItem()");
                orderService.orderItem(itemId);
                logTrace.end(status);
            }catch (Exception e){
                logTrace.exception(status,e);
                throw e;
            }
        }
    
    }
    

     


    인터페이스 기반 프록시와 클래스 기반 프록시

    프록시

    프록시를 사용한 덕분에 원본 코드를 변경하지 않고 V1,V2 애플리케이션에 LogTrace 기능을 적용할 수 있었다.

     

    인터페이스 기반 프록시 vs 클래스 기반 프록시

    • 인터페이스가 없어도 클래스 기반으로 프록시를 생성할 수 있다.
    • 클래스 기반 프록시는 해당 클래스에만 적용할 수 있다. 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
    • 클래스 기반 프록시는 구현이 아닌 상속을 하기 때문에 몇가지 제약이 있다.
      • 부모 클래스의 생성자를 호출해야한다.(앞서 본 예제)
      • 클래스에 final 키워드가 붙으면 상속이 불가능하다.(자바 문법)
      • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.

     

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

     

    ➕참고
    하지만 인터페이스 기반 프록시는 캐스팅 관련해서 단점이 존재한다.

     

    이론적으로는 모든 객체에 인터페이스를 도입해서 역할과 구현을 나누는 것이 좋다. 이렇게 하면 역할과 구현이 나누어져 있기 때문에 매우 편리하게 구현체을 변경할 수 있다. 하지만 실제로는 구현을 변경할 일이 없는 클래스도 많다.

    인터페이스를 도입하는 것은 변경할 가능성이 있을 때 효과적인데, 구현을 변경할 가능성이 없는 코드에 무작정 인터페이스를 사용하는것은 번거롭고 그렇게 실용적이지 않다. 이런곳에는 실용적인 관점에서 인터페이스를 사용하지 않고 구체 클래스를 바로 사용하는 것이 좋을 수 있다. (물론 인터페이스를 도입하는 것에는 다양한 이유가 있다. 핵심은 인터페이스가 항상 필수 적인 선택사항이 아니라는 점이다.)

     

    결론

    실무 상황에서는 프록시를 적용할 때 V1 처럼 인터페이스가 있는 경우도 있고, V2처럼 구체 클래스가 있는 경우도 있다. 둘이 함께 섞여 있는 것이 현실이기 때문에 2가지 상황 모두 대응할 수 있어야 한다.

     

    너무 많은 프록시 클래스

    지금까지 프록시를 사용해서 기존 코드를 변경하지 않고, 로그 추적기라는 부가적 기능을 적용할 수 있었다. 그런데 문제는 프록시 클래스를 너무 많이 만들어야 한다는 점이다. 잘 보면 프록시 클래스가 하는 일은 LogTrace를 사용하는 것인데, 그 로직이 모두 같고 대상 클래스만 다를 뿐이다. 만약 지금과 같은 방법으로 프록시를 도입하게 되다면 대상 클래스가 100개일 경우에는 프록시 클래스도 100개를 만들어야 한다. → 동적 프록시를 활용해서 이 문제를 해결할 수 있다.


    정리

    프록시를 사용하면 실제 객체가 스프링 빈으로 등록되는 것이 아니라 proxy객체를 빈으로 등록하고 proxy 객체가 실제 객체를 알 수 있도록 참조를 해주면 된다. 실제 객체는 스프링 컨테이너에 빈으로 등록해서는 안된다. 이렇게 해야 어디서 orderService 의존관계 주입을 받던지 상관없이 항상 프록시 객체가 주입이 되어야 하기대문이다. 항상 부가 기능이 실행되기 위해서는 실제 객테 인스턴스가 실해되기 전에 proxy객가 실행되어야 하기 때문이다. 

     

     

     

    [출처 - 스프링 핵심원리-고급편, 저 김영한]

    https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

     

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

    스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기 📢 수강

    www.inflearn.com

     

    댓글

Designed by Tistory.