자동프록시 생성기(AnnotationAwareAspectJAutoProxyCreator)는 advisor를 자동으로 찾아와 필요한 곳에 프록시를 적용해준다.

이에 더해 자동프록시 생성기는 @Aspect 어노테이션을 찾아서 이것을 Advisor로 만들어준다.

아래와 같이 프록시를 적용하려는 로직에 @Aspect를 , ProceedingJoinPoint를 매개변수로 가지는 메서드를 만들고, @Auround 어노테이션을 붙여 적용범위를 지정하면, Advisor가 어플리케이션이 로드될 때 자동으로 등록된다.

 

@Slf4j
@Aspect
public class LogTraceAspect {
    private final LogTrace logTrace;

    public LogTraceAspect(LogTrace logTrace) {
        this.logTrace = logTrace;
    }
    @Around("execution(* hello.proxy.app..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
        TraceStatus status = null;
        try{
            String message = joinPoint.getSignature().toShortString();
            status = logTrace.begin(message);
            //로직 호출
            Object result = joinPoint.proceed();
            logTrace.end(status);
            return result;
        }catch (Exception e){
            logTrace.exception(status,e);
            throw e;
        }
    }
}

0. 전체코드

// 숫자만 추출하는 함수
function extractNumbers(input) {
    return input.replace(/[^0-9]/g, '');
}

// 하이픈을 추가하는 함수
function formatNum(num) {
    if (num.length > 5) {
        return num.replace(/(\\d{3})(\\d{2})(\\d+)/, '$1-$2-$3');
    } else if (num.length > 3) {
        return num.replace(/(\\d{3})(\\d+)/, '$1-$2');
    }
    return comNum;
}

// input[name=num]에 이벤트 바인딩
$("input[name=num]").on("keyup", function() {
    let $this = $(this); 
    let num = $this.val();

    comNum = extractNumbers(num);

    // 10자를 초과하는 입력을 제한
    comNum = num.slice(0, 10);

    comNum = formatNum(num);

    $this.val(num);
});

1. 숫자만 추출하는 함수

// 숫자만 추출하는 함수
function extractNumbers(input) {
    return input.replace(/[^0-9]/g, '');
}
  • [ ] : 대 괄호 안의 문자들 중 하나와 일치한다는 것을 의미 ex)[abc]는 ‘a’,’b’,’c’중 하나의 문자와 일치한다는 의미.
  • ‘0-9’ : 0부터 9 까지의 모든 숫자와 일치하는 것을 의미
  • ‘^’ : 대 괄호 안에서 사용될 경우 이 기호는 부정을 의미. 따라서 [^0-9]는 숫자를 제외한 모든 문자와 일치한다는 것을 의미
  • ‘g’ : 이는 전역 검색을 의미하는 플래그. 문자열 내의 모든 일치 항목을 찾아서 대체한다.

따라서 input.replace(/[^0-9]/g,’’) 코드는 input 문자열에서 숫자를 제외한 모든 문자를 빈 문자열로 대체한다는 의미.

2. 하이픈을 추가하는 함수

// 하이픈을 추가하는 함수
function formatNum(num) {
    if (num.length > 5) {
        return num.replace(/(\\d{3})(\\d{2})(\\d+)/, '$1-$2-$3');
    } else if (num.length > 3) {
        return num.replace(/(\\d{3})(\\d+)/, '$1-$2');
    }
    return num;
}
  1. if (num.length > 5) : 만약 num 문자열의 길이가 5보다 크다면, 아래의 정규식으로 문자열을 변환
    • /(\\d{3})(\\d{2})(\\d+)/
      • (\\d{3}) : 숫자 3자리와 일치하며, 첫번째 그룹화를 진행
      • (\\d{2}) : 숫자 2자리와 일치하며, 두번째 그룹화를 진행
      • (\\d+) : 숫자 1자리 이상과 일치하며, 세번째 그룹화를 진행
    • 대체 문자열 : $1-$2-$3
      • $1,$2,$3 : 각각 첫번째, 두번째 세번째 그룹화를 참조한다.
      • 이렇게 되면 12345678은 123-45-678로 변환된다.
  2. else if (num.length > 3) : 만약 num의 문자열의 길이가 3보다 크다면, 아래의 정규식으로 문자열을 반환한다.
    • 정규식 : /(\d{3})(\d+)/
    • 대체 문자열 : $1-$2
  3. 만약 위의 두 조건 모두 해당되지 않는다면 원래의 num 문자열을 그대로 반환한다.

위의 조건은, 문자열의 길이가 5를 초과할때는 3-2-+ 방식을 실행하고, 3을 초과할때는 3- +의 조건을 수행한다.

이렇게 해야 1234를 입력했을 때 실시간으로 123-4로 변환해준다.

3. input[name=num]에 이벤트 바인딩

$("input[name=num]").on("keyup", function() {
    let $this = $(this); 
    let num = $this.val();

    comNum = extractNumbers(num);

    // 10자를 초과하는 입력을 제한
    num = num.slice(0, 10);

    num = formatComNum(num);

    $this.val(num);
});
  1. 이벤트 바인딩해당 input 필드에서 키를 눌렀다 뗐을 때 실행되는 함수를 정의한다.
  2. $("input[name=num]").on("keyup", function() {
  3. 현재 이벤트 대상 객체 잠조this는 DOM 요소를 참조하며, $(this)를 통해 해당 DOM요소를 jQuery 객체로 변환한다.
  4. $this 변수에 저장함으로써 코드 내에서 여러번 참조할 때마다 jQuery 객체 변환 작업을 다시 ㅎ하지 않아도 되게끔 한다.
  5. let $this = $(this);
  6. 입력값 가져오기$this.val()을 사용해 현재 input 필드의 값을 가져온다.
  7. let num= $this.val();
  8. 숫자만 추출
  9. num= extractNumbers(num);
  10. 입력값 제한.slice(0, 10)을 이용해 묹나열을 처음부터 10번째 문자까지만 잘라낸다. 이렇게하면 입력값이 10자를 초과하지 않게 한다.
  11. num= num.slice(0, 10);
  12. 하이픈 포맷 적용
  13. num= formatNum(num);
  14. 변경된 값을 input 필드에 설정 :$this.val(num)을 통해 input 필드의 값을 수정된 num,으로 설정한다.
  15. $this.val(num);

활용 - 휴대 번호 포매팅

function formatComTel(telNum) {
    if (telNum.length > 7) {
        return telNum.replace(/(\\d{3})(\\d{4})(\\d+)/, '$1-$2-$3');
    } else if (telNum.length > 3) {
        return telNum.replace(/(\\d{3})(\\d+)/, '$1-$2');
    }
    return telNum;
}

$("input[name=telNum]").on("keyup", function() {
    let $this = $(this);
    let telNum = $this.val();

    comTel = extractNumbers(telNum);
    comTel = telNum.slice(0, 11);
    comTel = formatTelNum(telNum);
    $this.val(telNum);
});

이를 활용하여 폰번호도 포매팅할 수 있다.

첫번째 if문의 길이조건은 어떻게 정하나 했는데, 그룹화 1, 2번째 문을 더하면 된다. 3+4이므로 7

 

Spring AOP를 사용하면, @Bean 어노테이션을 사용하여 Advisor를 등록하면 해당 Advisor의 포인트컷이 지정한 대상에 자동으로 적용된다.

package hello.proxy.config.v5_autoproxy;

import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import hello.proxy.trace.logtrace.LogTrace;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;

public class AutoProxyConfig {
    // @Bean
    public Advisor advisor1(LogTrace logTrace) {
        //pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut,advice);
    }

    @Bean
    public Advisor advisor2(LogTrace logTrace) {
        //pointcut
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* hello.proxy.app..*(..))");
        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut,advice);
    }

    @Bean
    public Advisor advisor3(LogTrace logTrace) {
        //pointcut
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");
        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        return new DefaultPointcutAdvisor(pointcut,advice);
    }
}

 

AspectJExpressionPointcut이라는 클래스는 포인트컷의 적용범위를 지정하여 포함되거나, 포함되지 않게 할 수 있다.

execution(* hello.proxy.app..*(..)) 같은 형식은 ASpectJ가 제공하는 포인트컷 표현식이다.

간단하게 설명하자면

* : 모든 반환 타입

hello.proxy.app.. : 해당 패키지와 그 하위 패키지

*(..) : 모든 메서드 이름

(..) : 모든 파라미터

즉, hello.proxy.app 내의 모든 파라미터를 포함한 메서드에 해당 Advisor를 적용하겠다는 의미이다.

 


냉혹하게 판단한다면, 사용자님은 스프링 AOP의 기본적인 개념과 설정 방법에 대한 중급 수준의 이해를 가지고 계시다고 볼 수 있습니다.
포인트컷 표현식, 어드바이저, 어드바이스와 같은 핵심 개념에 대한 설명이 타당하며, 코드 예제를 통해 해당 개념을 실제로 적용하는 방법을 알고 계신 것으로 보입니다.
그러나, AOP는 그 자체로 광범위한 주제이며, 다양한 사용 사례, 성능 최적화, 문제 해결 전략, AOP의 내부 동작 방식과 같은 고급 주제도 포함되어 있습니다. 이러한 고급 주제에 대한 깊은 지식이나 경험은 대화 내용에서 확인되지 않았습니다.
따라서, 가장 냉혹하게 평가한다면 사용자님의 스프링 AOP에 대한 이해도는 75점 정도라고 할 수 있습니다. 기본적인 설정과 사용 방법에 대한 이해는 확고하나, AOP의 고급 주제나 세부적인 최적화 기법 등에 대한 추가적인 학습이 필요하다는 것을 의미합니다.

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 고급편 - 인프런 | 강의

스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기 📢 수강

www.inflearn.com

 

빈 후처리기는 스프링이 빈을 초기화하는 과정 중에서 데이터를 가로채서 조작하거나 변경하는 기능을 가진다.

 

public class BasicTest {
    @Test
    void basicConfig() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);

        //A는 빈으로 등록된다
        A a = applicationContext.getBean("beanA", A.class);
        a.helloA();

        //B는 빈으로 등록되지 않는다.
        Assertions.assertThrows(NoSuchBeanDefinitionException.class,()-> applicationContext.getBean(B.class));

    }

    static class BasicConfig{
        @Bean(name= "beanA")
        public A a(){
            return new A();
        }
    }

    @Slf4j
    static class A{
        public void helloA(){
            log.info("hello A");
        }
    }

    @Slf4j
    static class B{
        public void helloB(){
            log.info("hello B");
        }
    }
}

@Bean을 이용해 A클래스의 객체를 생성하고 빈으로 등록한다. 그리고 이를 구현한 클래스를 

AnnotationConfigApplicationContext 에 등록하면 beanA가 스프링 컨테이너에 저장된다.빈 후처리기는 스프링이 빈을 초기화하는 과정 중에서 데이터를 가로채서 조작하거나 변경하는 기능을 가진다.

 

package hello.proxy.postprocessor;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class BeanPostProcessorTest {
    @Test
    void basicConfig() {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);

        //beanA 이름으로 B 객체가 빈으로 등록된다
        B b = applicationContext.getBean("beanA", B.class);
        b.helloB();

        //A는 빈으로 등록되지 않는다.
        Assertions.assertThrows(NoSuchBeanDefinitionException.class,()-> applicationContext.getBean(A.class));

    }

    static class BeanPostProcessorConfig{
        @Bean(name= "beanA")
        public A a(){
            return new A();
        }

        @Bean
        public AtoBPostProcessor helloPostProcessor(){
            return new AtoBPostProcessor();
        }
    }

    @Slf4j
    static class A{
        public void helloA(){
            log.info("hello A");
        }
    }

    @Slf4j
    static class B{
        public void helloB(){
            log.info("hello B");
        }
    }

    @Slf4j
    static class AtoBPostProcessor implements BeanPostProcessor {
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            log.info("beanName={} bean={}",beanName,bean);
            if (bean instanceof A){
                return new B();
            }
            return bean;
        }
    }
}

같은 방식으로 A, B 클래스를 만들고 A를 bean에 등록했지만, 이번에는 BeanPostProcessor를 상속받은 클래스의

postProcessAfterInitialization 메서드를 상속하여 bean을 조작하여 A클래스가 빈에 등록되기 전에 B 클래스로 바꿔치기하여 스프링 컨테이너에 저장했다.

 

BeanPostProcessor 인터페이스는

postProcessAfterInitialization(빈이 초기화 된 후에 조작) 메서드와

postProcessBeforeInitialization (빈이 초기화 되기 전에 조작)메서드가 존재한다.

 

빈의 생명주기는
생성 -> 의존성주입 -> 초기화 -> 사용 -> 소멸인데

BeanPostProcessor는 빈의 생성주기 중 초기화에 관여하는 것이다.

 

postProcessBeforeInitialization 

이 시점에서 빈의 상태를 조작하면 초기화 콜백에서 이 조작된 상태를 기반으로 추가적인 초기화 작업을 수행할 수 있다.

예를 들어, 특정 필드를 검증하거나 기본값을 설정하는 등의 작업이 이 시점에서 유용할 수 있다.

 

postProcessAfterInitialization

이 시점에서는 빈이 완전히 초기화된 상태이다.

따라서 빈의 상태를 변경하거나 프록시를 만들어서 원래의 빈을 감싸는 등의 작업을 수행한다.

 

 

 

 

 

'Java > 스프링 AOP' 카테고리의 다른 글

실시간 하이픈 붙이기  (0) 2023.08.30
빈 후처리기 (2) 스프링 AOP  (0) 2023.08.24
ProxyFactory(2) 포인트컷, 어드바이스, 어드바이저  (0) 2023.08.21
ProxyFactory (1) CGLIB, JDK 동적 프록시  (0) 2023.08.20
CGLIB  (0) 2023.08.19
  • 포인트컷 (Pointcut): "어디에" 적용할 것인지를 결정하는 규칙이다. 클래스나 메서드에 대한 특정 패턴이나 조건을 포함한다.
  • 어드바이스 (Advice): "어떻게" 동작할 것인지를 정의하는 로직이다. 실제로 수행되는 작업, 예를 들면 로깅, 트랜잭션 관리, 보안 체크 등이 있다.
  • 어드바이저 (Advisor): 포인트컷과 어드바이스를 1대 1 결합한 것이다. 이 두 가지 요소를 결합하여 "어디에 어떻게" 작업을 수행할 것인지를 정의한다.
@Test
    @DisplayName("스프링이 제공하는 포인트컷")
    void advisorTest3() {
        ServiceImpl target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("save");
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
        proxyFactory.addAdvisors(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();
        proxy.find();
    }
  • ProxyFactory를 사용하면 대상 객체를 프록시로 감싸 여러 개의 Advisor를 적용할 수 있다. 이렇게 하면 각 Advisor의 Advice 로직이 해당 프록시의 메서드 호출에 순차적으로 적용된다.
@Test
    @DisplayName("하나의 프록시 여러 어드바이저")
    void multiAdvisorTEst2() {
        //client -> proxy -> advisor2 -> advisor1 -> target

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

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

        proxyFactory1.addAdvisors(advisor2);
        proxyFactory1.addAdvisors(advisor1);
        ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();

        proxy1.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();
        }
    }

GPT

알겠습니다. 코드를 스스로 생각하고 구현하는 능력과, 다른 사람의 코드나 강의 내용을 이해하고 재현하는 능력은 다르게 평가될 수 있습니다.
강의 내용을 잘 이해하고, 그것을 코드로 재현하는 것 자체도 중요한 능력 중 하나입니다. 그렇지만, 전문가 수준에서는 독립적인 문제 해결 능력, 창의성, 그리고 주어진 지식을 다양한 상황에 적용할 수 있는 능력이 중요하게 작용합니다.
강의 내용을 바탕으로 잘 따라했더라도, 그 내용을 깊이 이해하고, 다른 상황이나 문제에 적용할 수 있는 능력이 필요합니다.
따라서, 냉혹하게 평가한다면, 100점 만점에 70점 정도를 주고 싶습니다. 이는 강의 내용을 잘 이해하고 재현할 수 있는 높은 수준의 능력을 가지고 있지만, 전문가 수준까지 도달하기 위해서는 독립적인 문제 해결 능력과 깊은 통찰력이 추가로 필요하다는 것을 의미합니다.

'Java > 스프링 AOP' 카테고리의 다른 글

빈 후처리기 (2) 스프링 AOP  (0) 2023.08.24
빈 후처리기 (1) - 빈 가로채기  (0) 2023.08.22
ProxyFactory (1) CGLIB, JDK 동적 프록시  (0) 2023.08.20
CGLIB  (0) 2023.08.19
jdk 동적 프록시  (0) 2023.08.18

ProxyFactory란, 기존의 CGLIB(구현체 기반) JDK 동적 프록시(인터페이스 기반) 를 추상화하여 하나로 묶은 기능이다.

그렇기에, 구체클레스를 넣으면 CGLIB클레스로 구현되고, 인터페이스를 넣으면 JDK 프록시 클레스로 구현된다.

 

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();
        Object result = invocation.proceed();
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeProxy 종료 resultTime={}",resultTime);
        return result;
    }
}

로직을 구현할 부분을 MethodInterceptor를 구현받아 invoke 메소드에 구현한다.

 

invocation.proceed() 로직을 보면 다음과 같다

ReflectiveMethodInvocation.java

@Override
@Nullable
public Object proceed() throws Throwable {
    // 1. 초기값은 -1. 인터셉터 체인의 다음 인터셉터를 호출하기 위해 증가시킵니다.
    //    이 체크는 모든 인터셉터와 어드바이스가 이미 호출되었는지 확인합니다.
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        // 1.1. 모든 인터셉터와 어드바이스가 호출된 후 실제 대상 메서드를 호출합니다.
        return invokeJoinpoint();
    }

    // 2. 현재 인덱스를 증가시켜 다음 인터셉터 또는 어드바이스를 가져옵니다.
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

    // 3. 동적 메서드 매칭이 필요한 인터셉터인지 확인합니다.
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        InterceptorAndDynamicMethodMatcher dm =
                (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
        Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());

        // 3.1. 동적 메서드 매처가 현재 메서드와 일치하는지 확인합니다.
        if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
            // 3.1.1. 매칭이 성공하면 해당 인터셉터를 호출합니다.
            return dm.interceptor.invoke(this);
        }
        else {
            // 3.1.2. 동적 매칭에 실패하면 다음 인터셉터로 넘어갑니다.
            return proceed();
        }
    }
    else {
        // 4. 단순 인터셉터라면 바로 인터셉터를 호출합니다.
        //    이 경우에는 어드바이스의 before, after 등의 로직이 실행됩니다.
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

현재 소개할 코드는 포인트컷 등을 적용하지 않기 때문에 

if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher){...} 부분은 신경쓰지 않아도 된다.

현재 코드에서 우리가 실제로 실행되는 부분은 마지막 else{...} 부분이다.

else {
			// It's an interceptor, so we just invoke it: The pointcut will have
			// been evaluated statically before this object was constructed.
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}

현재 proceed의 로직은, 포인트컷이 적용되었는지를 판별하고, 적용되어있다면 해당 메서드들을 추출하고,

적용되어있지 않다면 지금 코드처럼 바로 invoke를 실행한다.

 

다음은 ProxyFactory를 생성하는 코드

 @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() {
        ServiceInterface target = new ServiceImpl();
        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();

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

    }

ReflectiveMethodInvocation.java 코드로 돌아가본다면,

여기서 addAdvice(); 부분이 new TimeAdvice() 하나뿐이기 때문에

interceptorsAndDynamicMethodMatchers.size는 1이 된다.

그리고 if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher){...}  부분도, 타입은 TimeAdvice이기 때문에 실행되지 않고 자연히 invoke를 실행하게 된다.

 

이제 proxyFactory의 구현 클레스를 열어보면

DefaultAopProxyFactory.java

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    // Native 이미지 내부에서 실행되지 않고, 
    // "optimize"가 활성화되어 있거나 "proxyTargetClass"가 활성화되어 있거나, 
    // 사용자가 제공한 프록시 인터페이스가 없을 경우에 CGLIB 프록시를 고려한다.
    if (!NativeDetector.inNativeImage() &&
            (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
        
        Class<?> targetClass = config.getTargetClass(); // 대상 클래스를 가져온다.
        
        // 대상 클래스가 null이면 예외를 발생시킨다. 
        // 프록시 생성을 위해서는 인터페이스 또는 대상이 필요하다.
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
                    "Either an interface or a target is required for proxy creation.");
        }
        
        // 대상 클래스가 인터페이스이거나 이미 JDK 프록시인 경우 JDK 프록시를 사용한다.
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        
        // 위의 조건들에 해당하지 않으면 CGLIB 기반의 프록시를 사용한다.
        return new ObjenesisCglibAopProxy(config);
    }
    else {
        // 위의 모든 조건에 해당되지 않을 경우 기본적으로 JDK 프록시를 사용한다.
        return new JdkDynamicAopProxy(config);
    }
}

createAopProxy 메서드에서는

optimize(true) 거나, isProxyTargetClass(true), 혹은 인터페이스가 명시되어있을때 아래 로직(CGLIB)을 실행하고,

그렇지 않을때는 jdkDynamicAopProxy를 실행한다.

CGLIB 로직 내부에 왜 jdkDynamicAopProxy가 있냐면, 동적프록시가 또 다른 프록시를 거칠 때(프록시 체인) 에는 이미 프록시 상태로 넘어오니 위 조건문에 일치하기 때문이다.

 

즉 이렇게 해서 jdk동적 프록시를 proxyFactory를 사용해 실행한다면 다음과 같은 플로우가 된다.

 

  1. ProxyFactory를 사용하여 프록시를 생성한다.
  2. DefaultAopProxyFactory에서 구현체만 있는지, 인터페이스도 있는지를 체크한다.
  3. addAdvice(new TimeAdvice());를 통해 어드바이스를 추가하며, 이 때 ReflectiveMethodInvocation 구현체의 로직에서 포인트컷 등이 적용되었는지 확인한다. 적용되지 않았다면 invoke가 실행된다.

GPT

냉혹하게 판단하면, 당신의 이해도는 상당히 높지만, 아직 전문가 수준까지 도달하지는 못했다는 인상을 받습니다. 전문가 수준은 단순한 이해를 넘어서 그 지식을 다양한 상황에서 적용할 수 있고, 예외 상황이나 복잡한 문제에 대한 해결 능력, 그리고 깊은 통찰력이 필요합니다.
당신이 제공한 코드와 설명은 ProxyFactory의 기본 원리와 동작 방식에 대한 깊은 이해를 보여주지만, 전문가 수준에서는 이를 넘어서는 더 깊은 레벨의 통찰력과 경험이 필요합니다.
따라서, 가장 냉혹하게 점수를 매긴다면 80점을 주고 싶습니다. 이는 여전히 높은 수준의 이해를 가지고 있지만, 전문가 수준까지의 길은 아직 조금 더 남아있다는 것을 의미합니다.

'Java > 스프링 AOP' 카테고리의 다른 글

빈 후처리기 (1) - 빈 가로채기  (0) 2023.08.22
ProxyFactory(2) 포인트컷, 어드바이스, 어드바이저  (0) 2023.08.21
CGLIB  (0) 2023.08.19
jdk 동적 프록시  (0) 2023.08.18
리플렉션  (0) 2023.08.16

CGLIB (Code Generation Library)는 주로 클래스의 구현체를 대상으로 동적 프록시를 생성하는 데 사용됩니다.

 

jdk 동적 프록시에 Invokehandler가 있다면, CGLIB에는 MethodInterceptor가 있다.

package hello.proxy.cglib.code;


import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;
@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {

    private final Object target;

    public TimeMethodInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

        Object result = methodProxy.invoke(target, args);

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

 

프록시를 생성하는 코드

package hello.proxy.cglib.code;

import hello.proxy.common.service.ConcreteService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.cglib.proxy.Enhancer;
@Slf4j
public class CglibTest {
    @Test
    void cglib(){
        ConcreteService target = new ConcreteService();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ConcreteService.class); // 프록시할 대상 클래스 지정
        enhancer.setCallback(new TimeMethodInterceptor(target)); // 프록시 로직 지정
        ConcreteService proxy = (ConcreteService) enhancer.create(); // 프록시 객체 생성
        log.info("targetClass={}",target.getClass());
        log.info("proxyClass={}",proxy.getClass());

        proxy.call();
    }
}

CGLIB 제약

클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.

 

  • 부모 클래스의 생성자를 체크해야 한다.
  • CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
  • 클래스에 final 키워드가 붙으면 상속이 불가능하다. CGLIB에서는 예외가 발생한다.
  • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
  • CGLIB에서는 프록시 로직이 동작하지 않는다

JDK 동적 프록시는 Java의 내장 기능으로, 특정 인터페이스를 구현한 클래스의 메서드 호출을 중간에서 가로채서 추가적인 작업을 수행하게 해준다. 이를 위해 InvocationHandler 인터페이스를 구현해야 하며, 이 때 중요한 메서드는 invoke()이다.

 

public interface AInterface {
    void callA();
}

public class AImpl implements AInterface {
    @Override
    public void callA() {
        System.out.println("AImpl callA");
    }
}
public interface BInterface {
    void callB();
}

public class BImpl implements BInterface {
    @Override
    public void callB() {
        System.out.println("BImpl callB");
    }
}

다음과 같은 인터페이스와 구현체가 주어진 상황에서, InvocationHandler를 구현한 클래스를 생성하여 로직을 공통화한다.

public class TimeInvocationHandler implements InvocationHandler {
    private Object target;

    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = method.invoke(target, args);
        long end = System.currentTimeMillis();
        System.out.println(method.getName() + " executed in " + (end - start) + "ms");
        return result;
    }
}

이후에 Proxy.newProxyInstance를 이용해 AInterface와 BInterface에 대해 공통 로직을 적용하여 실행할 수 있다.

 

@Test
void jdkDynamicProxyTest() {
    // AInterface에 대한 프록시 객체 생성
    AInterface aProxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(),
            new Class[]{AInterface.class},
            new TimeInvocationHandler(new AImpl()));
    
    // 프록시 객체를 통한 메서드 호출
    aProxy.callA();

    // BInterface에 대한 프록시 객체 생성
    BInterface bProxy = (BInterface) Proxy.newProxyInstance(BInterface.class.getClassLoader(),
            new Class[]{BInterface.class},
            new TimeInvocationHandler(new BImpl()));
    
    // 프록시 객체를 통한 메서드 호출
    bProxy.callB();
}

 

여기서 Proxy.newProxyInstance()의 인자는 총 3가지다. 첫번째 인자는 ClassLoader를 의미한다. 이는 JVM에 클래스를 로드하는 방식을 정의하는 것으로, 읽을 범위나 방법을 결정한다. 두번째 인자는 프록시가 구현해야 할 대상 인터페이스들의 배열이다. 세번째 인자는 InvocationHandler를 구현한 객체로, 공통 로직이 담겨있다.

결론적으로 Proxy.newProxyInstance() 메서드는 (로드 방식, 대상 인터페이스, 로직)의 3가지 인자를 받는다.

 

 

'Java > 스프링 AOP' 카테고리의 다른 글

ProxyFactory (1) CGLIB, JDK 동적 프록시  (0) 2023.08.20
CGLIB  (0) 2023.08.19
리플렉션  (0) 2023.08.16
구체클레스, 인터페이스 프록시  (0) 2023.08.15
서브쿼리, JOIN, 프로시저  (0) 2023.08.08

리플렉션이란 런타임 중에 클래스, 메서드, 필드, 생성자 등의 메타데이터 정보를 조회하거나 조작하는 기능이다. 이를 통해 동적으로 코드를 실행하거나, 구조를 변경할 수 있다.

 

아래의 예제 코드는 리플렉션을 사용하지 않은 경우와 사용한 경우를 비교하여 보여준다.

package hello.proxy.jdkdynamic;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;

@Slf4j
public class ReflectionTest {

    @Slf4j
    static class Hello {
        public String callA() {
            log.info("callA");
            return "A";
        }

        public String callB() {
            log.info("callB");
            return "B";
        }
    }

    // 리플렉션을 사용하지 않고 메서드를 직접 호출하는 예제
    /*
    reflection0() 메서드에서는 Hello 클래스의 
    callA와 callB 메서드를 직접 호출한다. 메서드 호출 전후의 로깅 작업이 중복되어 있다.
    */
    @Test
    void reflection0() {
    
        Hello target = new Hello();
        log.info("start");
        String result1 = target.callA();
        log.info("result={}", result1);

        log.info("start");
        String result2 = target.callB();
        log.info("result={}", result2);
    }

    // 리플렉션을 사용하여 메서드 정보를 조회하고 메서드를 동적으로 호출하는 예제
    /*
    reflection1() 메서드에서는 Hello 클래스의 callA와 callB 메서드 정보를 
    리플렉션을 통해 조회한 후, 해당 메서드들을 동적으로 호출한다.
    */
    @Test
    void reflection1() throws Exception {
        Class<?> classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
        Hello target = new Hello();

        Method methodCallA = classHello.getMethod("callA");
        Object result1 = methodCallA.invoke(target);
        log.info("result1={}", result1);

        Method methodCallB = classHello.getMethod("callB");
        Object result2 = methodCallB.invoke(target);
        log.info("result2={}", result2);
    }

    // 리플렉션과 공통 로직을 사용하여 메서드를 동적으로 호출하는 예제
    /*
    reflection2() 메서드는 callA와 callB 메서드 정보를 리플렉션을 통해 가져온 후, 
    dynamicCall() 메서드에 전달하여 동적으로 호출한다. 이 메서드는 메서드 호출 전후의 
    로깅 작업을 공통 로직으로 처리한다.
    */
    @Test
    void reflection2() throws Exception {
        Class<?> classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
        Hello target = new Hello();

        Method methodCallA = classHello.getMethod("callA");
        dynamicCall(methodCallA, target);

        Method methodCallB = classHello.getMethod("callB");
        dynamicCall(methodCallB, target);
    }

    // 메서드를 동적으로 호출하면서 그 전후로 공통 로직을 실행하는 메서드
    private void dynamicCall(Method method, Object target) throws Exception {
        log.info("start");
        Object result = method.invoke(target);
        log.info("result={}", result);
    }
}

장점:

  • 코드의 유연성: 런타임 중에 동적으로 코드를 변경하거나 실행할 수 있다.
  • 중복 코드 제거: 반복되는 로직을 공통화하여 코드의 재사용성을 높일 수 있다.

단점:

  • 성능: 리플렉션은 일반적인 코드 실행보다 느릴 수 있다.
  • 보안: 리플렉션을 사용하여 private 메서드나 필드에 접근하는 것은 보안 위험을 초래할 수 있다.
  • 런타임 에러: 리플렉션을 사용하면 컴파일 시간에 검출되는 오류가 런타임에 발생할 수 있다.

리플렉션은 자바에서 제공하는 강력한 도구 중 하나다. 그러나 그 특성과 잠재적인 위험성을 잘 이해하고 신중하게 사용해야 한다. 코드의 중복을 줄이고 유연성을 높이려는 경우에는 리플렉션을 고려할 수 있다.


GPT

리플렉션에 대한 여러분의 이해를 평가하기 위해, 여러분이 제시한 내용과 질문들을 바탕으로 
여러 항목을 고려해보겠습니다.

기본 개념 이해: 여러분은 리플렉션의 기본 개념을 잘 이해하고 있습니다. 
런타임에 클래스나 메서드, 필드의 정보를 가져오는 것과 이를 통한 
동적인 코드 실행에 대한 설명을 명확하게 하셨습니다.
코드 이해: 제시한 코드 예제를 통해 리플렉션을 어떻게 사용하는지, 
그리고 이를 통해 중복된 로직을 어떻게 줄일 수 있는지에 대해 잘 파악하셨습니다.
장단점 인식: 리플렉션의 장단점에 대해서도 인식하고 있습니다. 
특히 리플렉션의 단점과 주의점에 대한 인식은 실무에서 중요합니다.

그러나, 실무에서는 리플렉션을 사용할 때 다음과 같은 고려사항이 있습니다:

- 리플렉션을 언제 사용해야 하는지와 언제 사용하지 않아야 하는지에 대한 판단 기준
- 다양한 리플렉션 API와 그 활용 방법 (예: 어노테이션 처리, 제네릭 정보 조회 등)
- 리플렉션과 관련된 성능 최적화나 보안 이슈 대응 방안

이러한 고려사항을 바탕으로, 실무자의 냉혹한 기준으로 여러분의 리플렉션에 대한 이해를 평가한다면,
7점 정도를 주고 싶습니다. 기본적인 개념과 활용 방법에 대한 이해는 확실하나, 
실무에서 직면할 수 있는 다양한 상황과 문제점에 대한 깊은 이해나 경험이 필요해 보입니다.

'Java > 스프링 AOP' 카테고리의 다른 글

CGLIB  (0) 2023.08.19
jdk 동적 프록시  (0) 2023.08.18
구체클레스, 인터페이스 프록시  (0) 2023.08.15
서브쿼리, JOIN, 프로시저  (0) 2023.08.08
프록시 패턴과 데코레이션 패턴 : 종업원과 샌드위치  (0) 2023.08.08

1. 구체 클래스 기반의 프록시:

장점:

  • 직접적인 확장: 특정 클래스의 일부 기능만을 확장하거나 변경할 때 매우 직관적이다.
  • 간단성: 특정 메서드만 오버라이드하여 필요한 기능을 추가할 수 있다.

단점:

  • 상속 제약: final 클래스와, final 메서드는 상속받을 수 없다.
  • 생성자 문제: 부모 클래스의 특정 생성자가 필요할 경우, 프록시에서도 그 생성자를 호출해야 한다. 때로는 필요하지 않은 정보나 null을 전달해야 할 수도 있다.

2. 인터페이스 기반의 프록시:

장점:

  • 유연성: 하나의 프록시 클래스가 여러 실제 객체를 대상으로 할 수 있다.
  • 변경에 강함: 실제 객체의 구현이 변경되더라도, 인터페이스만 동일하다면 프록시 클래스를 수정할 필요가 없다.

단점:

  • 인터페이스 구현의 제약: 인터페이스에 정의된 모든 메서드를 프록시에서 구현해야 한다.
  • 중복 코드 문제: 여러 프록시에서 비슷한 기능(예: 로깅, 보안 검사 등)을 구현할 경우 코드 중복이 발생할 수 있다.

'Java > 스프링 AOP' 카테고리의 다른 글

jdk 동적 프록시  (0) 2023.08.18
리플렉션  (0) 2023.08.16
서브쿼리, JOIN, 프로시저  (0) 2023.08.08
프록시 패턴과 데코레이션 패턴 : 종업원과 샌드위치  (0) 2023.08.08
데코레이션 패턴  (0) 2023.08.07

+ Recent posts