4 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

인터페이스와 구현체가 있는 V1 App에 지금까지 학습한 프록시를 도입해서 LogTrace를 사용해보자. 프록시를 사용하면 기존 코드를 전혀 수정하지 않고 로그 추적 기능을 도입할 수 있다.

V1 App의 기본 클래스 의존 관계와 런타임시 객체 인스턴스 의존 관계는 다음과 같다.

여기에 로그 추적용 프록시를 추가하면 다음과 같다.

Controller, Service, Repository각각 인터페이스에 맞는 프록시 구현체를 추가한다. (그림에서 리포지토리는 생략했다.)

그리고 애플리케이션 실행 시점에 프록시를 사용하도록 의존 관계를 설정해주어야 한다.

이 부분은 빈을 등록하는 설정 파일을 활용하면 된다.

그럼 실제 프록시를 코드에 적용해 보자.

코드 구현

해당 패키지에 프록시 관련 코드를 작성한다.

리포지토리부터 프록시를 만들어 보자.

OrderRepositoryInterfaceProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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.request()");  
            target.save(itemId);  
            logTrace.end(status);  
        } catch (Exception e) {  
            logTrace.exception(status, e);  
            throw e;  
        }  
    }  
}
  • 프록시를 만들기 위해 인터페이스를 구현하고 구현한 메서드에 LogTrace를 사용하는 로직을 추가한다. 지금까지는 OrderRepositoryImpl에 이런 로직을 추가 했어야 했는데, 프록시를 사용한 덕분에 이 부분을 프록시가 대신 처리해준다. 따라서 OrderRepositoryImpl을 수정할 필요가 없다.
  • OrderRepositoryV1 target으로 실제 OrderRepositoryV1Impl 을 호출할 원본 리포지토리의 참조를 가지게 된다.

OrderServiceInterfaceProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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;  
        }  
    }  
}

앞에 리포지토리와 같고, 로그 추적 로직 사이에 실제 OrderServiceV1Impl을 넣어준다.

OrderControllerInterfaceProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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 request = target.request(itemId);  
            logTrace.end(status);  
            return request;  
        } catch (Exception e) {  
            logTrace.exception(status, e);  
            throw e;  
        }  
    }  
  
    @Override  
    public String noLog() {  
        return target.noLog();  
    }  
}

여기도 마찬가지인데, 여긴 리턴 값이 있어서

String request = target.request(itemId); 으로 리턴 값을 받아서 return 해준다.

noLog()는 로그를 찍으면 안되기 때문에 그냥 바로 target.noLog()해준다.

이제 만든 프록시들을 등록할 Config를 구현하자.

InterfaceProxyConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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);  
    }  
}

다음과 같이 동작한다.(LogTrace는 따로 빈으로 등록해야 한다.)

빨강

은 실제 객체이고, 파랑은 프록시 객체이다.

V1 프록시 런타임 객체 의존 관계 설정

  • 프록시의 런타임 객체 의존 관계를 설정. 기존에는 스프링 빈이 orderControllerV1Impl같은 실제 객체를 반환했다. 하지만 이제는 프록시를 사용해야 한다. 따라서 프록시를 생성하고 프록시를 실제 스프링 빈 대신 등록한다. 실제 객체는 스프링 빈으로 등록하지 않는다.
  • 프록시는 내부에 실제 객체를 참조하고 있다. 위에 사진에서 보이는 것처럼 말이다.
  • 정리하자면 다음과 같은 의존 관계를 가지고 있다.
    • proxy -> target
    • orderServiceInterfaceProxy -> orderServiceV1Impl
  • 스프링 빈으로 실제 객체 대신에 프록시 객체를 등록했기 때문에 앞으로 스프링 빈을 주입 받으면 실제 객체 대신에 프록시 객체가 주입된다.
  • 실제 객체가 스프링 빈으로 등록되지 않는다고 해서 사라지는 것은 아니다. 프록시 객체가 실제 객체를 참조하기 때문에 프록시를 통해서 실제 객체를 호출할 수 있다.
    • 쉽게 이야기 해서 실제 객체를 프록시 객체로 감싼 것이다.

InterfaceProxyConfig를 통해 프록시를 적용한 후

  • 스프링 컨테이너에 프록시 객체가 등록된다. 스프링 컨테이너는 이제 실제 객체가 아니라 프록시 객체를 스프링 빈으로 관리한다.
  • 이제 실제 객체는 스프링 컨테이너랑 상관이 없다. 실제 객체는 프록시 객체를 통해서 참조될 뿐이다.
  • 프록시 객체는 스프링 컨테이너가 관리하고 자바 힙 메모리에도 올라간다. 반면에 실제 객체는 자바 힙 메모리에는 올라가지만 스프링 컨테이너가 관리하지는 않는다.

실제로 이런 런타임 객체 의존 관계가 발생한다.

config 등록

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//@Import(AppV1Config.class)  
//@Import({AppV1Config.class, AppV2Config.class})  
@Import(InterfaceProxyConfig.class)  
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의  
public class ProxyApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(ProxyApplication.class, args);  
    }  
  
    @Bean  
    public LogTrace logTrace() {  
       return new ThreadLocalLogTrace();  
    }  
}

이제 main 메서드에 config를 등록 해주는데

1
@Import(InterfaceProxyConfig.class)  

다음과 같이 등록했다.

1
2
3
4
@Bean  
public LogTrace logTrace() {  
   return new ThreadLocalLogTrace();  
}

그리고 멀티 쓰레드 환경에서 안정적이게 설계했던 ThreadLocalLogTraceLogTrace로 빈 등록 한다.

결과

실행 결과를 확인해보면 로그 추적 기능이 프록시를 통해 잘 동작하는 것을 확인할 수 있다.

정리

추가된 요구 사항을 다시 확인해보자.

추가된 요구 사항

  • 원본 코드를 전혀 수정하지 않고, 로그 추적기를 적용해라.
  • 특정 메서드는 로그를 출력하지 않는 기능
    • 보안 상 일부는 로그를 출력하면 안된다.
  • 다음과 같은 다양한 케이스에 적용할 수 있어야 한다.
    • v1 - 인터페이스가 있는 구현 클래스에 적용
    • v2 - 인터페이스가 없는 구체 클래스에 적용
    • v3 - 컴포넌트 스캔 대상에 기능 적용

프록시와 DI 덕분에 원본 코드를 전혀 수정하지 않고, 로그 추적기를 도입할 수 있었다. 물론 너무 많은 프록시 클래스를 만들어야 하는 단점이 있기는 하다. 이 부분은 나중에 해결하기로 하고,

우선은 v2 - 인터페이스가 없는 구체 클래스에 프록시를 어떻게 적용할 수 있는지 알아보자.

댓글남기기