반응형

본 내용은 인프런의 김영한 님의 강의 "스프링 핵심 원리 - 고급편" 내용을 바탕으로 정리한 내용입니다.

프록시 팩토리란?

  • 스프링 프레임워크에서 지원하는 기능으로, 동적 프록시를 편리하게 생성해주는 역할을 한다.
  • 프록시 객체를 생성하고 필요한 설정을 처리하여 반환한다.
  • 프록시 팩토리는 주로 인터페이스의 유무에 따라 JDK 동적 프록시 또는 CGLIB를 사용하여 프록시를 생성한다.

프록시 팩토리의 Advice 만들기

  • Advice 는 프록시에 적용하는 부가 기능 로직이다.
  • Advice만 작성하면 프록시 팩토리가 내부적으로 해당 프록시를 생성하고 관리한다.

Spring에서 프록시의 실행 로직을 위해 제공하는 인터셉터

package org.aopalliance.intercept; 

public interface MethodInterceptor extends Interceptor { 
    Object invoke(MethodInvocation invocation) throws Throwable; 
} 

프록시 팩토리 활용 예시

ConcreteService.class

  • call() 에서 로그를 출력한다.
package hello.proxy.common.service;  

import lombok.extern.slf4j.Slf4j;  

@Slf4j  
public class ConcreteService {  
    public void call() {  
        log.info("ConcreteService 호출");  
    }  
}

ServiceInterface.interface

package hello.proxy.common.service;  

public interface ServiceInterface {  
    void save();
    void find();  
}

ServiceImpl.class

  • save() 와 find() 메서드에서 로그를 출력한다.
package hello.proxy.common.service;  

import lombok.extern.slf4j.Slf4j;  

@Slf4j  
public class ServiceImpl implements ServiceInterface {  
    @Override  
    public void save() {  
        log.info("save 호출");  
    }  

    @Override  
    public void find() {  
        log.info("find 호출");  
    }  
}

TimeAdvice.class

  • MethodInterceptor를 구현하여 메서드의 실행 시간을 로그로 남기는 어드바이스를 제공한다.
package hello.proxy.common.advice;  

import lombok.extern.slf4j.Slf4j;  
import org.aopalliance.intercept.MethodInterceptor;  
import org.aopalliance.intercept.MethodInvocation;  

@Slf4j  
public class TimeAdvice implements MethodInterceptor {  
    @Override  
    public Object invoke(MethodInvocation invocation) throws Throwable {  
        log.info("TimeProxy 실행");  
        long startTime = System.currentTimeMillis();  

        // 알아서 target 클래스를 호출하고 그 결과를 받는다.  
        Object result = invocation.proceed();  

        long endTime = System.currentTimeMillis();  
        long resultTime = endTime - startTime;  
        log.info("TimeProxy 종료 resultTime={}", resultTime);  
        return result;  
    }  
}

ProxyFactoryTest.class

  • 테스트 실행 코드
  • AopUtils 클래스에서 프록시 팩토리 사용시 사용가능한 메소드를 제공한다.
  • AopUtils.isAopProxy : 프록시가 생성했는지 판단
  • AopUtils.isJdkDynamicProxy : JDK 동적 프록시로 생성했는지 판단
  • AopUtils.isCglibProxy : CGLIB 동적 프록시를 생성했는지 판단
package hello.proxy.proxyfactory;  

import hello.proxy.common.advice.TimeAdvice;  
import hello.proxy.common.service.ConcreteService;  
import hello.proxy.common.service.ServiceImpl;  
import hello.proxy.common.service.ServiceInterface;  
import lombok.extern.slf4j.Slf4j;  
import org.assertj.core.api.Assertions;  
import org.junit.jupiter.api.DisplayName;  
import org.junit.jupiter.api.Test;  
import org.springframework.aop.framework.ProxyFactory;  
import org.springframework.aop.support.AopUtils;  

import static org.assertj.core.api.Assertions.*;  

@Slf4j  
public class ProxyFactoryTest {

    @Test  
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")  
    void interfaceProxy() {  
        ServiceInterface target = new ServiceImpl();
        // target을 등록한다.
        ProxyFactory proxyFactory = new ProxyFactory(target);  
        proxyFactory.addAdvice(new TimeAdvice());  
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();  
        log.info("targetClass={}", target.getClass());  
        log.info("proxyClass={}", proxy.getClass());

        proxy.save();  

        assertThat(AopUtils.isAopProxy(proxy)).isTrue();  
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();  
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
    }

    @Test  
    @DisplayName("구체 클래스만 있으면 CGLIB 사용")  
    void concreteProxy() {  
        ConcreteService target = new ConcreteService();  
        ProxyFactory proxyFactory = new ProxyFactory(target);  
        proxyFactory.addAdvice(new TimeAdvice());  
        ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();  
        log.info("targetClass={}", target.getClass());  
        log.info("proxyClass={}", proxy.getClass());  

        proxy.call();  

        assertThat(AopUtils.isAopProxy(proxy)).isTrue();  
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();  
        assertThat(AopUtils.isCglibProxy(proxy)).isTrue();  
    }
}

테스트 결과

ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl
ProxyFactoryTest - proxyClass=class com.sun.proxy.$Proxy9
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=0

targetClass=class hello.proxy.common.service.ConcreteService
ProxyFactoryTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerBySpringCGLIB$$f8064e9a
TimeAdvice - TimeProxy 실행
TimeAdvice - TimeProxy 종료 resultTime=9
  • interfaceProxy() : 인터페이스를 구현한 클래스에 대해 JDK 동적 프록시를 생성하는 것을 테스트한다.
  • concreteProxy() : 메서드에서 구체 클래스에 대해 CGLIB 프록시를 생성하는 것을 테스트한다.
  • 프록시 팩토리는 proxyTargetClass 라는 옵션을 제공하는데, 이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용하고 클래스 기반의 프록시를 생성한다.
  • 스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 사용한다.

포인트컷(Pointcut)

  • AOP(Aspect-Oriented Programming)에서 부가 기능을 적용할 조인트 포인트(메서드 실행 지점 등)를 선별하는 필터링 규칙이다.
  • 일반적으로 클래스와 메서드 이름으로 필터링한다.
  • 대상 여부를 확인하는 필터 역할을 담당한다.

어드바이스(Advice)

  • 포인트컷에 의해 선택된 조인트 포인트에 적용되는 부가 기능으로 메서드 호출 전후에 로깅이나 트랜잭션 관리 등의 기능을 수행할 수 있다.
  • 부가 기능 로직을 담당한다.

어드바이저(Advisor)

  • 하나의 포인트컷과 하나의 어드바이스를 결합한 것으로, 부가 기능을 어디에(포인트컷) 적용할지와 어떤 기능(어드바이스)을 적용할지를 결정한다.
  • 포인트 컷에 필터링 되는 프록시들만 어드바이스를 적용한다.

외우기 좋은 Tip

  • 조언자( Advisor )는 어디( Pointcut )에 조언( Advice )을 해야할지 알고 있다.

스프링에서 제공해주는 포인트 컷 활용

AdvisorTest.class

package hello.proxy.advisor;  

import hello.proxy.common.advice.TimeAdvice;  
import hello.proxy.common.service.ServiceImpl;  
import hello.proxy.common.service.ServiceInterface;  
import lombok.extern.slf4j.Slf4j;  
import org.junit.jupiter.api.DisplayName;  
import org.junit.jupiter.api.Test;  
import org.springframework.aop.ClassFilter;  
import org.springframework.aop.MethodMatcher;  
import org.springframework.aop.Pointcut;  
import org.springframework.aop.framework.ProxyFactory;  
import org.springframework.aop.support.DefaultPointcutAdvisor;  
import org.springframework.aop.support.NameMatchMethodPointcut;  

import java.awt.*;  
import java.lang.reflect.Method;  

@Slf4j  
public class AdvisorTest {

    @Test  
    void advisorTest() {  
        ServiceInterface target = new ServiceImpl();  
        ProxyFactory proxyFactory = new ProxyFactory(target);  
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());  
        proxyFactory.addAdvisor(advisor);  
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();  

        proxy.save();  
        proxy.find();  
    }

    @Test
    void saveAdvisorTest() {  
        ServiceInterface target = new ServiceImpl();  
        ProxyFactory proxyFactory = new ProxyFactory(target);  
        // 포인트 컷 : 메서드 이름이 save 인 경우만 어드바이스를 적용한다.
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();  
        pointcut.setMappedNames("save");  
        // 포인트컷과 어드바이저를 변수로 넘겨 어드바이저를 만든다.
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());  
        proxyFactory.addAdvisor(advisor);  
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();  

        proxy.save();  
        proxy.find();  
    }   
}

테스트 결과

TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=0
TimeAdvice - TimeProxy 실행
ServiceImpl - find 호출
TimeAdvice - TimeProxy 종료 resultTime=0

ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl
ProxyFactoryTest - proxyClass=class com.sun.proxy.$Proxy9
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=0
  • advisorTest() 메서드 실행 시 Spring AOP를 사용하여 ServiceImpl 클래스의 모든 메서드(savefind)에 대해 TimeAdvice를 적용시킨다.
  • saveAdvisorTest() 메서드 실행 시 Spring AOP를 사용하여 ServiceImplsave 메서드에만 TimeAdvice를 적용하는 방법을 보여준다.

스프링에서 제공하는 포인트 컷

1. NameMatchMethodPointcut
  • 메서드 이름을 기반으로 매칭한다.
    2. JdkRegexpMethodPointcut
  • JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
    3. TruePointcut
  • 항상 참을 반환한다.
    4. AnnotationMatchingPointcut
  • 애노테이션으로 매칭한다.
    5. AspectJExpressionPointcut
  • aspectJ 표현식으로 매칭한다.
  • 실무에서 가장 사용하기 좋은 포인트 컷이다.

하나의 프록시에 여러개의 어드바이저 적용하기

  • 스프링의 AOP는 적용 수 만큼 프록시가 생성되는 것이 아니라 하나의 프록시에 여러 어드바이저를 적용시킬 수 있다.

MultiAdvisorTest.class

package hello.proxy.advisor;  

import hello.proxy.common.advice.TimeAdvice;  
import hello.proxy.common.service.ServiceImpl;  
import hello.proxy.common.service.ServiceInterface;  
import lombok.extern.slf4j.Slf4j;  
import org.aopalliance.intercept.MethodInterceptor;  
import org.aopalliance.intercept.MethodInvocation;  
import org.junit.jupiter.api.DisplayName;  
import org.junit.jupiter.api.Test;  
import org.springframework.aop.Pointcut;  
import org.springframework.aop.framework.ProxyFactory;  
import org.springframework.aop.support.DefaultPointcutAdvisor;  
import org.springframework.aop.support.NameMatchMethodPointcut;  

public class MultiAdvisorTest {

    @Test  
    @DisplayName("하나의 프록시, 여러 어드바이저")  
    void multiAdvisorTest() {  
        //client -> proxy -> advisor2 -> advisor1 -> target  

        DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());  
        DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());  

        //프록시1 생성  
        ServiceInterface target = new ServiceImpl();  
        ProxyFactory proxyFactory1 = new ProxyFactory(target);

        // 어드바이저를 2개 등록한다.
        proxyFactory1.addAdvisor(advisor2);  
        proxyFactory1.addAdvisor(advisor1);  
        ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();  

        //실행  
        proxy.save();
    }

    @Slf4j  
    static class Advice1 implements MethodInterceptor {  
        @Override  
        public Object invoke(MethodInvocation invocation) throws Throwable {  
            log.info("advice1 호출");  
            return invocation.proceed();  
        }  
    }

    @Slf4j  
    static class Advice2 implements MethodInterceptor {  
        @Override  
        public Object invoke(MethodInvocation invocation) throws Throwable {  
            log.info("advice2 호출");  
            return invocation.proceed();  
        }  
    }
}

테스트 결과

MultiAdvisorTest$Advice2 - advice2 호출
MultiAdvisorTest$Advice1 - advice1 호출
ServiceImpl - save 호출
  • 하나의 프록시에 여러 어드바이저가 등록되어 실행된다.

스프링 AOP 어드바이스 적용 예시

LogTraceAdvice.class

  • 메소드 호출 전후에 로깅을 수행하는 로깅 어드바이스를 구현한다.
package hello.proxy.config.v3_proxyfactory.advice;  

import hello.proxy.trace.TraceStatus;  
import hello.proxy.trace.logtrace.LogTrace;  
import org.aopalliance.intercept.MethodInterceptor;  
import org.aopalliance.intercept.MethodInvocation;  

import java.lang.reflect.Method;  

public class LogTraceAdvice implements MethodInterceptor {  

    private final LogTrace logTrace;  

    public LogTraceAdvice(LogTrace logTrace) {  
        this.logTrace = logTrace;  
    }  

    @Override  
    public Object invoke(MethodInvocation invocation) throws Throwable {  
        TraceStatus status = null;  
        try {  
            Method method = invocation.getMethod();  
            String message = method.getDeclaringClass().getSimpleName() + "." +  
                    method.getName() + "()";  
            status = logTrace.begin(message);  

            //로직 호출  
            Object result = invocation.proceed();  

            logTrace.end(status);  
            return result;  
        } catch (Exception e) {  
            logTrace.exception(status, e);  
            throw e;  
        }  
    }  
}

ProxyFactoryConfigV1.class

  • 인터페이스가 존재하는 애플리케이션의 프록시를 설정하는 구성 클래스
package hello.proxy.config.v3_proxyfactory;  

import hello.proxy.app.v1.*;  
import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;  
import hello.proxy.trace.logtrace.LogTrace;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.aop.Advisor;  
import org.springframework.aop.framework.ProxyFactory;  
import org.springframework.aop.support.DefaultPointcutAdvisor;  
import org.springframework.aop.support.NameMatchMethodPointcut;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  

@Slf4j  
@Configuration  
public class ProxyFactoryConfigV1 {  

    @Bean  
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {  
        OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));  
        ProxyFactory factory = new ProxyFactory(orderController);  
        factory.addAdvisor(getAdvisor(logTrace));  
        OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();  
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());  
        return proxy;  
    }  

    @Bean  
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {  
        OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));  
        ProxyFactory factory = new ProxyFactory(orderService);  
        factory.addAdvisor(getAdvisor(logTrace));  
        OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();  
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());  
        return proxy;  
    }  

    @Bean  
    public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {  
        OrderRepositoryV1Impl orderRepository = new OrderRepositoryV1Impl();  

        ProxyFactory factory = new ProxyFactory(orderRepository);  
        factory.addAdvisor(getAdvisor(logTrace));  
        OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();  
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());  
        return proxy;  
    }  

    /**  
     * 포인트 컷 사용하여 메소드 이름으로 필터링  
     * @param logTrace  
     * @return  
     */  
    private Advisor getAdvisor(LogTrace logTrace) {  
        //pointcut  
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();  
        pointcut.setMappedNames("request*", "order*", "save*");  
        //advice  
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);  
        return new DefaultPointcutAdvisor(pointcut, advice);  
    }  
}

ProxyFactoryConfigV2.class

  • 구현 클래스만 존재하는 애플리케이션의 프록시를 설정하는 구성 클래스
package hello.proxy.config.v3_proxyfactory;  

import hello.proxy.app.v2.OrderControllerV2;  
import hello.proxy.app.v2.OrderRepositoryV2;  
import hello.proxy.app.v2.OrderServiceV2;  
import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;  
import hello.proxy.trace.logtrace.LogTrace;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.aop.Advisor;  
import org.springframework.aop.framework.ProxyFactory;  
import org.springframework.aop.support.DefaultPointcutAdvisor;  
import org.springframework.aop.support.NameMatchMethodPointcut;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  

@Slf4j  
@Configuration  
public class ProxyFactoryConfigV2 {  

    @Bean  
    public OrderControllerV2 orderControllerV2(LogTrace logTrace) {  
        OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));  
        ProxyFactory factory = new ProxyFactory(orderController);  
        factory.addAdvisor(getAdvisor(logTrace));  
        OrderControllerV2 proxy = (OrderControllerV2) factory.getProxy();  
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());  
        return proxy;  
    }  

    @Bean  
    public OrderServiceV2 orderServiceV2(LogTrace logTrace) {  
        OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));  
        ProxyFactory factory = new ProxyFactory(orderService);  
        factory.addAdvisor(getAdvisor(logTrace));  
        OrderServiceV2 proxy = (OrderServiceV2) factory.getProxy();  
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());  
        return proxy;  
    }  

    @Bean  
    public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {  
        OrderRepositoryV2 orderRepository = new OrderRepositoryV2();  

        ProxyFactory factory = new ProxyFactory(orderRepository);  
        factory.addAdvisor(getAdvisor(logTrace));  
        OrderRepositoryV2 proxy = (OrderRepositoryV2) factory.getProxy();  
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());  
        return proxy;  
    }  

    /**  
     * 포인트 컷 사용하여 메소드 이름으로 필터링  
     * @param logTrace  
     * @return  
     */ 
    private Advisor getAdvisor(LogTrace logTrace) {  
        //pointcut  
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();  
        pointcut.setMappedNames("request*", "order*", "save*");  
        //advice  
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);  
        return new DefaultPointcutAdvisor(pointcut, advice);  
    }  

}

ProxyApplication.class

  • ProxyFactoryConfigV1ProxyFactoryConfigV2를 임포트하여 설정하고, LogTrace 빈을 등록하여 서버 기동 시 애플리케이션에서 적용할 수 있다.
package hello.proxy;  

import hello.proxy.config.v3_proxyfactory.ProxyFactoryConfigV1;  
import hello.proxy.config.v3_proxyfactory.ProxyFactoryConfigV2;
import hello.proxy.trace.logtrace.LogTrace;  
import hello.proxy.trace.logtrace.ThreadLocalLogTrace;  
import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Import;  

@Import(ProxyFactoryConfigV1.class, ProxyFactoryConfigV2.class)   
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의  
public class ProxyApplication {  

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

    /**  
     * 동시성 문제 해결  
     * @return  
     */  
    @Bean  
    public LogTrace logTrace() {  
       return new ThreadLocalLogTrace();  
    }  
}
반응형

'Spring' 카테고리의 다른 글

[스프링] @Aspect  (0) 2024.05.18
[스프링] 빈 후처리기란?  (0) 2024.05.11
[스프링] 동적 프록시  (0) 2024.05.11
[스프링] 프록시란?  (0) 2024.05.11
[스프링] 템플릿 메서드 패턴  (0) 2024.05.04

+ Recent posts