프로젝트는 - proxy
Client ----> Proxy ----> Server
간접적인 호출
접근 제어
부가 기능 추가
프록시 체인
프록시 객체가 중간에 있으면 크게 '접근 제어'와 '부가 기능 추가'를 수행할 수 있다.
아무 객체나 프록시가 될 수 있는거 같다.
하지만 객체에서 프록시가 되려면 클라이언트는 서버에게 요청을 한 것인지,
프록시에게 요청을 한 것인지 조차 몰라야한다.
서버와 프록시는 같은 인터페이스를 사용해야 한다.
클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도
클라이언트 코드를 변경하지 않고 동작이 가능해야 한다.
프록시 패턴
과 데코레이터 패턴
으로 구분한다프록시라는 개념은 클라이언트 서버 라는 큰 개념안에서 자연스럽게 발생할 수 있다.
객체안에서의 프록시 개념도 있고, 웹 서버에서의 프록시도 있다.
규모의 차이만 있을뿐 근본적인 역할은 같다.
test.proxy
test.decorator
Decorator
들은 스스로 존재할 수 없다.component
를 속성으로 가지고 있어야 한다.interface_proxy
concreteProxy package
인터페이스 기반 프록시가 더 좋음.
프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 더 명확하게 나눈다
단점은 인터페이스가 필요하다는것. 인터페이스가 없으면 인터페이스 기반 프록시를 만들 수 없다.
하지만 실제로는 구현을 변경할 일이 거의 없는 클래스도 많다.
이런곳에는 실용적인 관점에서 인터페이스를 사용하지 않고 구체 클래스를 바로 사용하는 것이 좋음
실무에서는 프록시를 적용할때는 인터페이스와 구체 클래스가 섞여 있는 경우가 있다.
따라서 2가지 상황을 모두 다 대응할 수 있어야 한다.
test.jdkdynamic.RelfectionTest
주의점 : 리플렉션을 사용하면 애플리케이션을 동적으로 유연하게 만들 수 있다. 하지만
리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
리플렉션은 일반적으로 사용하면 안된다.
지금까지 프로그래밍 언어가 발달하면서 타입 정보를 기반으로 컴파일 시점에 오류를 잡아준
덕에 개발자가 오류를 바로 잡았는데, 리플렉션은 그것에 역행하는 방식이다
리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요한 때 부분적으로
주의해서 사용해야한다.
test.jdkdynamic.code
동적 프록시 기술을 활용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다. 런타임에
개발자 대신 만들어 준다. 그리고 동적 프록시에 원하는 실행 로직을 지정할 수 있다.
"주의"
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 인터페이스가 필수!!
JDK 동적 프록시에 적용할 로직은 InvocationHandler
인터페이스를 구현해서 작성하면 된다.
구현해야할 메서드 : public Object invoke(Object proxy, Method method, Object[] args)
1. object proxy
- 프록시 자신
2. Method method
- 호출한 메서드
3. Object[] args
- 메서드를 호출할 때 전달한 인수
실행 순서
1. 클라이언트는 JDK 동적 프록시의 call()
실행
2. JDK 동적 프록시는 InvocationHandler.invoce()
호출.
-> TimeInvocationHandler 구현체가 있으므로 TimeInvocationHandler.invoke() 호출
3. TimeInvocationHandler
가 내부로직을 수행하고, method.invoke(target, args)
를 호출해서
target
인 실제 객체를 호출한다
4. 실제 객체의 call()
이 실행
5. 실제 객체의 call()
의 실행이 끝나면 TimeInvocationHandler
로 응답이 돌아온다.
시간 로그를 출력하고 결과를 반환함.
결론
적용 대상만큼 프록시 객체를 만들지 않아도 된다. 부가 기능 로직을 한번만 개발해서
공통으로 적용 가능. 적용 클래스가 100개여도 동적 프록시를 통해 생성하고,
필요한 InvocationHandler
만 만들어서 넣어주면 된다.
결과적으로 프록시 클래스를 수없이 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의
클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게 되었다.
실제 예제에 적용
일차적으로 메서드의 실행시간을 제는 LogTrace기능을 JDK 동적 프록시를 통해 적용했는데,
문제가 하나 생겼다. 바로 no-log 메서드에도 log가 남는것이다. 이 메서드에는 로그가 남으면 안된다
이때 해결할 수 있는 방법 - JDK 동적 프록시에는 Method 이름 필터 기능이 있다.
같이 연계해서 Spring의 PatternMathUtils를 사용하면 패턴의 매칭을 맞추어,
그 문자열이 들어가는 메서드만 로그를 남길 수 있다.
CGLIB : Code Generator Library
test.cglib
바이트 코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리
인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어 낼 수 있다.
원래 CGLIB은 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스코드에 포함했다.
따라서 스프링을 사용하면 별도의 추가 없이 사용 가능
참고 : 우리가 CGLIB을 직접 사용할 경우는 거의없다. 이후 배울 스프링의 ProxyFactory
라는
것이 이 기술을 편리하게 사용하도록 도와줌.
실제 구현해야 될것은 JDK 동적 프록시의 InvocationHandler와 비슷하다
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
invoke Method를 구현해야되는 jdk와 intercept라는 것을 받는 CGLIB. 실제로 메서드는 거의 비슷함.
CGLIB 제약
test.proxy.proxyfactory
유사한 기술들 (JDK 동적 프록시, CGLIB)를 통합해서 일관성 있게 접근할 수 있고,
편리하게 사용할 수 있는 추상화된 기술을 Spring이 제공한다
Adivce
도입
스프링은 Advice를 통해서 InvocationHandler나 MethodInterceptor를 신경쓰지 않고
Advice
만 만들면 됨. 이 프록시 팩토리를 사용하면 Advice
를
호출하는 전용 InvocationHandler, MethodInterceptor를 호출함.
특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공된다
앞서 특정 메서드 이름의 조건에 맞을때만(Filter) 프록시 부가 기능이 적용되는 코드를
직접 만들었다. Spring에선 Pointcut
이라는 개념을 도입해 이 문제를 해결하였다.
Advice 만들기
Advice
는 프록시에 적용하는 부가기능 로직. 기존의 동적프록시와 CGLIB을 개념적으로 추상화
한 것이다. Advice
를 만드는 방법은 여러가지가 있지만 기본적으로 인터페이스를 구현한다
MethodInterceptor -> CGLIB과 메서드 이름이 같지만, 패키지가 다르다.(Interceptor를 상속
하고 Interceptor는 Advice 인터페이스를 상속한다.)
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return null;
}
기존에 보던 메서드들과 달리 파라미터가 적다. 그 파라미터들의 기능은 전부 MethodInvocation
이 들고 있다.
// 프록시팩토리를 생성할때 타겟 객체를 넣음. advice는 만들어둔 advice를 넣어주면된다.
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdivce());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
// 프록시펙토리를 사용시 AopUtils를 통해 proxy를 사용하고있나 아닌가 확인가능
AopUtils.isAopProxy(proxy);
AopUtils.isJDKDynamicProxy(proxy);
AopUtils.isCglibProxy(proxy);
어떤 것을 선택하나?
정리
조언(Advice)를 어디(Pointcut)에 할것인가?
조언자(Advisor)는 어디(Pointcut)에 조언(Advice)을 해야할지 알고 있다.
이는 역할과 책임을 명확하게 분리한 것이다.
포인트컷은 대상 여부를 확인하는 필터 역할만 담당. 어드바이스는 부가기능 로직만 담당
둘을 합치면 어드바이저. 스프링의 어드바이저는 하나의 포인트컷 + 하나의 어드바이스로 구성됨
프록시 펙토리는 Advisor가 필수다.
프록시를 호출할때 필수적으로 어드바이저를 넣어줘야됨. 그 전에 만들었을 때는
어드바이스만 넣었는데 이미 그 어드바이스에 편의메서드로 포인트컷이 함께 제공된
어드바이저가 들어가있던것임.
public interface Pointcut{
ClassFilter getClassFilter();
MethodMathcer getMethodMatcher();
}
스프링은 무수히 많은 포인트컷을 제공한다. 대표적인 몇가지만 알아보자
가장 중요한 것은 aspectJ 표현식
사실 다른것은 중요하지 않다. 실무에서는 사용성과 편리성을 고려해 aspectJ 표현식을
기반으로 사용하는 AspectJExpressionPoincut을 사용하게됨.
프록시를 여러개 생성해서, 프록시 호출 ( 문제점: 프록시 개수를 적용 수만큼 만들어야됨.)
프록시 하나에 여러 Advisor 사용 가능 ( 결과는 같고, 성능은 더 좋아진다)
정리
스프링 AOP를 처음 공부하거나 사용시, AOP 적용 수 만큼 프록시가 생성된다고 착각함.
스프링은 AOP를 적용할때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고,
하나의 프록시에 여러 어드바이저를 적용한다.
target
에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target
마다 하나의 프록시만
생성한다.
프록시 팩토리 덕분에 매우 편리하게 프록시를 생성할 수 있게 되었다.
추가로 어드바이저, 어드바이스, 포인트컷 이라는 개념덕분에 어떤 부가기능을 어디에 적용해야할지
명확하게 이해할 수 있었다.
남은 문제
이 두가지 문제를 한번에 해결하는 방법이 바로 빈 후처리기 이다.
BeanPostProcessor
는 빈 후처리기인데, 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도빈 등록 과정
1. 생성 - 스프링 빈이 대상 객체를 생성(@Bean, @Component)
2. 전달 : 생성된 객체를 빈 저장소에 등록(이때 빈 후처리기를 사용하면 빈 후처리기에 먼저 전달됨)
3. 후 처리 작업: 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔치기 가능.
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
빈을 조작한다는 것은 해당 객체의 특정 메서드를 호출하는 것을 뜻한다.
일반적으로 스프링컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은
중간에 조작할 방법이 없는데, 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을
중간에 조작할 수 있다. 이 말은 '빈 객체를 프록시로 교체'하는 것도 가능하다는 뜻.
library 등록 해야됨 - 'org.springframework.boot:spring-boot-starter-aop'
자동 프록시 생성기 - AutoProxyCreator
스프링 부트 자동 설정으로 빈 후처리기가 스프링 빈에 등록된다.
스프링 빈으로 등록된 Advisor들을 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
생성 과정
Advisor
를 조회Advisor
에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시 적용 대상인지프록시 적용 여부 판단 - "생성 단계"
어드바이스 적용 여부 판단 - "사용 단계"
프록시를 모든 곳에 생성하는 것은 비용 낭비이다. 꼭 필요한 곳에 최소한의 프록시를 적용해야 한다.
자동 프록시 생성기는 모든 스프링 빈에 프록시를 적용하는 것이 아니라 포인트컷으로 한번 필터링,
어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성.
문제점 : 서버를 실행만 해보면, 스프링이 초기화 되면서 기대하지 않은 로그가 올라온다.
그 이유는 단순하게 지금은 어떤 단어만 포함되어있으면 매칭 된다고 판단하기 때문.
결국 스프링 내부에서 사용하는 빈, 메서드 이름에 그 단어만 있으면 프록시가 만들어지고
어드바이스도 적용됨.
결론적으로 매우 정밀한 포인트컷이 필요하다. -> AspectJ를 이용한다. 또한 실무에서는 이 AspectJ
를 대부분 사용한다.
스프링 애플리케이션에 프록시를 적용하려면 포인트컷과 어드바이스로 구성되어 있는 어드바이저를
만들어서 스프링 빈으로 등록하면 된다. 그럼 나머지는 자동 프록시 생성기가 자동으로 처리해줌.
스프링은 @Aspect
애노테이션으로 매우 편리하게 포인트컷과 어드바이스로 구성되어있는
어드바이저 생성 기능을 지원한다. 직접 만들었던 어드바이저에 @Aspect 애노테이션을 사용해
만들면쉬워 진다.
@Aspect
는 관점 지향 프로그래밍(AOP)을 가능하게 하는 AspectJ 프로젝트에서 제공하는
애노테이션이다. 스프링은 이것을 차용해서 프록시를 통한 AOP를 가능하게 한다.
AOP와 AspectJ 관련된 내용은 다음에 설명.. 지금은 이 애노테이션을 사용해서
스프링이 편리하게 프록시를 만들어주는것을 생각하면 됨.
사실 자동 프록시 생성기는 2가지 일을 한다
1. @Aspect를 보고 어드바이저로 변환해서 저장한다.
2. 어드바이저를 기반으로 프록시를 생성한다.
"@Asepect를 어드바이저로 변환해서 저장하는 과정"
@Aspect
애노테이션이 붙은@Aspect
어드바이저 빌더를 통해 @Aspect
의 정보를 기반으로 어드바이저 생성.@Aspect
어드바이저 빌더 내부에 저장한다.참고 : 본 글은 김영한님의 스프링 강의를 정리한 것이다.