쓰레드 로컬 - ThreadLocal
동시성 문제
- 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제
- 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다.
- 값을 읽기만 할 때는 발생하지 않는다.(변경하기 때문에 발생)
개념
- 쓰레드 로컬을 사용하면 각 쓰레드 마다 별도의 내부 저장소를 제공한다.
- 해당 쓰레드가 쓰레드를 모두 사용하고 나면 remove를 호출해서 저장된 데이터를 제거해줘야 한다. 기존 데이터가 남아있을 위험성이 있다.
템플릿 메서드 패턴과 콜백 패턴
템플릿 메서드 패턴 시작
핵심기능과 부가기능
- 핵심기능: 해당 객체가 제공하는 고유의 기능
- 주문 로직
- 부가기능: 핵심 기능을 보조하기 위해서 제공하는 기능
- 로그 추적 로직
- 트랜잭션 기능
정리
- 변하는 것과 변하지 않는 것을 분리
- 이 둘을 잘 분리해서 모듈화를 해야 한다.
문제 코드
void templateMethodV0(){
logic1();
logic2();
}
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 실행
log.info("비즈니스 로직1 실행);
// 비즈니스 로직 종료
long resultTime = entTime - startTime;
log.info("resultTime={}", resultTime);
}
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 실행
log.info("비즈니스 로직2 실행);
// 비즈니스 로직 종료
long resultTime = entTime - startTime;
log.info("resultTime={}", resultTime);
}- 변하는 부분
- 비즈니스 로직
- 변하지 않는 부분
- 시간 측정 위와 같이 중복되는 변하지 않는 부분이 있는 데 이 문제를 해결하는 방법이 템플릿 메소드 패턴이다.
템플릿 메서드 패턴 적용
단순 개념
- 추상 메소드를 모아놓은 다음에 call()이라는 메소드를 만들어서 이것을 자식들이 오버라이드 해서 구현을 하는 것이다.
- 템플릿이라는 변하지 않는 거대한 틀을 만들어 놓는다.
- 그리고 일부 변하는 부분은 별도로 호출해서 해결한다.
- 상속을 사용한다.
- 변하는 부분만큼의 자식 클래스를 만들게 된다.
예제 코드
public void execute() {
long startTime = System.currentTimeMillis();
// 비즈니스 실행
call() // 상속
// 시간 측정
long resultTime = entTime - startTime;
log.info("resultTime={}", resultTime);
}
protected abstract void call();단점
- 클래스를 계속 만들어야 하는 단점이 있다.
- 이 단점을 해결하기 위해서 익명 내부 클래스를 사용한다.
- 자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않는데 부모 클래스를 알아야 한다.
익명 내부 클래스로 구현
AbstractTemplate tempalte1 = new AbstractTempalte() {
@Override
prootected void call() {
log.info(비즈니스 로직 1);
}
}좋은 설계란
- 진정한 좋은 설계는 바로 변경이 일어날 때 자연스럽게 드러난다.
- 템플릿 메소드 패턴을 예로 들면 변하지 않는 부분에서 수정을 한다면 템플릿 메소드 내부의 로직만 수정하면 된다.
정의
- 작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기한다. 템플릿 메서드를 사용하면 하위 클래스가 알고리즘의 구조를 변경하지 않고도 알고리즘의 특정 단계를 재정의 할 수 있다.
- 부모 클래스에 알고리즘의 골격인 템플릿을 정의하고 일부 변경되는 로직은 자식 클래스에 정의한다.
전략 패턴
정의
- 변하지 않는 부분을
Context라는 곳에 두고 변하는 부분을Strategy라는 인터페이스를 만들고 해당 인터페이스를 구현해서 문제를 해결한다. - 상속이 아니라 위임으로 문제를 해결한다.
- 알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환하게 만들자. 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.
문제코드
void templateMethodV0(){
logic1();
logic2();
}
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 실행
log.info("비즈니스 로직1 실행);
// 비즈니스 로직 종료
long resultTime = entTime - startTime;
log.info("resultTime={}", resultTime);
}
private void logic1() {
long startTime = System.currentTimeMillis();
// 비즈니스 실행
log.info("비즈니스 로직2 실행);
// 비즈니스 로직 종료
long resultTime = entTime - startTime;
log.info("resultTime={}", resultTime);
}코드 적용
Context 클래스
public class ContextV1 {
// 전략을 주입받는다.
// 인터페이스만 의존을 한다.
public interface Strategy strategy
public void execute() {
long startTime = System.currentTimeMillis();
// 비즈니스 실행
strategy.call(); // 위임
// 비즈니스 로직 종료
long resultTime = entTime - startTime;
log.info("resultTime={}", resultTime);
}
}Strategy 클래스
public interface Strategy {
void call();
}
public class StrategyLogic1 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직 실행");
}
}선 조립 후 실행
Context와Strategy를 실행 전에 원하는 모양으로 조립해두고 실행을 하는 방식이다.- 단점
Context와Strategy를 조립한 이후에는 전략을 변경하기가 번거롭다는 점이다.- 변경할 수는 있지만 싱글톤으로 사용하게 되는 경우 동시성 이슈 등 고려해야할 점이 많다.
- 수정하는 것보다
Context를 하나 더 생성하는 것이 나은 선택일 수 있다.
템블릿 콜백 패턴
콜백 정의
- 다른 코드의 인수로서 넘겨주는 실행 가능한 코드
context.execute(() -> log.info("비즈니스 로직 실행))- 콜백을 넘겨받는 코드는 즉시 실행할 수도 있고 나중에 실행할 수 있다.
- 코드가 호출은 되는데(
call) 실행은 넘겨준 곳 뒤(back)에서 실행이 된다.
정의
- 전략 패턴에서
Context가 템플릿의 역할을 하고Strategy부분이 콜백으로 넘어온다고 생각하면 된다. - 스프링 안에서만 이렇게 사용이 된다.(전략 패턴에서 템플릿과 콜백 부분이 강조된 부분)
콜백 전달 인터페이스
public interface TraceCallback<T> {
T call();
}트레이스 템플릿
public class TraceTemplate{
private final LogTrace trace;
// 생성자
// 실행 메소드
public<T> T execute(String message, TraceCallback<T> callback) {
...
// 콜백을 통해서 호출을 한다.
T result = callback.call();
...
}
}사용 로직
public classs Repository {
public final TraceTemplate template;
public Repoistory(LogTrace trace) {
this.template = new TraceTempalte(trace);
}
public void save(String itemId) {
template.execute("저장 실행", () -> {
if(itemId.equals("ex")) {
// 예외 발생
}
// 비즈니스 로직
})
}
}프록시(프록시 패턴 및 데코레이터 패턴)
- 클라이언트와 서버를 요청하는 객체와 요청을 처리하는 객체와 대응할 수도 있다.
- 클라이언트가 요청한 결과를 서버에 직접 호출하는 것이 아니라 대리자를 통해서 간접적으로 서버에 요청할 수 있다.
- 간접 호출을 하면 대리자가 중간에 여러가지 일을 할 수 있다는 점이다.
- 클라이언트는 서버 객체에 요청을 한 것인지 프록시에게 요청한 것인지 몰라야한다.
- 같은 인터페이스를 사용해야 한다.
- 서버 객체를 프록시 객체로 변경을 해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 한다.
주요 기능
- 접근 제어
- 권한에 따른 접근 차단
- 캐싱
- 지연 로딩
- 부 기능 추가
- 원래 서버가 제공하는 기능에 더해서 부가 기능을 추가한다.
GOF 디자인 패턴
- 의도에 따라서 프록시 패턴과 데코레이터 패턴으로 구분한다.
- 프록시 패턴
- 접근 제어가 목적으로 대리자를 제공
- 데코레이터 패턴
- 새로운 기능 추가가 목적
- 객체에 추가 책임을 동적으로 추가해서 기능 확장을 위한 유연한 대안 제공
프록시 패턴 예제
서버
public interface Subject {
String operation();
}
@Slf4j
public class RealSubject implements Subject {
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000);
return "Okay";
}
}클라이언트
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}프록시(접근 제어)
@Slf4j
public class CacheProxy implements Subject {
private Subject target;
private String cacheValue;
// 프록시도 실제 객체와 모양이 같아야 한다.
public CacheProxy(Subject target) {
this.target = target;
}
@Override
public String operation() {
log.info("프록시 호출");
if(cacheValue == null) {
cacheValue = target.operation();
}
return cacheValue;
}
}핵심
- 클라이언트 코드와 서버 코드를 전혀 바꾸지 않고 호출 시간을 단축시킬 수 있다.(접근 제어를 할 수 있다)
- 클라이언트는 서버가 주입되었는 지 프록시가 주입 되었는 지 모른다.
데코레이터 패턴
서버
public interface Component {
String operation();
}
@Slf4j
public class RealComponent implements Component {
@Override
public String operation() {
log.info("RealComponent 실행");
return "okay";
}
}클라이언트
@Slf4j
public class DecoratorPatternClient {
private Component componet;
public DecoratorPatternClient(Component component) {
this.component = component
}
public void execute() {
String result = component.operation();
log.info("result={}", result);
}
}데코레이터(문자열 Wrapping)
@Slf4j
public class MessageDecorator implements Component {
private Component component
public MessageDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("MessageDecorator 실행");
String operation = component.operation();
String decoResult = "*****" + operation + "*****";
log.info("MessageDecorator 꾸미기 적용 전 = {}, 꾸미기 적용 후 = {}", operatin, decoResult);
return decoResult;
}
}데코레이터(시간 측정)
- 프록시 패턴은 체이닝이 가능하다.
@Slf4j
public class TimeDecorator implements Component {
private Component component
public TimeDecorator(Component component) {
this.component = component;
}
@Override
public String operation(){
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = component.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료 resultTime={}ms", resultTime);
return result;
}
}정리
- 데코레이터들은 항상 꾸며줄 대상이 있어야 하고 그러다 보니 꾸며줄 대상 여기서는
Component를 호출하는 부분은 중복된 부분이다. 이를 해결하기 위해서는Component를 갖고 있는Decorator라는 추상 클래스를 만드는 방법이 있다.
클래스 기반 프록시
- 상위 타입만 맞으면 다형성이 적용된다.
- 클래스 기반으로 상속을 받아서 프록시를 적용할 수 있다.
- 부모 클래스는 자식 클래스를 품어줄 수 있다.
- 단점
- 자식 클래스를 생성할 때 항상
super()를 호출해준다. 따라서 부모 생성자를 초기화 하기 위해서super(null)을 호출해야 한다.
- 자식 클래스를 생성할 때 항상
인터페이스 기반 프록시 vs 클래스 기반 프록시
- 클래스는 해당 클래스에만 적용할 수 있으며 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
- 클래스 기반 프록시의 제약 사항
- 부모 클래스의 생성자를 호출해야 한다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- 메서드에 final 키워드가 붙으면 오버라이딩이 불가능하다.
- 인터페이스 기반의 프록시가 상속이라는 제약에서 자유롭다.
- 인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적이다. (인터페이스가 항상 필요하지 않다.)
동적 프록시 기술
리플렉션
- 클래스나 메소드의 메타데이터를 얻어와서 동적으로 메소드를 호출할 수 있다.
예제
@Test
void reflection1() throws Exception {
Class classHello
= Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
//callA 메서드 정보
// 문자로 메소드 정보를 얻어올 수 있다.
Method methodCallA = classHello.getMethod("callA");
Object result1 = methodCallA.invoke(target);
log.info("result1={}", result1);
//callB 메서드 정보
Method methodCallB = classHello.getMethod("callB");
Object result2 = methodCallB.invoke(target);
log.info("result2={}", result2);
}methodCallA.invoke(target): 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다. 여기서 알 수 있는 것은 Method로 추출했기 때문에 추상화가 가능하다.
private void dynamicCall(Method method, Object target) throws Exception {
log.info("start");
// invoke를 사용해서 메소드를 동적으로 호출할 수 있다.
Object result = method.invoke(target);
log.info("result={}", result);
}주의
- 리플렉션 기술은 런타임 시점에서 동작하기 때문에 컴파일 시점에서 오류를 확인할 수 없다.
동적 프록시
- 동적 프록시 기술을 사용하면 직접 프록시 클래스를 만들지 않아도 된다.
- 인터페이스가 필수로 필요하므로 인터페이스를 구현한 클래스에만 적용할 수 있다.
- 원하는 실행 로직을 지정할 수 있다.
예제
- AInterface
public interface AInterface {
String call();
}- AImpl
@Slf4j
public class AImpl implements AInterface {
@Override
public String call() {
log.info("A 호출");
return "a";
}
}- BInterface
public interface BInterface {
String call();
}- BImpl
@Slf4j
public class BImpl implements BInterface {
@Override
public String call() {
log.info("B 호출");
return "b";
}
}- JDK 동적 프록시가 제공하는 핸들러
- JDK 동적 프록시가 제공하는 InvocationHandler를 구현한다.
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
Throwable {
public Object invoke(Object proxy, Method method, Object[] args) throws
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
// 실행될 메소드가 넘어온다.
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}- 동적 프록시를 사용해보자
import java.lang.reflect.Proxy;
@Slf4j
public class JdkDynamicProxyTest {
@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()); // jdk가 만든 프록시
}
@Test
void dynamicB() {
BInterface target = new BImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
BInterface proxy = (BInterface) Proxy
.newProxyInstance(
BInterface.class.getClassLoader()
, new Class[]{BInterface.class}, handler
);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
}실행 순서
- 클라이언트는 JDK 동적 프록시의
call()을 실행한다. - JDK 동적 프록시는
InvocationHandler.invoke()를 호출한다. 여기서는TimeInvocationHandler가 구현체로 있으므로 구현체의invoke()가 호출된다. TimeInvocationHandler가 내부 로직을 수행하고 실제 객체를 호출한다.- 실제 객체의
call()이 실행된다. - 실제 객체의
call()이 끝나면TimeInvocationHandler로 응답이 돌아오고 클라이언트로 반환된다.
CGLIB(Code Generator library)
- 바이트 코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리
- 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
예제
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
@Slf4j // cglib를 위한 상속(실행 로직을 정의)
public class TimeMethodInterceptor implements MethodInterceptor {
// Proxy는 항상 target 생성자가 필요하다.
private final Object target;
public TimeMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
// 성능상 MethodProxy 사용을 권장한다.
Object result = proxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}- 테스트 해보기
@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();
}제약
- 부모 클래스가 기본 생성자가 필요하다.
- 클래스에
final키워드가 있으면 상속이 불가능하므로CGLIB에서 예외가 발생한다.
스프링이 지원하는 프록시
프록시 팩토리
- 스프링은 상황에 따라서 JDK 동적프록시를 사용하거나 CGLIB를 사용해야 하는 고민할 필요 없이 프록시 팩토리 하나로 편리하게 동적 프록시를 생성할 수 있도록 지원한다.
- 두 기술을 함께 사용할 때 부가 기능을 적용하기 위해서 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor을 중복으로 따로 만드는 것을 방지하기 위해서
Advice라는 새로운 개념을 도입하였다. - 특정 위치에만 프록시 로직을 적용하는 방법은
Pointcut을 사용해서 지정해줄 수 있다. - 여러 어드바이저를 적용할 수 있다.
MethodInterceptor(스프링에서 제공하는 메서드)
package org.aopalliance.intercept; // 패키지 주의
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}MethodInvocation invocation- 내부에 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args 및 메서드 정보 등이 포함되어 있다.
예제
- Target class를 직접 설정해 주지 않아도
MethodInvocation invocation내부에 이미 들어있다.
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();
// invocation에 이미 proceed를 만들어 놨다.
// 알아서 target을 찾아서 인수를 넘기면서 실행을 해준다.
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}ms", resultTime);
return result;
}
}- 실제 호출하는 부분
@Slf4j
public class ProxyFactoryTest {
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
void interfaceProxy() {
ServiceInterface target = new ServiceImpl();
// 타겟을 넣으면서 프록시 팩토리를 만들어 준다. => 타겟 정보를 알 수 있다.
// 따라서 Target을 상속받는 메소드에 선언을 안해줘도 된다.
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
// Object로 반환 하므로 casting 해준다.
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();
}
}포인트컷, 어드바이스, 어드바이저
포인트 컷
- 어디에 부가 기능을 적용할 지 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직
- 필터 역할
어드바이스
- 프록시가 호출하는 부가 기능
- 프록시 로직이라고 생각해도 무방
- 부가 기능 로직
어드바이저
- 하나의 포인트 컷과 하나의 어드바이스를 갖고 있는 것을 어드바이저라고 한다.
- 포인트컷(1)+ 어드바이스(1)
한 줄 정리
- 포인트 컷은 어디에 어드바이스는 어떤 로직을 적용할 지 어드바이저는 둘 다 알 고 있다.
예제
- 어드바이저 추가해보기
@Test
void advisorTest1() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
// Advisor 인터페이스의 가장 일반적인 구현체
DefaultPointcutAdvisor advisor =
new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
// ProxyFactory에 advisor를 지정해준다.
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
// 프록시가 중간에 호출 된 것을 확인할 수 있다.
proxy.save();
proxy.find();
}- 포인트 컷 구현해보기
@Test
@DisplayName("직접 만든 포인트컷")
void advisorTest2() {
ServiceImpl target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(
new MyPointcut(), new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
// PointCut 구현하기
static class MyPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new MyMethodMatcher();
}
}
//
static class MyMethodMatcher implements MethodMatcher {
private String matchName = "save";
// method와 targetClass의 정보가 넘어온다.
@Override
public boolean matches(Method method, Class<?> targetClass) {
boolean result = method.getName().equals(matchName);
log.info("포인트컷 호출 method={} targetClass={}"
, method.getName(), targetClass);
log.info("포인트컷 결과 result={}", result);
return result;
}
// 성능의 구분 위함
@Override
public boolean isRuntime() {
return false;
}
// runtime이 true면 실행
@Override
public boolean matches(Method method, Class<?> targetClass, Object...args) {
throw new UnsupportedOperationException();
}
}- 스프링이 제공하는 포인트 컷
@Test
@DisplayName("스프링이 제공하는 포인트컷")
void advisorTest3() {
ServiceImpl target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
// save가 method 인 것만 적용할 수 있도록 한다. (이름 기반 매칭)
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();
}빈 후처리기
- 스프링이 빈 저장소에 등록할 목적으로 생성된 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용하면 된다.
BeanPostProcessor빈을 생성한 후 무언가를 처리하기 위한 용도
스프링 -> 빈 후처리기 -> 스프링 빈 저장소사용 방법
BeanPostProcessor인터페이스를 구현하고 스프링 빈으로 등록하면 된다.postProcessBeforeInitialization- 빈 초기화 전
postProcessAfterInitialization- 진 초기화 후
예제
public class BasicTest{
@Test
void basicConfig() {
ApplicationContext applicationContext = new AnnotationConfigApplocationContext(BeanConfig.class);
// A는 빈으로 등록된다.
}
@Configuration
static class BeanConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
@Bean
public AtoBPostProcessor helloProcessor() {
return new AToBPostProcessor();
}
}
static class A {
public void helloA() {
log.info("hello A");
}
}
static class B {
public void helloB() {
log.info("hello B");
}
}
static class AtoBPostProcessor implements BeanPostProcessor {
// 스프링에 등록한 빈 후처리기 클래스
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("beanName = {}, bean={}", beanName, bean);
// 만약 빈이 A이면 B를 반환한다.(몰래 바꾸기)
if(bean instanceof A) {
return new B();
}
}
}
정리
- 빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트
- 빈 객체를 조작하거나 다른 객체로 바꾸어 버릴 수 있을 정도로 막강하다 (ex> 객체 내부의 메소드를 호출, PostConstruct 호출)
- 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다. 빈 객체를 프록시로 교체하는 것도 가능하다.
PackageLogTracePostProcessor
private final String basePackage;
private final Advisor advisor;
// 생성자
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("param beanName={}, bean={}", beanName, bean.getClass());
// 프록시 적용 대상 여부 체크
// 프록시 적용 대상이 아니면 원본을 그대로 진행
Package packageName = bean.getClass().getPackageName();
if(packageName.startsWith(backPackage)){
return bean;
}
// 프록시 대상이면 프록시를 만들어서 반환
// 파라미터로 타겟을 넣어줘야 한다. 그것 타겟은 바로 bean
ProxyFactory proxyFactory = new ProxyFactory(bean);
// 지정할 어드바이저를 갖고 온다.
proxyFactory.addAdvisor(advisor);
Object proxy = proxyFactory.getProxy();
log.iinfo("create proxy: target={}, proxy={}", bean.class, proxy.class);
return proxy;
}
위에서 생성한 빈 후처리기를 빈으로 등록하기
public class BeanPostProcessorConfig {
@Bean
public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
return new PackageLogTracePostProcessor("{basePackage}", getAdvisor(logTrace))
}
private Advisor getAdvisor(LogTrace logTrace) {
//pointcut(적용할 메소드 이름(위치))
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
//advice(적용할 기능(여기선 로깅기능))
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}특징
- 직접 등록한 스프링 빈들 뿐만 아니라 컴포넌트 스캔으로 등록한 빈들도 프록시를 적용할 수 있다는 점이 있다.
- 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기에 넘어온다. 이 대상을 설정하는 효율적인 방법은 포인트컷을 사용하는 것이다.
사용하는 이유
기존 문제
- 너무 많은 겹치는 설정들이 존재한다. 모든 빈들마다 여기서는 프록시 생성 코드를 작성해줘야한다.
- 컴포넌트 스캔 클래스에는 개입할 수가 없어서 직접 빈 등록하는 코드에만 적용할 수 있었다.
해결
- 빈 후처리기 덕분에 프록시에 생성하는 부분을 하나로 집중할 수 있다.
- 컴포넌트 스캔 클래스에도 중간에 가로채서 프록시를 빈으로 등록할 수 있다.
포인트 컷은 다음 두 곳에서 사용된다.
- 프록시 적용 대상 여부를 체크해서 필요한 곳에서만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
- 프록시의 어떤 메소드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부 - 프록시는 객체(클래스)단위 이기 때문에 내부 메소드를 특정하기 위함)
스프링이 제공하는 빈 후처리기
implementation 'org.springframework.boot:spring-boot-starter-aop'- 자동 프록시 생성기 -
AutoProxyCreator- 위에서 공부한 PostProcessor
개념(AutoProxyCreator)
- 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기
- 이 빈 후처리기는 스프링 빈으로 등록된
Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다. Advisor는Pointcut과Advice가 포함되어 있다. 따라서 적용될 위치는Pointcut을 사용해서 확인할 수 있고 적용할 기능은Advice를 통해서 알 수 있다.
프록시 생성 과정
- 1. 생성: 스프링이 빈 대상이 되는 객체를 생성한다.
- 2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
- 3. 모든 Advisor 빈 조회: 자동 프록시 생성기 - 빈 후처리기는 스프링 컨테이너에서 모든
Advisor를 조회한다. - 4: 프록시 적용 대상 체크: 앞서 조회한
Advisor에 포함되어 있는 포인트 컷을 사용해서 프록시를 적용할 대상인 지 판단한다. 여기서 하나라도 만족하는 클래스는 프록시 적용 대상이 된다. - 5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 반환해서 스프링 빈으로 등록한다.
- 6. 빈 등록: 반환된 객체는 스프링 빈으로 등록 된다.
주의
- 이 시점에 사용되는 포인트컷과 등록된 이후에 동작할 때 사용되는 포인트 컷과 구분해야 한다.
포인트 컷은 2가지로 사용이 된다
- 프록시 적용 여부 판단 단계 - 생성 단계
- 자동 프록시 생성기는 포인트 컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는 지 판단을 한다.
- 클래스 + 메소드 조건을 모두 비교해 본다. 이 때 모든 메소드를 체크하는데 포인트 컷 조건에 하나라도 만족하면 프록시를 생성한다.
- 어드바이스 적용 여부 판단 단계 - 사용 단계
- 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할 지 말지를 포인트컷을 보고 판단한다.
- 여기서 포인트 컷 조건에 만족하지 않으면 해당 타겟 메소드는 어드바이스를 호출하지 않고 바로 타겟을 호출한다.
AspectJExpressionPointcut
- 여기서는 특별한 표현식으로 복잡한 포인트 컷을 만들 수 있구나로 이해하면 된다. 자세한 건 뒤에 설명한다.
@Bean
public Advisor advisor2(LogTrace logTrace) {
AspectJExpressionPointCut pointcut = new AspectJExpressionPointCut();
// 프록시 적용 대상을 지정한다.
pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noList(..))");
...
}참고
- 만약 여러 포인트 조건을 만족하면 프록시 자동 생성기는 프록시를 여러개 생성할까?
- 하나의 프록시를 생성하고 안에 여러 Advisor가 적용 된다.
@AspectAOP
동작 원리
자동 프록시 생성기가 하는 일
@Aspect를 확인해서 어드바이저(Advisor)로 변환해서 저장한다.- 어드바이저를 기반으로 프록시를 생성한다.
순서
- 실행: 스프링 애플리케이션 로딩 시점에 자동 프록시 생성기를 호출한다.
- 모든 @Aspect 빈 조회: 자동 프록시 생성기는 스프링 컨테이너에서
@Aspect애노테이션이 붙은 스프링 빈을 모두 조회한다. - 어드바이저 생성:
@Aspect어드바이저 빌더를 통해@Aspect애노테이션 정보를 기반으로 어드바이저를 생성한다. - @Aspect 기반 어드바이저 저장: 생성한 어드바이저를
@Aspect어드바이저 빌더 내부에 저장한다.
참고
BeanFactoryAspectJAdvisorBuilder클래스를 통해서@Aspect정보를 기반으로 어드바이저를 생성해준다.
예제
@Aspect는 관점 지향 프로그래밍(AOP)을 가능하게 하는 AspectJ 프로젝트에서 제공하는 애노테이션
@Aspect
public class LogTractAspect {
// 두 개 합쳐서 어드바이저
// pointcut
@Around("execution(* hello.proxy.app..*(..))")
public Object execute(proceedingJoinPoint joinPoint) throws Throwable {
// 어드바이스
// 부가기능 로직
// 메소드를 꺼내는 방법
String message = joinPoint.getSignature().toShortString();
// 타겟 호출
Object result = joinPoint.proceed();
// 별도 로직 실행
return result;
}
}AOP 소개
핵심 기능과 부가 기능
핵심 기능
- 해당 객체가 제공하는 고유 기능
부가 기능
- 핵심 기능을 보조하기 위해 제공하는 기능
- 보통 부가 기능은 여러 클래스에 걸쳐서 함께 사용된다.
(여러 곳에서 동일하게 사용된다.)
- 이런 기능을 횡단 관심사 라고 한다.
부가 기능을 중복해서 적용하면 발생하는 문제
- 많은 반복이 필요하다. ⇒ 중복 코드를 만들어 낸다.
- 변경이 필요한 경우 거대한 수정이 필요하다.
AOP 정리
- 부가 기능과 부가 기능을 어디에 적용할 지 선택하는 기능을 합해서 하나의 모듈로 만들었는데 그것이 바로
Aspect이다. - 애스펙트를 사용한 프로그래밍 방식을 관점 지향 프로그래밍(AOP)라고 한다.
AOP 적용 방식
실제 로직에 추가하는 방법
- 컴파일 시점
.java⇒.class만드는 시점에 부가 기능 로직을 적용할 수 있다.- 실제 대상 코드에 애스팩트를 통한 부가 기능 호출 코드가 포함된다.
- 클래스 로딩 시점
.class파일을 JVM 내부의 클래스 로더에 올릴 때 적용한다.- 자바 언어는
.class파일을 JVM에 저장하기 전에 조작할 수 있다. - 실제 대상 코드에 애스팩트를 통한 부가 기능 호출 코드가 포함된다.
- 런타임 시점(프록시)
- 실제 대상 코드는 그대로 유지된다.
- 대신 프록시를 통해 부가 기능이 적용 된다.
특징
- 프록시는 메소드 오버라이딩 개념으로 동작하기 때문에 생성자, static 메소드, 필드 값 접근에는 프록시를 적용할 수 없다.
- 프록시 사용하는 스프링 AOP의 조인 포인트는 메소드 실행으로 제한 된다.
- 스프링 컨테이너 관리할 수 있는 스프링 빈에만 AOP를 적용할 수 있다.
- 스프링은 AspectJ의 문법을 차용하고 프록시 방식의 AOP를 적용한다. AspectJ를 직접 사용하는 것이 아니다.
AspectJ를 직접 사용하지 않는 이유
- 공부해야 하는 개념이 너무 많아지고 자바 관련 설정도 너무 복잡하고 운영도 어렵다.
- 다만 스프링 AOP는 별도의 추가 자바 설정 없이 스프링만 있으면 편리하게 AOP를 사용할 수 있다.
주요 용어
- 조인 포인트
- 어드바이스가 적용될 수 있는 위치(프로그램 실행 중 지점)
- 추상적인 개념 ⇒ AOP를 적용할 수 있는 모든 지점
- 조인 포인트는 항상 메소드 실행 시점으로 제한된다.
- 포인트 컷
- 조인 포인트 중에서 어드바이스가 적용될 위치를 선별
- 주로 AspectJ 표현식을 사용해서 지정한다.
- 메소드 실행 지점만 지정할 수 있다.
- 타겟
- 어드바이스를 받는 객체
- 어드바이스
- 프록시로 추가되는 부가 기능
Around,Before,After
- 애스펙트
- 어드바이스 + 포인트 컷을 모듈화 한 것
@Aspect- 여러 어드바이스와 포인트 컷이 존재
- 어드바이저
- 하나의 어드바이스와 하나의 포인트 컷
- 위빙
- 포인트컷으로 결정한 타겟의 조인 포인트에 어드바이스를 적용하는 것
스프링 AOP 구현
포인트 컷을 분리할 수 있다.
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))") //pointcut expression
private void allOrder(){} //pointcut signature
// 조인 포인트를 갖고 온다.
@Around("allOrder()")- 메소드 이름과 파라미터를 합쳐서 포인트컷 시그니처 라고 한다.
- 코드 내용을 비워둔다.
- 이렇게 분리하면 의미를 지정할 수 있다.
두 개의 포인트 컷을 지정할 수 있다.
@Around("allOrder() && allService()")&&(and),||(or),!(Not) 3개의 형식 모두 지정 가능하다.
어드바이스 순서 정하기
@Order애노테이션을 적용하면 순서를 지정할 수 있지만 해당 애노테이션은 클라스 단위로 적용할 수 있다. 메소드 단위로 적용하면 안된다
어드바이스 종류
@Around- 메소드 호출 전후에 수행
- 대부분 이걸 사용한다.(모든 걸 할 수 있다.)
@Before@AfterReturning@AfterThrowing- @After
예제
- Around를 제외하고 나머지 4개의 방식은 Around를 쪼개서 특정 부분을 실행하는 것이라고 생각하면 된다.
try {
//@Before
log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
//@AfterReturning
log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
//@AfterThrowing
log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
//@After
log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature());
}- Before 예제
- 실제로
joinPoint.proceed()를 실행하지 않고 간단하게 로직을 지정할 수 있다.
- 실제로
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}- AfterReturning 예제
- 반환 되는 객체 자체를 변경할 수는 없다.
returning절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)- 객체의 경우 메소드를 호출가능하다. (setter가 존재하면 내부 프로퍼티는 변경 가능)
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}- After
- 일반적으로 리소스를 반환하는데 사용한다.
@Around
- 메소드 실행의 주변에 실행된다.
- 가장 강력한 어드바이스
- 조인 포인트 실행 여부 선택(
joinPoint.proceed()) - 전달 값 변환
- 반환 값 변환
- 예외 변환
- 트랜잭션 처럼
try ~ catch ~ finally모두 들어가는 구문 처리 가능 - proceed()를 여러번 실행할 수 있다.
- 조인 포인트 실행 여부 선택(
좋은 설계는 제약이 있는 것이다.
@Around만 있으면 되는데 나머지 4개는 왜 존재하는 가 ⇒ 제약을 통해서 실수을 미연에 방지할 수 있다. 다른 개발자들이 기존의 코드를 보고 해당 코드의 역할을 바로 파악할 수 있다.
포인트 컷
포인트컷 지시자
- 포인트컷 표현식은
execution같은 포인트컷 지시자(Pointcut Designator)로 시작한다. 줄여서 PCD라 한다.
종류
executionwithinargsthistarget@target@within@annotation@argsbean
execution
- 참고용 메소드
// 아래의 메소드 정보를 매칭해서 포인트 컷 대상을 찾아낸다.
public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)문법
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)- 메소드 실행 조인 포인트를 매칭한다.
- ?는 생략 가능
*패턴 지정 가능
예제
- 위 참고용 메소드를 정확하기 매칭하는 표현식
"execution(public String hello.aop.member.MemberServiceImpl.hello(String))"매칭 조건
-
접근제어자?:
public -
반환타입:
String -
선언타입?:
hello.aop.member.MemberServiceImpl -
메서드이름:
hello -
파라미터:
(String) -
예외?: 생략
-
가장 생략한 표현식
"execution(* *(..))"- 서브 패키지 주의
..로 지정하지 않으면 정확한 패키지로 매칭을 하게 되므로 주의 해야 한다..: 정확하게 해당 위치의 패키지..: 해당 위치의 패키지와 그 하위 패키지도 포함
// 잘못 매칭
"execution(* hello.aop.*.*(..))"
// 정상 매칭
"execution(* hello.aop..*.*(..))"- 부모 타입으로 매칭은 가능 하나 부모 타입에 속한 메소드만 매칭이 가능하다.
// 매칭 성공
@Test
void typeExactSuperType() {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod,
MemberServiceImpl.class)).isTrue();
}
// 매칭 실패(부모 타입에 없는 메소드)
@Test
void typeMatchInternal() throws NoSuchMethodException {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isTrue();
}파라미터 매칭 규칙
(String): 정확하게 String 타입 파라미터(): 파라미터가 없어야 한다.(*): 정확히 하나의 파라미터, 단 모든 타입을 허용한다.(*, *): 정확히 두 개의 파라미터, 단 모든 타입을 허용한다.(..): 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. 참고로 파라미터가 없어도 된다.0..*로 이해하면 된다.(String, ..): String 타입으로 시작해야 한다. 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다.- 예)
(String),(String, Xxx),(String, Xxx, Xxx)허용
- 예)
within
- 타입을 매칭한다.
void withinExact() {
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
// 별을 사용할 수도 있다.
pointcut.setExpression("within(hello.aop.member.*Service*)");
}- 표현식에 부모 타입을 지정하면 안된다.
- 타겟의 타입에만 직접 저용 인터페이스를 선정하면 안된다.
- 아래는 execution과의 차이를 확인한다.
// within은 매핑에 실패한다.
pointcut.setExpression("within(hello.aop.member.MemberService)");
// execution은 매핑에 성공한다.
pointcut.setExpression("execution(*hello.aop.member.MemberService.*(..))");args
execution은 파라미터 타입이 정확하게 매칭 되어 야 한다. 클래스에 선언된 정보를 기반으로 매핑한다.(정적)args은 부모 타입을 허용한다. 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다. (동적)
pointcut("args(String)")@target, @wihtin
- @target: 인스턴스의 모든 메소드를 조인 포인트로 적용한다.
- 부모 클래스의 메소드까지 어드바이스를 다 적용한다.
- @within: 해당 타입 내에 있는 메소드만 조인 포인트로 적용한다.
- 부모에 있는 메소드에는 어드바이스를 적용하지 않는다.
@ClassAop
class Target{}- 예제
@Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop)")주의
args, @args, target지시자는 단독으로 사용하면 안된다.- 실제 객체 인스턴스가 생성된 이후 실행될 때 때 어드바이스 적용 여부를 확인 할 수 있다.
- 위와 같은 포인트 컷 지시자가 있으면 모든 스프링 빈에 AOP 프록시를 적용하려고 한다.
- 런타인 시점에 어드 바이스를 적용할 지 판단할 수 있다. 하지만 이렇게 어드 바이스를 적용하려면 필 수 적으로 프록시여야 한다. 따라서 프록시를 만드는 것에 최적화가 불가능하다.
@annotation, @args
- 메소드가 주어진 애노테이션을 갖고 있는 조인포인트를 매칭한다.
// 여기서는 MethodAop를 지정해서 AOP를 지정하는 것이다.
@Around("@annotation(hello.aop.member.annotation.MethodAop)")
public Object doAtAnnotation(ProceedingJointPoint joinPoint){
// 여기에 출력 로그
return joinPoint.proceed();
}bean
- 빈의 이름으로 포인트 컷을 지시할 수 있다.
- 이것은 스프링에서만 사용할 수 있는 특별한 지시자이다.
- 빈 이름이 확정적이라서 빈의 이름이 거의 변경되지 않을 때 사용하면 유용하다.
@Around("bean(orderService) || bean(*Repository))
public Object doBeanLog(ProceedingJointPoint joinPoint){
// 여기에 출력 로그
return joinPoint.proceed();
}매개변수 전달
- 포인트컷 표현식을 사용해서 어드바이스의 매개변수를 전달할 수 있다.
- 대표 예시
@Before("allMember() && args(arg,...))
public void logArgs3(String arg){
// 로그 출력
}- 포인트컷의 이름과 매개변수 이름을 맞춰야 한다.
- 두 개의 타입도 맞아야 한다. (arg의 타입이 맞아야 한다)
@Aspect
static class parameterAspect {
@Pointcut("execution(* hello.aop.member..*.*(..))")
private void allMember() {} // member에 있는 모든 메소드 및 클래스
// 여기서는 직접 꺼내야 하기 때문에 코드가 맛이 아ㅓ업ㅅ다.
@Around("allMember()")
public Object logArgs(ProceedingJoinPoint joinPoin) {
Object arg1 = joinPoint.getArgs()[0];
}
// 위를 개선한 코드 동일한 이름으로 파라미터를 넣어줄 수 있다.
@Around("allMember() && args(arg)")
public Object logArgs2(ProceedingJoinPoint joniPoint, Object args) throws throwable {
// 로깅
return joinPoint.prooceed
}
// 아래와 같이 타입을 지칭할 수 있다.
@Before("allMember() && args(args, ..)")
public void logArgs3(String arg) {
// 로깅 출력
}
}- this와 target을 확인해보자
// 컨테이너에 올라간 객체(프록시 객체를 받을 수 있다)
@Before("allMember() && this(obj)")
public void thisArgs(JointPoint joinPoint, MemberService obj) {
// 로깅 출력
}
// 실제 대상 구현체
@Before("allMember() && target(obj)")
public void targetArgs(JointPoint joinPoint, MemberService obj) {
// 로깅 출력
}- annotation을 사용하는 3개의 방식을 확인하자
//
@Before("allMember() && @target(annotation)")
public void atTargetArgs(JointPoint joinPoint, ClassAop annotation) {
// 로깅 출력
}
//
@Before("allMember() && @within(annotation)")
public void atWithinArgs(JointPoint joinPoint, ClassAop annotation) {
// 로깅 출력
}
// 메소드의 애노테이션을 전달 받는다
@Before("allMember() && @annotation(annotation)")
public void atAnnotation(JointPoint joinPoint, MethodAop annotation) {
annotation.value => 이런 방식으로 annotation에 넣어준 value와 같은 값을 갖고 올 수 있다.
// 로깅 출력
}this, target
this와target은 적용 타입 하나를 정확하게 저장해야한다.- 패턴 사용은 불가능 하다.
- 부모 타입은 허용한다.
// 스프링 빈 객체를 대상으로 하는 조인 포인트
// 스프링 AOP 프록시를 대상
this(hello.aop.member.MemberService)
// Target을 대상으로 하는 조인 포인트
target(hello.aop.member.MemberService)프록시 생성 방식에 따른 차이
스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록된다.
여기서 this 는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 하며 target은 원본 객체를 대상으로 한다.
스프링은 프록시를 생성할 때 JDK 동적 프록시와 CGLIB를 선택할 수 있다. 둘이 프록시를 생성하는 방식이 다르기 때문에 차이가 발생한다.
- JDK 동적 프록시: 인터페이스가 필수이고 인터페이스를 구현한 프록시 객체를 생성한다.
- CGLIB: 인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체를 생성한다.
JDK 동적 프록시
MemberService 인터페이스 지정
this(hello.aop.member.MemberService)- proxy객체를 보고 판단, 문제 없이 적용 된다.
target(hello.aop.member.MemberService)- target 객체를 보고 판단, 문제 없이 적용 된다.
MemberServiceImpl 구체 클래스 지정
this(hello.aop.member.MemberService)- 인터페이스를 기반으로 구현된 클래스이다. 따라서 AOP의 적용 대상이 아니다.
target(hello.aop.member.MemberService)- target 객체를 보고 판단, 문제 없이 적용 된다.
CGLIB 프록시
MemberService 인터페이스 지정
this(hello.aop.member.MemberService)- proxy객체 및 부모 타입도 허용, 문제 없이 적용 된다.
target(hello.aop.member.MemberService)- target 객체 및 부모 타입도 허용, 문제 없이 적용 된다.
MemberServiceImpl 구체 클래스 지정
this(hello.aop.member.MemberService)- CGLIB는 구체 클래스를 상속 받아서 프록시를 만들기 때문에 문제 없이 지정이 가능하다.
- proxy객체 및 부모 타입도 허용, 문제 없이 적용 된다.
target(hello.aop.member.MemberService)- target 객체 및 부모 타입도 허용, 문제 없이 적용 된다.
프록시 생성 클래스 설정
spring.aop.proxy-target-class=true // cglib spring.aop.proxy-target-class=false // JDK 동적 프록시
스프링 AOP 실무 주의 사항
프록시 내부 호출 문제
- AOP를 적용하려면 프록시를 통해서 대상 객체를 호출해야 한다.
- 하지만 대상 객체 내부에서 메소드 호출이 발생하면 프록시를 거치지 않고 대상 객체를 직접 호출하는 문제가 발생한다.
- 프록시 방식의 AOP의 한계이다.
여러 해결방법
자기 자신 주입
public class CallServiceV1 {
private CallServiceV1 callServiceV1;
// setter를 사용해서 의존관계를 주입해야 한다.
@Autowired
public void setCallServiceV1(CallServiceV1 callServiceV1) {
this.callServiceV1 = callServiceV1;
}
public void external() {
// 자기 자신을 받는다.
callServiceV1.internal();
}
public void internal() {
// 로그
}
}지연 조회
- 스프링 빈을 지연해서 조회를 하면 생성자 주입이 가능하다.
- 주입 시점을 클래스 생성 시점이 아니라 사용 시점으로 지연할 수 있다.
public class CallServiceV1 {
private CallServiceV1 callServiceV1;
// ApplicationContext를 주입받을 수 있다.
private final ApplicationContext applicationContext;
public void setCallServiceV1(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
// 위와 같이 ApplicationContext를 직접 주입받는 거는 너무 거대한 로직 중 일부를 사용하는 데 불필요한 주입이다.
// 이를 해결하기 위해서 ObjectProvider를 주입받는 것으로 최소화 할 수 있다.
private final ObjectProvider<CallService2> callServiceProvider;
public void setCallServiceV1(ObjectProvider<CallService2> callServiceProvider) {
this.callServiceProvider = callServiceProvider;
}
public void external() {
// ApplicationContext를 사용 한다.
CallServiceV1 callServiceV1 = applicationContext.getBean(CallServiceV1.class);
// ObjectProider를 사용 한다.
CallServiceV1 callServiceV1 = callServiceProvider.getObject();
// 자기 자신을 받는다.
callServiceV1.internal();
}
public void internal() {
// 로그
}
}구조 변경
- 내부 호출 하는 메소드인
internal()를 다른 클래스로 분리하는 것이 가장 권장되는 방법이다.
프록시 기술의 한계 - 타입 캐스팅
JDK 동적 프록시 한계
- 인터페이스 기반으로 프록시를 생성하는 JDK 동적프록시는 구체 클래스로 타입 캐스팅이 불가능한 한계가 있다.
void jdkProxy() {
MemberServiceImpl proxy = new MemberServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(false); // jdk 동적 프록시
// 프록시를 인터페이스로 캐스팅 성공
MemberService memberServiceProxy = (MemberService) proxyFactory.getProxy();
// JDK 동적 프록시는 인터페이스를 구현한 것이기 때문에 구체 클래스로의 캐스팅이 불가능하다.
// ClassCastException이 발생한다.
MemberServiceImpl castingMemberService = (MemberServiceImpl) memberServiceProxy;
}- 의존관계 주입하면 문제가 발생한다.
- JDK 동적 프록시는 인터페이스를 기반으로 만들어지기 때문에 구체 클래스 정보를 알 수가 없다. 따라서 타입 오류가 발생한다.
public class ProxyDiTest{
@Autowired MemberService memberService
@Autowired MemberServiceImpl memberServiceImpl
@Test go() {
// 여기서 위에서 실행을 하게 될 경우
// JDK 동적 프록시는 memberServiceImpl의 경우 구체 클래스이므로 알 수가 없기 때문에 문제가 발생한다.
}
}CGLIG의 단점
대상 클래스에 기본 생성자가 필수
- 자바언어에서 상속을 받으면 자식 클래스의 생성자를 호출할 때 부모 클래스의 생성자도 호출해줘야 한다.
- CGLIB는 대상 클래스의 기본 생성자를 호출한다는 특징이 있다.
생성자가 2번 호출된다.
- 실제 target의 객체를 생성할 때
- 프록시 객체를 생성할 때 부모 클래스의 생성자 호출