2022 10 17
2022-10-17¶
프록시 패턴 & 데코레이터 패턴¶
- 프록시 패턴 : "접근 제어"가 목적
- 권한에 따른 접근 차단
- 캐싱
- 지연 로딩
- 데코레이터 패턴 : "새로운 기능 추가"가 목적
- 원래 서버가 제공하는 기능에 더해 부가기능 수행
JDK 동적 프록시¶
- 특징
- 리플렉션 기술을 활용하여 프록시 객체를 동적으로 런타임에 개발자 대신 만들어줌
- 인터페이스를 기반으로 프록시 생성 -> 인터페이스가 필수!
- 자바 언어가 기본적으로 제공함
- 구현 방법
- InvocationHandler를 만들어 필요한 전/후처리 로직 구현
- Proxy.newProxyInstance(프록시할 객체의 클래스 로더, 프록시할 객체의 타입, 핸들러 로직)
@Test void dynamicA() { AInterface target = new AImpl(); TimeInvocationHandler handler = new TimeInvocationHandler(target); AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler); proxy.call(); log.info("targetClass = {}", target.getClass()); log.info("proxyClass = {}", proxy.getClass()); }
- 한계
- 인터페이스가 필수적임!
CGLIB 프록시¶
- 특징
- 바이트코드를 조작해 동적으로 클래스를 생성하는 기술을 제공
- 구체 클래스만 가지고 동적 프록시를 만들 수 있음
- 스프링 사용시 별도의 외부 라이브러리 필요 없음
-
구현 방법
- JdkDynamic의 InvocationHandler 역할을 CGLIB에서는 MethodInterceptor가 담당
- Enhancer : CGLIB은 Enhancer를 통해 프록시 생성
- 실행은 요렇게
@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("target = {}", target.getClass()); log.info("proxy = {}", proxy.getClass()); proxy.call(); }
- 한계
- 상속을 통한 구현이라 부모 클래스의 생성자 체크할 것
- final 키워드 붙으면 상속/오버라이딩 불가능
Spring의 프록시¶
- 개요
- Spring은 동적으로 프록시를 통합하여 편리하게 만들어주는 ProxyFactory 제공
- 인터페이스가 있으면 JDK, 구체클래스만 있다면 CGLIB 활용할 수 있음
- 그러면 InvocationHandler, MethodInterceptor를 매번 두개 만들어줘?
- 스프링은 Advice라는 개념을 도입함
- Advice를 통해 내부적으로 InvocationHandler, MethodInterceptor를 사용한다

- 특정 메서드 이름의 조건이 맞을 때만 프록시 부가기능 하려면 Pointcut과 같은 방식을 지원해줌!
- Advice
- 프록시에 적용하는 부가 로직
- CGLIB의 InvocationHandler, Jdk 동적 프록시의 MethodInterceptor와 유사한 개념
- 아래 인터페이스를 구현하여 사용할 것
-
ProxyFactory
- 스프링에서 동적으로 프록시를 만들어주는 기능을 제공
- ProxyFactory에 Advice 적용해주는 감성
@Test void interfaceProxy() { ServiceInterface target = new ServiceImpl(); ProxyFactory proxyFactory = new ProxyFactory(target); proxyFactory.addAdvice(new TimeAdvice()); ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy(); log.info("target = {}", target.getClass()); log.info("proxy = {}", proxy.getClass()); proxy.save(); assertThat(AopUtils.isAopProxy(proxy)).isTrue(); assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue(); assertThat(AopUtils.isCglibProxy(proxy)).isFalse(); }
- 인터페이스 타입이 존재하는 인터페이스/구체클래스 타입을 target으로 넘겨주면 jdk 동적 프록시,
- 구체클래스 타입을 target으로 넘겨주면 CGLIB으로 프록시 생성
- 만약
proxy.setProxyTargetClass(true)를 활용하면 무조건 cglib으로 프록시 생성
- 포인트컷, 어드바이스, 어드바이저
- 포인트컷: 어디에 부가기능을 적용할지, 어디에 부가기능을 적용하지 않을지 판단하는 "필터링 로직"
- 어드바이스: 프록시가 호출하는 "부가 기능"
- 어드바이저: 포인트컷 1개 + 어드바이스 1개
Advisorknows whichAdviceat whichPointcut
- 하나의 프록시 여러 어드바이저
- 스프링은 하나의 프록시에 여러 어드바이저를 적용하게 함으로써 동작
- 어드바이스 마다 프록시를 생성하는 것보다 좋은 성능, 같은 결과
- 스프링은 AOP를 적용할 때, 최적화를 진행해 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저 적용
- 스프링은 하나의 프록시에 여러 어드바이저를 적용하게 함으로써 동작
빈 후처리기¶
- 설명
- 생성: 스프링 빈 대상이 되는 객체 생성
- 전달: 생성된 객체를 빈 저장소에 등록하기 전 빈 후처리기에 전달
- 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체 조작 or 바꿔치기 가능
- 등록: 빈 후처리기가 반환한 빈을 등
- With Proxy
- 프록시를 생성하는 부분을 하나로 집중할 수 있음
- 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록과정 가로채 빈으로 등록 가능
- 지금 프로젝트에서 빈 후처리기가 어떤 일을 해주는지 기록하기
@Aspect AOP¶
- 개요
- Spring에서 프록시를 쓰려면? Advisor를 만들어서 스프링 빈으로 등록하면 됨
- 그럼 자동 프록시 생성기에서 자동으로 처리해줌
- 스프링 빈으로 등록된 어드바이저를 찾고 스프링 빈들에 자동으로 프록시 등록해줌
- Spring -
@Aspect를 활용하여 편리하게 "포인트컷/어드바이스"로 구성되어 있는 어드바이저 생성 기능 지원
- Spring에서 프록시를 쓰려면? Advisor를 만들어서 스프링 빈으로 등록하면 됨
@Aspect를 어떻게 Advisor로 바꿔주는거지?- 자동 프록시 생성기[AnnotationAwareAspectJAutoProxyCreator]는 Advisor를 자동으로 찾아와 필요한 곳에 프록시를 생성하고 적용해줌
- 자동 프록시 생성기가
@Aspect를 찾아Advisor로 변경해주는 역할
- 자동 프록시 생성기가
- 자동 프록시 생성기[AnnotationAwareAspectJAutoProxyCreator]는 Advisor를 자동으로 찾아와 필요한 곳에 프록시를 생성하고 적용해줌
- 어드바이저를 기반으로 프록시 생성
1. 생성: 스프링 빈 대상이 되는 객체 생성
2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달
3. Advisor 빈 조회: 스프링 컨테이너에서 Advisor 빈 모두 조회- 여기에서
@AspectAdvisor 조회도 같이! 4. 프록시 적용 대상 체크 5. 프록시 생성 6. 빈 등록
- 여기에서
Spring AOP¶
- 개요
- 횡단 관심사에 따라 부가 기능 적용
- AspectJ 프레임워크 사용
- 오류 검사 및 처리
- 동기화
- 성능 최적화
- 모니터링 및 로깅
- 적용 방식
- 컴파일 시점
- 클래스 로딩 시점
- 런타임 시점
- 용어 정리
- 조인포인트 : 어드바이스가 적용될 수 있는 위치. 메서드 실행 시점으로 제한됨
- 포인트컷 : 어드바이스가 적용될 위치 선별
- 어드바이스 : 부가 기능 그 자체
@Around- 메서드 호출 전후에 수행@Before- 조인 포인트 실행 이전에 실행@AfterReturning- 조인 포인트가 정상 완료 후 실행@AfterThrowing- 메서드가 예외를 던지는 경우 실행@After- 조인 포인트가 정상/예외에 관계없이 실행
- 타겟 : 어드바이스를 받는 객체
- 애스펙트 : 어드바이스+포인트컷의 모듈화
- 어드바이저 : 하나의 어드바이스+포인트컷
KasService에 AOP 적용하기¶
- 우선 Aspect 설정 안해주고 그냥 KasService 썼을때는 그냥 Bean 참조 직접 해버림
- 근데 Aspect 등록해주고 kasService 메서드에 기입해주면 CGLIB 프록시 만들어냄
-
그래서 해당 프록시에서 메서드 호출할 땐 아래의 로직을 거침
// General purpose AOP callback. Used when the target is dynamic or when the proxy is not frozen. // AOP의 제네럴한 목적을 구현한다. 타겟이 다이나믹하거나, 프록시가 얼려져있지 않으면 해당 로직 사용(?) private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable { private final AdvisedSupport advised; public DynamicAdvisedInterceptor(AdvisedSupport advised) { this.advised = advised; } @Override @Nullable public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; Object target = null; // TargetSource에 우리가 정의한 Bean이 들어있음. 매번 동적으로 받아오네? TargetSource targetSource = this.advised.getTargetSource(); try { if (this.advised.exposeProxy) { oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } target = targetSource.getTarget(); Class<?> targetClass = (target != null ? target.getClass() : null); // 여기에 내가 정의한 AOP가 있다면 받아짐 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); Object retVal; if (chain.isEmpty() && CglibMethodInvocation.isMethodProxyCompatible(method)) { Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = invokeMethod(target, method, argsToUse, methodProxy); } else { // 여기에서 내 로직이 수행됨 찐 객체 찔러서 결과 가져옴 retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(); } // this를 반환하면 래핑, null이 primitive으로 반환되지 않도록 처리 retVal = processReturnType(proxy, target, method, retVal); return retVal; } finally { if (target != null && !targetSource.isStatic()) { targetSource.releaseTarget(target); } if (setProxyContext) { AopContext.setCurrentProxy(oldProxy); } } } -
DynamicAdvisedInterceptor에서 MethodInterceptor를 구현하고 있다는건? -> CGLIB을 활용하여 지지고 볶는다는 것
- this.advised.getTargetSource()를 통해 TargetSource를 받아와 -> 여기서 우리의 Bean을 가져옴
- 스프링 빈에 둘다 등록되는건가...? 아니지 상속받아서 cglib 객체로 만들어두고 컴포넌트로 방식으로 들고있는거를 Bean으로 등록했을거야

- List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
- 이렇게 내가 정의한 Aspect를 받아올 수 있어

- new CglibMethodInvocation()을 통해서 실제로 객체 찔러서 결과 가져옴.
- 이후 processReturnType을 통해 타입 변환해주는듯
- 느낀점
- 이런식으로 필요한 전처리/후처리를 특정 메서드를 찌를때 수행해줄 수 있음!
- Transactional 도 이렇게 동작하겠구나!
- 스크린샷
- 기본으로 1000ms 넘어가면 warning
- 내가 threshold 결정할 수 있음
- 기본으로 1000ms 넘어가면 warning



