상세 컨텐츠

본문 제목

[스프링2] 7장. AOP 프로그래밍

22-23/22-23 Spring 2

by YUZ 유즈 2022. 11. 10. 10:00

본문

728x90

 

 

이 장의 키워드

AOP
프록시, 대상 객체

Advice의 순서
@Pointcut
@Around


핵심

공통 기능 구현 / 핵심 기능 구현을 분리

 

1. 프록시와 AOP

  • 프록시 : 핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체
                   핵심 기능은 구현하지 않지 않고, 여러 객체에 공통으로 적용할 수 있는 기능을 구현한다.
  • 대상 객체 : 실제 핵심 기능을 구현하고 실행하는 객체

 

 

AOP(Aspect Oriented Programming)

여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법

핵심 기능의 코드를 수정하지 않으면서 공통 기능의 구현을 추가한다.

 

 

스프링이 제공하는 AOP 방식

런타임에 프록시 객체를 생성해서 공통 기능을 삽입한다.

 

 

 

예시

// Calculator 인터페이스
public interface Calculator {
	public long factorial(long num);
}

<Calculator 인터페이스>

- 계승을 구하기위한 인터페이스 정의

 

// Calculator 첫번째 구현 클래스
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;
	}
}

<Calculator 첫번째 구현 클래스>

- for 문을 이용하여 계승 값을 구한다.

 

 

// Calculator 두번째 구현 클래스
public class RecCalculator implements Calculator {
	@Override
	public long factorial(long num) {
		if (num == 0)
			return 1;
		else
			return num * factorial(num -1);
	}
}

<Calculator 두번째 구현 클래스>

- 재귀호출을 이용해서 계승 값을 구한다.

 

 

// 공통 기능(실행 시간 출력)이 구현된 클래스 => 프록시 객체가 된다.
public class ExeTimeCalculator implements Calculator {

	private Calculator delegate;  // Calculator 객체
	
	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;
	}
}

<ExeTimeCalculator>

- Calculator를 구현해 공통 기능을 사용할 수 있는 클래스 => 프록시 객체가 된다.

- 생성자를 통해 다른 Calculator (ImpeCalculator, RecCalculator) 객체를 전달받아 delegate 필드에 할당

- factorial() 기능 자체를 직접 구현하기보다는 다른 객체에 factorial() 실행을 위임,

- 다른 부가적인 기능(실행 시간 측정) 실현

 

 

 

 

프록시 기반의 AOP 매커니즘

 

 

AOP 주요 용어

  • Aspect  :  여러 객체에 공통으로 적용되는 기능
  • Advice  :  언제 공통 관심 기능을 핵심 로직에 적용할 지 정의
  • Joinpoint  :  Advice를 적용 가능한 지점
                       스프링은 프록시를 이용해서 AOP를 구현하기 때문에 메서드 호출에 대한 Joinpoint만 지원
  • Pointcut  :  Joinpoint의 부분 집합으로서 실제 Advice가 적용되는 Joinpoint를 나타낸다.
  • Weaving  :  Advice를 핵심 로직 코드에 적용하는 것

 

 

Advice의 종류 (공통 기능 실행 시점)

  • Before Advice  :  대상 객체의 메서드 호출 전
  • After Returning Advice  :  대상 객체의 메서드가 익셉션 없이 실행된 이후
  • After Throwing Advice  :  대상 객체의 메서드를 실행하는 도중 익셉션이 발생한 경우
  • After Advice  :  대상 객체의 메서드 실행 후 (익셉션 발생 여부 상관x)
  • Around Advice  :  대상 객체의 메서드 실행 전, 후 또는 익셉션 발생 시점

▶ 널리 사용되는 것은 Around Advice

 

 

 

2. 스프링 AOP 구현

① Aspect로 사용할 클래스에 @Aspect 애노테이션을 붙인다.
@Pointcut 애노테이션으로 공통 기능을 적용할 Pointcut을 정의한다.
공통 기능을 구현한 메서드@Around 애노테이션을 적용한다.

 

<ExeTimeAspect.java>

import java.util.Arrays;
import org.lang.*;
import com.sun.org.apache.bcel.internal.classfile.Signature;

@Aspect  // Aspect로 사용할 클래스이다.
public class ExeTimeAspect {
	
	// 공통 기능 적용할 Pointcut
	@Pointcut("execution(public * chap07..*(..))")
	private void publicTarget() {
	}
	
    // 공통 기능을 구현한 메서드에 @Around 붙이기
	@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));
			}
	}
}
  • @Pointcut
    공통 기능을 적용할 대상 설정
    publicTarget 메서드 : chap07 패키지와 그 하위패키지에 위치한 public 메서드를 대상으로 설정하겠다는 의미

  • @Around("publicTarget()")
    publicTarget() 메서드에 정의한 Pointcut 공통 기능을 적용하겠다는 의미
    즉, measure() 메서드를 적용하겠다는 의미이다.

 

④ 스프링 설정 클래스를 작성한다.

<AppCtx.java>

@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
	@Bean
	public ExeTimeAspect exeTimeAspect() {
		return new ExeTimeAspect();
	}
	@Bean
	public Calculator calculator() {
		return new RecCalculator();
	}
}
  • @EnableAspectJAutoProxy
    @Aspect 애노테이션을 붙인 클래스를 공통 기능으로 적용해준다.
    @Aspect 애노테이션이 붙은 빈 객체를 찾아서 빈 객체의 @Pointcut 설정과 @Around 설정을 사용한다.
  • @Around, @Pointcut
    Around 애노가 Pointcut으로 publicTarget() 메서드를 설정
    => publicTarget() 메서드의 @Pointcut은 (chap07패키지나 하위패키지의)빈 객체의 메서드를 설정
    => 여기서는 calculator빈에 ExeTimeAspect 클래스에 정의한 공통 기능인 measure()

 




3. 프록시 생성 방식

[MainAspect 클래스 - 14행]

Calculator cal = ctx.getBean("calculator", Calculator.class);

 

스프링은 AOP를 위한 프록시 객체를 생성할 때

실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성

"calculator" 빈의 실제 타입은 Calculator를 상속한 프록시 타입 

 

** @EnableAspectJAutoProxy(proxyTargetClass = true) 

애노테이션의 proxyTargetClass 속성을 true로 지정하면 자바 클래스를 상속받아 프록시 생성

 

 

 

3.1 execution 명시자 표현식

Pointcut 설정에 execution 명시자 사용, Advice를 적용할 메서드를 지정할 때 사용

execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴))

 

 

3.2 Advice 적용 순서

한 Pointcut에 여러 Advice 적용

 

<CacheAspect.java>

@Aspect
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];
        // 위에서 구한 키값이 cache에 존재할 때 : 키에 해당하는 값 리턴
		if (cache.containsKey(num)) {
			System.out.printf("CacheAspect: Cache에서 구함[%d]\n", num);
			return cache.get(num);
		}
        // 위에서 구한 키값이 cache에 존재하지 않을 때 : 프록시 대상 객체 실행
		Object result = joinPoint.proceed();
		cache.put(num.result, result);
		System.out.printf("CacheAspect: Cache에 추가[%d]\n", num);
		return result;
	}
}
  • @Around 값으로 cacheTarget() 메서드 지정
  • @Pointcut 설정은 첫 번째 인자가 long인 메서드 대상
    => execute() 메서드는 Calculator의 factorial(long)메서드에 적용
  • 새로운 Aspect 구현으로 스프링 설정 클래스에 두 개의 Aspect 추가 가능

 

<MainAspectWithCache.class>

public class MainAspectWithCache {
	public static void main(String[] args) {}
	AnnotationConfigApplicationContext ctx =
		new AnnotationConfigApplicationContext(AppCtxWithCache.class);
	
	Calculator cal = ctx.getBean("calculator", Calculator.class);
	cal.factorial(7);
	cal.factorial(7);
	cal.factorial(5);
	cal.factorial(5);
	ctx.close();
}

실행 결과

첫 번째 실행 결과 : ExeTimeAspect와 CacheAspect 모두 적용

두 번째 실행 결과 : CacheAspect만 적용

 

이유?

Advice를 다음 순서로 적용했기 때문이다.

Calculator cal = ctx.getBean("calculator", Calculator.class); 에서 구한

  • calculator 빈
    => CacheAspect 프록시 객체
  • CacheAspect 프록시 객체의 대상 객체
    => ExeTimeAspect 프록시 객체
  • ExeTimeAspect 프록시 객체의 대상 객체
    => 실제 대상 객체

 

첫 번째 factorial() 실행 흐름
두 번째 factorial() 실행 흐름

두 번째 factorial() 실행
=> 첫 번째 실행으로 cache 맵에 데이터가 추가되어 있다.
=> cache.containsKey(num)이 true를 리턴한다.
=> joinPoint.proceed() 를 실행하지 않으므로 ExeTimeAspect나 실제 객체가 실행되지 않는다.
=> CacheAspect가 생성한 메시지만 출력한다.

** Order 애노테이션의 지정값에 따라 Aspect의 적용 순서를 결정할 수 있다.




3.3 공통 Pointcut

만약 같은 Pointcut을 여러 Advice가 함께 사용 

=> 공통 Pointcut 재사용


예시

CacheAspect 클래스의 @Around 애노테이션에서
ExeTimeAspect 클래스의 publicTarget() 메서드의 Pointcut을 사용하고 싶어한다.

@Aspect
public class ExeTimeAspect {
	@Pointcut("execution(public * chap07..*(..))")
    // publicTarget()메서드를 private에서 public으로 변경한다.
    public void public Target() {    
	}
}
@Aspect
public class CacheAspect {
	// 해당 Pointcut의 완전한 클래스 이름을 포함한 메서드 이름을 사용
	@Around("aspect.ExeTimeAspect.publicTarget()")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
	...
	}
}


① Pointcut의 메서드를 public으로 설정한다.
② 다른 클래스에 위치한 @Around에서 Pointcut의 완전한 클래스 이름을 포함한 메서드 이름을 사용한다.

 

 

** 여러 Aspect에서 공통으로 사용하는 Pointcut이 있다면
별도 클래스에 Pointcut을 정의하고, 각 Aspect 클래스에서 해당 Pointcut을 사용하도록 구성

 

 

 

 

문제

1. 핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체는 (프록시)이며,
    실제 핵심 기능을 구현하고 실행하는 객체는 (대상 객체)이다

2. 스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체가 인터페이스를 상속하면 (인터페이스)를
    이용해서 프록시를 생성한다.
3. Advice를 적용 가능한 지점을 (     ㉠   )라고 하며, 이 때 실제 Advice가 적용되는 (      ㉠        )을   (      ㉡       )라 한다.
Joinpoint   ㉡Pointcut
4. 대상 객체의 메서드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능을 실행하는데 사용되는 Advice는 (Around Advice)이다.

5. @Aspect 애노테이션을 붙인 클래스를 공통 기능으로 적용하려면 (@EnableAspectJAutoProxy) 애노테이션을 설정 클래스에 붙여야한다.

6. (execution 명시자)는 Advice를 적용할 메서드를 지정할 때 사용하며, Pointcut이나 Around 애노테이션에 지정할 수 있다.

7. (AOP)는 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법이다.




1.  7행, 14행의 빈칸을 채우세요

// 공통 기능(실행 시간 출력)이 구현된 클래스 => 프록시 객체가 된다.
// ExeTimeCalculator.java
public class ExeTimeCalculator implements Calculator {

	private Calculator delegate;  // Calculator 객체
	
	public ExeTimeCalculator(                       ) {
		this.delegate = delegate;
	}
	
	@Override
	public long factorial(long num) {
		long start = System.nanoTime();   // 전
		long result =         .factorial(num);
		long end = System.nanoTime();     // 후
		System.out.printf("%s.factorial(%d) 실행 시간 = %d\n",
				delegate.getClass().getSimpleName(),
				num, (end-start));
		return result;
	}
}

 

2. 1행, 5행, 10행의 빈칸을 채우세요.

@        // Aspect로 사용할 클래스이다.
public class ExeTimeAspect {
	
	// 공통 기능 적용할 
	@          ("execution(public * chap07..*(..))")
	private void publicTarget() {
	}
	
    // 공통 기능을 구현한 메서드에 @       붙이기
	@           ("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));
			}
	}
}
728x90

관련글 더보기