쓰레드 로컬 - 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("비즈니스 로직 실행");
	}
 
}

선 조립 후 실행

  • ContextStrategy를 실행 전에 원하는 모양으로 조립해두고 실행을 하는 방식이다.
  • 단점
    • ContextStrategy를 조립한 이후에는 전략을 변경하기가 번거롭다는 점이다.
    • 변경할 수는 있지만 싱글톤으로 사용하게 되는 경우 동시성 이슈 등 고려해야할 점이 많다.
    • 수정하는 것보다 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들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
  • AdvisorPointcutAdvice가 포함되어 있다. 따라서 적용될 위치는 Pointcut을 사용해서 확인할 수 있고 적용할 기능은 Advice를 통해서 알 수 있다.

프록시 생성 과정

  • 1. 생성: 스프링이 빈 대상이 되는 객체를 생성한다.
  • 2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
  • 3. 모든 Advisor 빈 조회: 자동 프록시 생성기 - 빈 후처리기는 스프링 컨테이너에서 모든 Advisor를 조회한다.
  • 4: 프록시 적용 대상 체크: 앞서 조회한 Advisor에 포함되어 있는 포인트 컷을 사용해서 프록시를 적용할 대상인 지 판단한다. 여기서 하나라도 만족하는 클래스는 프록시 적용 대상이 된다.
  • 5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 반환해서 스프링 빈으로 등록한다.
  • 6. 빈 등록: 반환된 객체는 스프링 빈으로 등록 된다.

주의

  • 이 시점에 사용되는 포인트컷과 등록된 이후에 동작할 때 사용되는 포인트 컷과 구분해야 한다.

포인트 컷은 2가지로 사용이 된다

  1. 프록시 적용 여부 판단 단계 - 생성 단계
    1. 자동 프록시 생성기는 포인트 컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는 지 판단을 한다.
    2. 클래스 + 메소드 조건을 모두 비교해 본다. 이 때 모든 메소드를 체크하는데 포인트 컷 조건에 하나라도 만족하면 프록시를 생성한다.
  2. 어드바이스 적용 여부 판단 단계 - 사용 단계
    1. 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할 지 말지를 포인트컷을 보고 판단한다.
    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)로 변환해서 저장한다.
  • 어드바이저를 기반으로 프록시를 생성한다.

순서

  1. 실행: 스프링 애플리케이션 로딩 시점에 자동 프록시 생성기를 호출한다.
  2. 모든 @Aspect 빈 조회: 자동 프록시 생성기는 스프링 컨테이너에서 @Aspect애노테이션이 붙은 스프링 빈을 모두 조회한다.
  3. 어드바이저 생성: @Aspect어드바이저 빌더를 통해 @Aspect애노테이션 정보를 기반으로 어드바이저를 생성한다.
  4. @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라 한다.

종류

  • execution
  • within
  • args
  • this
  • target
  • @target
  • @within
  • @annotation
  • @args
  • bean

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

  • thistarget은 적용 타입 하나를 정확하게 저장해야한다.
  • 패턴 사용은 불가능 하다.
  • 부모 타입은 허용한다.
// 스프링 빈 객체를 대상으로 하는 조인 포인트
// 스프링 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의 객체를 생성할 때
  • 프록시 객체를 생성할 때 부모 클래스의 생성자 호출