프록시 적용

바그다드·2023년 8월 19일
0

지난 포스팅에서는 프록시에 대해 알아보았다.
프록시는 일종의 대리자로서 실체 객체 대신에 호출이 되어 부가기능을 수행하고, 실제 객체를 대신 호출해주는 역할을 한다.
프록시의 장점은 핵심 로직은 전혀 수정하지 않고 부가 기능을 추가할 수 있다는 것이다.

그럼 기존에 봤던 로그 추적기에 프록시를 적용하면 코드가 어떻게 바뀌는지 확인해보자.

1. 인터페이스에 프록시 적용

컨트롤러와 서비스를 이용해 의존 관계가 어떻게 형성되는지 확인해보자.

1.컨트롤러 인터페이스

  • 프록시와 타겟을 구현하기 위해 인터페이스를 생성한다.
@RequestMapping
@ResponseBody
public interface OrderControllerV1 {

    // 로그 적용
    @GetMapping("/v1/request")
    String request(@RequestParam("itemId") String itemId);

    // 로그 미적용
    @GetMapping("v1/no-log")
    String noLog();
}

2. 컨트롤러 구현체 생성

  • 여기서는 로그추적을 위한 LogTrace객체도 지니지 않고 있고,
    로그를 남기기 위한 로직도 존재하지 않는 핵심 로직만 남아있다.
public class OrderControllerV1Impl implements OrderControllerV1 {

    private final OrderServiceV1 orderService;

    public OrderControllerV1Impl(OrderServiceV1 orderService) {
        this.orderService = orderService;
    }

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

    @Override
    public String noLog() {
        return "ok";
    }
}

3. 프록시 생성

@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()");
            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();
    }
}
  • 같은 인터페이스를 구현하고 있기 때문에 프록시에서 타겟 객체를 주입받아 호출할 수 있게 되었다.

4. 서비스 인터페이스 생성

public interface OrderServiceV1 {
    void orderItem(String itemId);
}

5. 서비스 구현체 생성

public class OrderServiceV1Impl implements OrderServiceV1 {

    private final OrderRepositoryV1 orderRepository;

    public OrderServiceV1Impl(OrderRepositoryV1 orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public void orderItem(String itemId) {
        orderRepository.save(itemId);
    }
}

6. 프록시 생성

@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.orderItem(itemId);
            logTrace.end(status);
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }

    }
}

7. 의존성 주입

@Configuration
public class InterfaceProxyConfig {

    @Bean
    public OrderControllerV1 orderController(LogTrace trace) {
    	// OrderControllerV1Impl에 orderService의 프록시를 주입
        OrderControllerV1Impl controllerImpl = new OrderControllerV1Impl(orderService(trace));
        return new OrderControllerInterfaceProxy(controllerImpl, trace);
    }
    
    @Bean
    public OrderServiceV1 orderService(LogTrace trace) {
    	// OrderServiceV1Impl에 orderRepository의 프록시를 주입
        OrderServiceV1Impl serviceImpl = new OrderServiceV1Impl(orderRepository(trace));
        return new OrderServiceInterfaceProxy(serviceImpl, trace);
    }

}
  • 리포지토리 코드는 생략하였다.
  1. 스프링 빈으로 등록되는 것은 실제 객체가 아니라 프록시 객체이다.
  2. 여기서 각 실제 객체가(xxImpl) 주입받는 것은 프록시가(xxProxy) 들어있다는 것을 명심하자.
    프록시와 실제 객체가 모두 하나의 인터페이스를 구현하고 있기 때문에 가능하다.

여기까지는 인터페이스를 구현하고 있어 지난 포스팅에서 봤던 방식과 다르지 않다.
하지만 따로 인터페이스가 존재하지 않는 어플리케이션도 많은데 이런 경우에는 프록시를 어떻게 적용할 수 있을까?

2. 구현 클래스에 프록시 적용

  • 구현 클래스만 있을 경우 상속을 이용해 프록시를 생성할 수 있다.
  • 따로 인터페이스를 사용하지 않고 각 계층에서 로직을 구축하였다.

1. 컨트롤러 생성

@Slf4j
@RequestMapping
@ResponseBody
public class OrderControllerV2 {

    private final OrderServiceV2 orderService;

    public OrderControllerV2(OrderServiceV2 orderService) {
        this.orderService = orderService;
    }

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

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

2. 프록시 생성

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()");
            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();
    }
}
  • 상속(extends)을 이용해 실제 객체를 상속받고 있다.
  • 여기서 부모 클래스의 생성자는 사용하지 않을 것이므로 파라미터로 null을 넣어주자.
  • target으로 부모 클래스인 OrderControllerV2를 주입받을 것이다.

3. 서비스 생성

public class OrderServiceV2 {
    private final OrderRepositoryV2 orderRepository;

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

    public void orderItem(String itemId) {
        orderRepository.save(itemId);
    }
}

4. 프록시 생성

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.orderItem(itemId);
            logTrace.end(status);
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

5. 의존성 주입

@Configuration
public class ConcreteProxyConfig {

    @Bean
    public OrderControllerV2 orderControllerV2(LogTrace trace) {
        OrderControllerV2 controllerImpl = new OrderControllerV2(orderServiceV2(trace));
        return new OrderControllerConcreteProxy(controllerImpl,trace);
    }

    @Bean
    public OrderServiceV2 orderServiceV2(LogTrace trace) {
        OrderServiceV2 serviceImpl = new OrderServiceV2(orderRepositoryV2(trace));
        return new OrderServiceConcreteProxy(serviceImpl,trace);
    }
    
}
  • 인터페이스를 활용한 프록시 생성과 마찬가지로 실제 객체를 상속받아 생성한 프록시를 스프링 빈으로 등록하고,
    각 프록시에 실제 객체를 주입해준다.

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

  • 인터페이스 기반 프록시는 인터페이스만 같다면 프록시를 적용할 수 있다.

    • 인터페이스 자체가 없다면 프록시를 생성할 수 없다.
  • 클래스 기반 프록시는 상속에 따른 제약이 몇가지 있다.

    • 부모 클래스의 생성자를 호출해야 한다.
    • 부모 클래스에 final키워드가 붙어 있다면 상속이 불가능하다.
    • final 키워드가 붙은 메서드는 오버라이딩이 불가능하다.

인터페이스의 가장 큰 장점은 구현체 변경이 편리하다는 것이다. 하지만 구현체를 변경할 가능성이 적은 곳에서 굳이 인터페이스를 생성해서 사용하는 것은 번거롭다. 따라서 상황에 적합한 방법을 사용하자.

정리

이번 포스팅에서는 인터페이스 기반으로 프록시를 생성하는 방법과 클래스 기반으로 프록시를 생성하는 방법에 대해 알아보았다.
인터페이스를 사용할 경우 인터페이스를 구현해 프록시를 생성하면 되고 클래스를 사용할 경우 상속을 이용해 프록시를 생성하면 된다. 스프링에는 프록시를 빈으로 등록하고, 각 프록시에 실제 객체를 주입 받는다. 또 각 실제 객체에는 다시 프록시를 주입해준다.

그런데 여기서 드러나는 문제가 있다.
프록시를 적용하는 클래스마다 프록시 클래스를 새로 생성해야 하는 것이다. 그만큼 반복되는 코드가 늘어나는 것이다.
그렇다면 하나의 프록시 클래스만 만들어서 모든 클래스에 적용하는 방법은 없을까?
다음 포스팅에서 알아보자.

profile
꾸준히 하자!

0개의 댓글