[김영한 스프링 review] 스프링 핵심 원리 - 고급편 (4)

조갱·2024년 5월 6일
0

스프링 강의

목록 보기
16/23

이 포스팅을 읽기 전에 이전 포스팅 ([김영한 스프링 review] 스프링 핵심 원리 - 고급편 (3) - 프로젝트 준비) 에 있는 예제코드를 미리 준비해주세요.!

v1 - 인터페이스가 있는 구현 클래스에 적용

그림으로 보는 의존관계

프록시 적용 전 컴파일 시점 클래스 의존관계

프록시 적용 전 런타임 시점 클래스 의존관계

프록시 적용 후 컴파일 시점 클래스 의존관계

프록시 적용 후 런타임 시점 클래스 의존관계

OrderControllerInterfaceProxy

import hello.proxy.app.v1.OrderControllerInterface;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class OrderControllerInterfaceProxy implements OrderControllerInterface {

    private final OrderControllerInterface 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();
    }
}

OrderServiceInterfaceProxy

import hello.proxy.app.v1.OrderServiceInterface;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class OrderServiceInterfaceProxy implements OrderServiceInterface {

    private final OrderServiceInterface 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;
        }
    }
}

OrderRepositoryInterfaceProxy

import hello.proxy.app.v1.OrderRepositoryInterface;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class OrderRepositoryInterfaceProxy implements OrderRepositoryInterface {

    private final OrderRepositoryInterface 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;
        }
    }
}

InterfaceProxyConfig

import hello.proxy.app.v1.*;
import hello.proxy.config.v1_proxy.interface_proxy.OrderControllerInterfaceProxy;
import hello.proxy.config.v1_proxy.interface_proxy.OrderRepositoryInterfaceProxy;
import hello.proxy.config.v1_proxy.interface_proxy.OrderServiceInterfaceProxy;
import hello.proxy.trace.logtrace.LogTrace;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

Application

// 빈 중복등록을 피하기 위해, 기존 컨피그를 주석처리한다.
// @Import({AppV1Config.class, AppV2Config.class})
@Import(InterfaceProxyConfig.class)
@SpringBootApplication(scanBasePackages = "{로직 패키지}")
public class ProxyApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProxyApplication.class, args);
    }

	// 앞으로 사용할 예제에서 계속 사용하기 위해 여기에 빈 등록함
    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }
}

실행 결과

http://localhost:8080/v1/request?itemId=hello

[65b39db2] OrderController.request()
[65b39db2] |-->OrderService.orderItem()
[65b39db2] |   |-->OrderRepository.save()
[65b39db2] |   |<--OrderRepository.save() time=1002ms
[65b39db2] |<--OrderService.orderItem() time=1002ms
[65b39db2] OrderController.request() time=1003ms

v2 - 인터페이스가 없는 구체 클래스에 적용

OrderControllerConcreteProxy

아래 코드를 보면, 생성자에서 super(null)을 호출하는 것을 볼 수 있다.

이는 클래스 기반 프록시의 단점인데, 자바에서는 자식 클래스를 생성할 때 항상 super()를 통해 부모 생성자를 호출해야 한다. (생략 시 기본생성자)

하지만 부모 클래스인 OrderServiceConcrete는 기본 생성자가 없기 때문에 명시적으로 생성자를 호출해줘야한다. -> super(null)

프록시는 부모 객체의 기능을 사용하지 않기 때문에 super(null) 을 입력해도 무방하다. 인터페이스 기반 프록시는 이런 고민을 하지 않아도 된다.

import hello.proxy.app.v2.OrderControllerConcrete;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;

public class OrderControllerConcreteProxy extends OrderControllerConcrete {

    private final OrderControllerConcrete target;
    private final LogTrace logTrace;

    public OrderControllerConcreteProxy(OrderControllerConcrete 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;
        }
    }
}

OrderServiceConcreteProxy

import hello.proxy.app.v2.OrderServiceConcrete;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;

public class OrderServiceConcreteProxy extends OrderServiceConcrete {

    private final OrderServiceConcrete target;
    private final LogTrace logTrace;

    public OrderServiceConcreteProxy(OrderServiceConcrete 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;
        }
    }
}

OrderRepositoryConcreteProxy

import hello.proxy.app.v2.OrderRepositoryConcrete;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;

public class OrderRepositoryConcreteProxy extends OrderRepositoryConcrete {

    private final OrderRepositoryConcrete target;
    private final LogTrace logTrace;

    public OrderRepositoryConcreteProxy(OrderRepositoryConcrete 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;
        }
    }
}

ConcreteProxyConfig

import hello.proxy.app.v2.OrderControllerConcrete;
import hello.proxy.app.v2.OrderRepositoryConcrete;
import hello.proxy.app.v2.OrderServiceConcrete;
import hello.proxy.config.v1_proxy.concrete_proxy.OrderControllerConcreteProxy;
import hello.proxy.config.v1_proxy.concrete_proxy.OrderRepositoryConcreteProxy;
import hello.proxy.config.v1_proxy.concrete_proxy.OrderServiceConcreteProxy;
import hello.proxy.trace.logtrace.LogTrace;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConcreteProxyConfig {

    @Bean
    public OrderControllerConcrete orderControllerConcrete(LogTrace logTrace) {
        OrderControllerConcrete controllerImpl = new OrderControllerConcrete(orderServiceConcrete(logTrace));
        return new OrderControllerConcreteProxy(controllerImpl, logTrace);
    }

    @Bean
    public OrderServiceConcrete orderServiceConcrete(LogTrace logTrace) {
        OrderServiceConcrete serviceImpl = new OrderServiceConcrete(orderRepositoryConcrete(logTrace));
        return new OrderServiceConcreteProxy(serviceImpl, logTrace);
    }

    @Bean
    public OrderRepositoryConcrete orderRepositoryConcrete(LogTrace logTrace) {
        OrderRepositoryConcrete repositoryImpl = new OrderRepositoryConcrete();
        return new OrderRepositoryConcreteProxy(repositoryImpl, logTrace);
    }
}

Application

//@Import({AppV1Config.class, AppV2Config.class})
//@Import(InterfaceProxyConfig.class)
@Import(ConcreteProxyConfig.class)
@SpringBootApplication(scanBasePackages = "{로직 패키지명}")
public class ProxyApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProxyApplication.class, args);
    }

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }
}

정리

프록시를 사용하여, 원본 로직에 손을 대지 않고 로그찍기 기능을 적용할 수 있었다.
인터페이스 기반이나, 클래스 기반이나 모두 프록시를 적용할 수 있었다.
다만, 클래스 기반 프록시는 상속의 단점을 모두 가져간다.

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

인터페이스 기반 프록시는 인터페이스만 같다면 어디에든 적용할 수 있다.
(인터페이스를 구현하는 하위 클래스에도 적용할 수 있다.)
반면에, 클래스 기반 프록시는 해당 클래스에만 적용할 수 있다.

인터페이스 기반 프록시에도 단점이 존재하는데,

  • 항상 인터페이스가 있어야만 한다.
  • 캐스팅과 관련한 단점이 존재한다. (나중에 다시 설명)

지금까지 알아본 프록시의 가장 큰 단점은, 적용할 구체클래스, 인터페이스마다 매번
프록시 클래스를 만들어줘야 한다는 것이다.
우리는 실행 전/후로 로그를 찍는 기능 1개만 공통적으로 적용하는데, 이를 위해 OrderControllerInterfaceProxy, OrderServiceInterfaceProxy, OrderRepositoryInterfaceProxy, OrderContollerConcreteProxy, OrderServiceConcreteProxy, OrderRepositoryConcreteProxy 이렇게 6개를 만든것 처럼 말이다.

이는 이후에 다룰 동적 프록시 기술을 통해 해결할 수 있다.
다음 시간에는 동적 프록시의 근간이 되는 리플렉션에 대해 알아본다.

profile
A fast learner.

0개의 댓글