Open otakijae opened 1 year ago
delegator
핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 (스프링 쪽에서) 프록시라고 부른다
실제 핵심 기능을 실행하는 객체를 대상 객체라고 부른다
ExeTimeCalculator가 프록시이고, ImpeCalculator 객체가 프록시의 대상 객체가 된다
public class MainProxy {
public static void main(String[] args) {
ExeTimeCalculator ttCal1 = new ExeTimeCalculator(new ImpeCalculator());
System.out.println(ttCal1.factorial(20));
ExeTimeCalculator ttCal2 = new ExeTimeCalculator(new RecCalculator());
System.out.println(ttCal2.factorial(20));
}
}
public class ExeTimeCalculator implements Calculator {
private Calculator delegate;
public ExeTimeCalculator(Calculator delegate) {
this.delegate = delegate;
}
@Override
public long factorial(long num) {
long start = System.nanoTime();
long result = delegate.factorial(num);
long end = System.nanoTime();
System.out.printf("%s.factorial(%d) 실행 시간 = %d\n",
delegate.getClass().getSimpleName(),
num, (end - start));
return result;
}
}
public interface Calculator {
public long factorial(long num);
}
public class ImpeCalculator implements Calculator {
@Override
public long factorial(long num) {
long result = 1;
for (long i = 1; i <= num; i++) {
result *= i;
}
return result;
}
}
delegate 대리자로 전달받아서 작업 수행
프록시는 접근 제어 관점에 초점이 맞춰져 있다면, 데코레이터는 기능 추가와 확장에 초점이 맞춰져있어서 ExeTimeCalculator가 decorator 객체에 가깝다고 볼 수 있다
이렇게 공통 기능구현과 핵심 기능 구현을 분리하는 것이 AOP의 핵심이다
여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법
AOP는 핵심 기능과 공통 기능의 구현을 분리함으로써 핵심 기능을 구현한 코드의 수정 없이 공통 기능을 적용할 수 있게 만들어준다
핵심 기능에 공통 기능을 삽입하는 방법
컴파일 시점에 코드에 공통 기능을 삽입하는 방법
클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법
AOP 개발 도구가 소스 코드를 컴파일 하기 전에 공통 구현 코드를 소스에 삽입하는 방식
클래스를 로딩할 때 바이트 코드에 공통기능을 클래스에 삽입하는 방식으로 동작
이 두가지는 스프링 AOP에서 지원하지 않으면 AspectJ 같은 AOP 전용 도구를 사용해서 적용할 수 있다
내부 동작 CGLIB vs JDK Dynamic Proxy 참고
런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 방법
스프링 AOP에서는 프록시 객체를 자동으로 만들어서 실제 객체의 기능 실행 전후 공통 작업 수행하도록 함
Aspect 로 사용할 클래스에 @Aspect annotation을 붙인다. 스프링이 해당 프록시를 만들어준다
@Pointcut annotation으로 공통 기능을 적용할 Pointcut을 정의한다
공통 기능을 구현한 메소드에 @Around annotation을 적용한다
@Aspect
public class ExeTimeAspect {
@Pointcut("execution(public * chap07..*(..))")
private void publicTarget() {
}
@Around("publicTarget()")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try {
Object result = joinPoint.proceed();
return result;
} finally {
long finish = System.nanoTime();
Signature sig = joinPoint.getSignature();
System.out.printf("%s.%s(%s) 실행 시간 : %d ns\n",
joinPoint.getTarget().getClass().getSimpleName(),
sig.getName(), Arrays.toString(joinPoint.getArgs()),
(finish - start));
}
}
}
@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
@Bean
public ExeTimeAspect exeTimeAspect() {
return new ExeTimeAspect();
}
@Bean
public Calculator calculator() {
return new RecCalculator();
}
}
AOP Aspect Advice 클래스(작업)를 위 annotation을 통해서 구현을 하게 되면, 내부적으로 proxy 객체를 생성해서 핵심 기능 수행 전후로 공통 기능을 수행할 수 있도록 만들어준다
스프링은 AOP를 위한 프록시 객체를 생성할 때, 실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성한다
RecCalculator 클래스가 Calculator 인터페이스를 상속하므로 Calculator 인터페이스를 상속받은 프록시 객체를 생성했다
아래처럼 빈의 실제 타입이 RecCalculator라고 하더라도, calculator 빈 객체 타임은 Calculator 인터페이스를 상속받은 프록시 타임이 된다
Calculator cal = ctx.getBean("calculator", Calculator.class);
RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);
==> 이렇게 하면 익셉션 발생
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppCtxWithClassProxy {
@Bean
public ExeTimeAspect exeTimeAspect() {
return new ExeTimeAspect();
}
@Bean
public Calculator calculator() {
return new RecCalculator();
}
}
빈 객체가 인터페이스를 상속할 때, 인터페이스가 아닌 자바 클래스를 이용해서 프록시를 생성하고 싶다면 @EnableAspectJAutoProxy(proxyTargetClass = true)
를 명시해주면 된다. 자바 클래스를 상속받아 프록시를 생성한다
이렇게 하면 아래 코드 예외 발생 안 함
RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);
execution(public void set*(..))
execution(* chap07.*.*())
execute(* chap07..*.*(..))
execution(Long chap07.Calculator.factorial(..))
execution(* get*(*))
execution(* get*(*,*))
execution(* read*(Integer, ..))
cache hit 되면서 바로 리턴할 경우, proceed 하지 않아서 다음 Advice 실행이 안 될 수 있음
Configuration에서 빈 등록한 순서대로 등록이 되는 것 같지만, 뒤죽박죽인 경우 헷갈리니까 order로 순서 지정 가능함
@Order(1)
@Aspect
@Order(1)
public class ExeTimeAspect {
@Pointcut("execution(public * chap07..*(..))")
private void publicTarget() {
}
@Around("publicTarget()")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try {
Object result = joinPoint.proceed();
return result;
} finally {
long finish = System.nanoTime();
Signature sig = joinPoint.getSignature();
System.out.printf("%s.%s(%s) 실행 시간 : %d ns\n",
joinPoint.getTarget().getClass().getSimpleName(),
sig.getName(), Arrays.toString(joinPoint.getArgs()),
(finish - start));
}
}
}
@Order(2)
@Aspect
@Order(2)
public class CacheAspect {
private Map<Long, Object> cache = new HashMap<>();
@Pointcut("execution(public * chap07..*(long))")
public void cacheTarget() {
}
@Around("cacheTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
Long num = (Long) joinPoint.getArgs()[0];
if (cache.containsKey(num)) {
System.out.printf("CacheAspect: Cache에서 구함[%d]\n", num);
return cache.get(num);
}
Object result = joinPoint.proceed();
cache.put(num, result);
System.out.printf("CacheAspect: Cache에 추가[%d]\n", num);
return result;
}
}
aop를 단순히 로깅을 위한 작업으로 활용하는 것이 아니라,
프로젝트 내부 여러 컴포넌트를 꼬치처럼 끼워서 사용할 수 있도록 아키텍처를 가져갈 수 있지 않을까 생각.
하나의 요청이 각각의 컴포넌트를 통과하면서 수행되는 느낌
사용자 요청 ==> 사용자 인증 ==> request validation ==> api 메인 작업 ==> 후속 작업(로그 전송)
같은 흐름
aop의 가장 중요한 개념 중 하나, 스프링에서의 프록시 개념인듯 ==> 어떻게 지원하는지, 구현하는지 내부 동작 CGLIB vs JDK Dynamic Proxy 참고 related https://github.com/endsharp/study/issues/9#issuecomment-1364675344
spring-context, aspectjweaver 모듈 의존성 추가
public class ReculsiveCalculator implements Calculator {
@Override
public long factorial(long num) {
if (num = 0)
return 1;
else
return num * factorial(num - 1);
}
}
코드가 있다고 할 때 이 함수의 실행시간을 알아내려면 어떻게 해야 하는가? 함수 앞뒤에 모두 시간을 출력하는 라인을 추가해야 하며 수정사항이 있을 때마다 반복해야한다. 이를 딱 한번만 수정하기 위해 프록시의 개념이 등장한다.
public class TimeReculsiveCalculator implements Calculator {
private Calculator delegate;
public TimeReculsiveCalculator(Calculator delegate) {
this.delegate = delegete;
}
@Override
public long factorial(long num) {
long start = System.nanoTime();
long result = delegate.factorial(num);
long end = System.nanoTime();
System.out.printf("%s.factorial(%d) 실행 시간 = %d\n", delegate.getClass().getSimpleName(), num, (end-start));
return result;
}
}
이로써 기존 코드 수정 없이 시간을 구할 수 있게 되었다.
엄밀히 말하면 프록시보다는 데코레이터 개념에 가깝다.
proxy: 접근 제어 관점에 초점
decorator: 기능 추가와 확장에 초점
프록시의 특징은 핵심 기능은 구현하지 않는다는 것
Aspect Oriented Programming. 핵심 기능과 공통 기능을 분리하는 것
구현 방법은 세 가지이다.
컴파일 시점에 코드에 공통 기능 삽입
클래스 로딩 시점에 바이트 코드에 공통 기능 삽입
런타임에 프록시 객체를 생성해 공통 기능 삽입
1, 2번은 spring AOP에서 지원하지 않는다. 원할 경우 AspectJ를 사용하면 가능하다.
호출 순서는 다음과 같다.
client -> AOP proxy -> 공통 기능 모듈, 실제 비즈니스 객체
스프링 AOP는 프록시를 자동으로 만들어주므로 직접 구성할 필요 없이, 공통 기능 클래스만 구현하면 된다.
@Aspect를 붙인다.
@Pointcut 애노테이션으로 공통 기능을 적용할 Pointcut을 정의한다.
공통 기능을 구현한 메서드에 @Around 애노테이션을 적용한다.
ex)
@Aspect
public class ExeTimeAspect {
@Pointcut("execution(public * chap07..*(..))") // chap07와 하위 패키지 public method에 적용
private void publicTarget() {}
@Around("publicTarget()") // 메서드의 시작과 끝에 적용
public Object measure(ProceedingJointPoint joinPoint) throws throwable { // ProceedingJointPoint: 대상 메서드 호출 시 사용
...
joinpoint.proceed(); // 대상 메서드 호출
...
}
}
빈 객체가 인터페이스를 상속하면 기본적으로 프록시는 그 인터페이스를 상속한다.
그렇기 때문에 아래 코드는 BeanNotOfRequiredTypeException 오류를 발생한다.
applicationContext.getBean("calculator", ReculsiveCalculator);
인터페이스인 applicationContext.getBean("calculator", Calculator); 로 가져와야 한다.
앞서 Aspect를 적용할 위치를 지정할 때 사용한 @Pointcut 설정에서 excution 명시자를 사용했다.
패턴은 다음과 같다.
execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴))
수식어패턴은 생략가능하며 public, private 등이 온다. 각 패턴은 *을 사용하여 모든 값을 표현하며, 점 두개(..)를 이용하여 0개 이상을 표현한다.
// 예시
execution(public void set*(..))
한 클래스에 두 개 이상의 프록시(어드바이스)가 적용된다면, 그 순서는 자바나 스프링 프레임워크 버전에 따라 달라진다. 그러므로 순서가 중요하다면 @Order 애노테이션을 통해 직접 지정해줄 수 있다.
@Order(숫자)는 숫자의 값이 작은 것부터 큰 것 순으로 Advice를 적용한다.
//예시
@Order(1)
@Aspect
public class ExeTimeAspect {
}
책 정리 & 피드백/논의를 위해