모르지 않다는 것은 아는것과 다르다.

객체지향

프록시 적용하기

채마스 2022. 8. 6. 23:33

인터페이스 기반 프록시 적용

  • 인터페이스를 기반으로 프록시를 적용한다면 아래와 같은 구조가 된다.

  • 위의 그림처럼 Controller와 Service를 인터페이스로 구현하고, 실제 구현체와 같은 인터페이스를 구현하도록 프록시를 만든다.
  • 이렇게 되면 런타임 시점에는 아래와 같이 의존 관계가 설정된다/

 

구현

Repository, RepositoryProxy

public class OrderRepositoryV1Impl implements OrderRepositoryV1 {
    @Override
    public void save(String itemId) {
        //... 생략
    }
}
@RequiredArgsConstructor
public class OrderRepositoryInterfaceProxy implements OrderRepositoryV1 {
    private final OrderRepositoryV1 target;
    private final 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; 
        }
    } 
}
  • 위와 같이 OrderRepositoryV1를 구현하도록 OrderRepositoryInterfaceProxy를 구현한다.
  • 또한 실제 구현체(target)를 주입 받을 수 있도록 OrderRepositoryV1타입의 target을 변수로 가지고 있는다.
  • 로그를 남기고 실제 구현체를 실행시키는 것을 확인할 수 있다.
  • 이렇게되면, 실제 구현체를 건드리지 않고도 로그를 남기는 기능을 추가할 수 있다.

 

Service, ServiceProxy

@RequiredArgsConstructor
public class OrderServiceV1Impl implements OrderServiceV1 {

    private final OrderRepositoryV1 orderRepository;

    @Override
    public void orderItem(String itemId) {
        orderRepository.save(itemId);
    }
}
@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도 위와 같은 방식으로 프록시를 구현한다.

 

Controller, ControllerProxy

@RequiredArgsConstructor
public class OrderControllerV1Impl implements OrderControllerV1 {

    private final OrderServiceV1 orderService;

    @Override
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }

    @Override
    public String noLog() {
        return "ok";
    }
}
@RequiredArgsConstructor
public class OrderControllerV1Impl implements OrderControllerV1 {

    private final OrderServiceV1 orderService;


    @Override
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }

    @Override
    public String noLog() {
        return "ok";
    }
}
@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();
    }
}

 

Config

@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);
    }
}
  • 위와 같이 의존 관계를 설정해 주어야 한다.
  • orderRepository는 실제 구현체를 반환하는 것이 아니라 프록시를 반환한다.
  • OrderRepositoryV1Impl(실제 구현체)는 빈으로 등록되지 않지만, 프록시에 포함 되어 있기 때문에 프록시를 통해서 호출할 수 있다.
  • 프록시 객체는 스프링 컨테이너가 관리하고 자바 힙 메모리에도 올라간다. 반면에 실제 객체는 자바 힙 메모리에는 올라가지만 스프링 컨테이너가 관리하지는 않는다.
  • 따라서 스프링 컨테이너에는 아래와 같이 빈이 등록된다.

 

구체 클래스 기반 프록시

  • 인터페이스가 아닌 경우라도 프록시를 적용할 수 있다.

 

Repository, RepositoryProxy

public class OrderRepositoryV2 {

    public void save(String itemId) {
        //...생략
    }
}
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; 
        }
    } 
}
  • OrderRepositoryV2를 상속받아 OrderRepositoryConcreteProxy를 만든다.
  • 실제 구현체를 주입을 받을 수 있도록 OrderRepositoryV2 타입의 target을 변수로 가지고 있는다.
  • 구현 방식은 인터페이스 방식과 거의 비슷하다.

 

Service, ServiceProxy

@RequiredArgsConstructor
public class OrderServiceV2 {
    private final OrderRepositoryV2 orderRepository;

    public void orderItem(String itemId) {
        orderRepository.save(itemId);
    }
}
public class OrderServiceConcreteProxy extends OrderServiceV2 {
    private final OrderServiceV2 target;
    private final LogTrace logTrace;

    public OrderServiceConcreteProxy(OrderServiceV2 target, LogTrace logTrace) {
        super(null);
        this.target = target;
        this.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; 
        }
    } 
}   
  • 인터페이스 방식과 차이점은 생성자에서 super(null)을 담아 줘야한다는 것이다.
  • java 문법상 super(null)를 생략하면 super()가 기본으로 호출 되기 때문이다.
  • 이 부분이 구체 클래스 방식의 단점이다.

 

Controller, ControllerProxy

@Slf4j
@RequestMapping
@ResponseBody
@RequiredArgsConstructor
public class OrderControllerV2 {

    private final OrderServiceV2 orderService;

    @GetMapping("/v2/request")
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }

    @GetMapping("/v2/no-log")
    public String noLog() {
        return "ok";
    }

}
public class OrderControllerConcreteProxy extends OrderControllerV2 {
    private final OrderControllerV2 target;
    private final LogTrace logTrace;

    public OrderControllerConcreteProxy(OrderControllerV2 target, LogTrace logTrace) {
        super(null);
        this.target = target;
        this.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;
        } 
    }
}
  • OrderControllerConcreteProxy 도 마찬가지로 OrderControllerV2를 상속받으며, OrderControllerV2 타입이 target을 변수로 가지고 있다.
  • 또한, super(null)을 호출했다.
  • 프록시는 부모 객체의 기능을 사용하지 않기 때문에 super(null) 을 입력해도 된다.

 

Config

@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);
    }
}
  • 다음과 같이 인터페이스가 아닌 구체 클래스를 기반으로 프록시를 만들고 의존관계를 설정해준다.

 

정리

  • 인터페이스 기반의 프록시는 상속이라는 제약에서 자유기 때문에 구체 클래스 방식보다 좋다.
  • 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다.
  • 하지만 무작정 인터페이스 방식이 좋은 것은 아니다. -> 인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적 때문에 변할 가능성이 거의 없다면 구현 클래스 방식이 더 좋은 경우도 있다.

 

 

References

  • 김영한님의 스프링 고급편

'객체지향' 카테고리의 다른 글

프록시 패턴과 데코레이터 패턴  (0) 2022.08.06
생성 패턴 (디자인 패턴)  (0) 2022.05.15
행위 패턴 (디자인 패턴)  (0) 2022.05.15
구조패턴 (디자인 패턴)  (0) 2022.05.15
클래스 다이어그램  (0) 2022.04.23