[Spring] AOP 프로그래밍

2022. 9. 5. 21:42Spring

728x90
반응형
SMALL

프록시

어떤 클래스에 팩토리얼을 계산하는 메소드가 있다고 하자.

만약 이 메소드의 실행 시간을 출력하려면 어떻게 해야할까?

간단한 방법은 메소드의 시작과 끝에서 시간을 구하고, 두 값의 차를 출력하면 될 것이다.

 

만약 시간을 구하는 코드를 변경해야 된다면 너무 귀찮지 않을까?

예를 들면, ms로 구하다가 ns로 구하고 싶다던가... 등등등 (사실 아직 필요성을 완벽히 느끼지는 못했다.)

 

이런 시간을 구하거나 기타 부가적인 기능을 대신하여 제공하는 객체를 프록시라고 부른다.

우리는 프록시를 이용해 위의 귀찮은 상황들을 해결할 수 있다.

 

프록시의 특징은 핵심 기능은 구현하지 않는다는 점이다. 대신 여러 객체에 공통으로 적용할 수 있는 기능을 구현한다.

이렇게 공통 기능 구현과 핵심 기능 구현을 분리하는 것이 AOP의 핵심이다.


AOP

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

핵심 기능과 공통 기능의 구현을 분리함으로써 핵심 기능을 구현한 코드의 수정벗이 공통 기능을 적용할 수 있게 한다.

 

핵심 기능에 공통 기능을 삽입하는 방법에는 세 가지가 있다.

1. 컴파일 시점에 코드에 공통 기능을 삽입하는 방법
2. 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법
3. 런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 방법

1번, 2번 방법은 스프링 AOP에서는 지원하지 않고, AspectJ와 같이 AOP 전용 도구를 사용해 적용할 수 있다.

우리는 스프링이 제공하는 3번째 방법에 대해 알아볼 것이다.

 

AOP 주요 용어는 다음과 같다.

1. Advice : 언제 공통 관심 기능을 핵심 로직에 적용할 지를 정의하고 있다.
2. Joinpoint : Advice를 적용 가능한 지점을 의미한다.
3. Pointcut : Joinpoint의 부분 집합으로 실제 Advice가 적용되는 Joinpoint를 나타낸다.
4. Weaving : Advice를 핵심 로직 코드에 적용하는 것이다.
5. Aspect : 여러 객체에 공통으로 적용되는 기능이다.

Advice에도 다양한 종류가 있는데 가장 널리 사용되는 것은 Around Advice이다.

왜냐하면 대상 객체의 메소드를 다양한 시점에 원하는 기능을 삽입할수 있기 떄문이다.


스프링 AOP 구현

스프링 AOP를 이용해 @Aspect, @Pointcut, @Around 애노테이션을 활용하여 공통 기능을 구현하고 적용할 수 있다.

package aspect;

import java.util.Arrays;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import org.springframework.core.annotation.Order;

@Aspect
@Order(2) //Aspect 적용순서
public class ExeTimeAspect {

	//chap07 패키지와 그 하위 패키지에 위치한 public 메소드를 Pointcut으로 설정
	@Pointcut("execution(public * chap07..*(..))")
	public void publicTarget() { //public으로 하면 다른 클래스에서 해당 Pointcut 사용가능
	}
	
	@Around("publicTarget()") //publicTarget()에 정의한 Pointcut에 공통 기능 적용
	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);
		}
	}
}

위 코드에서는 해당 클래스에 @Aspect 애노테이션을 적용해 Advice와 Pointcut을 사용한다.

 

@Pointcut 애노테이션은 execution 명시자를 통해 Pointcut을 설정하고 있다.

위 코드에서는 chap07 패키지와 그 하위 패키지의 public 메소드를 설정하고 있다.

 

@Around 애노테이션은 @Pointcut을 설정한 메소드를 속성값으로 하여 설정된 Pointcut에 공통 기능을 적용한다.

따라서 Pointcut에 해당하는 메소드에 measure() 메소드를 적용한다.

 

 

위와 같은 @Aspect 애노테이션을 붙인 클래스를 사용하려면 @EnableAspecJAutoProxy 애노테이션을 설정 클래스에

붙여야한다.

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import aspect.ExeTimeAspect;
import chap07.Calculator;
import chap07.RecCalculator;
//import chap07.ImpeCalculator;

@Configuration
@EnableAspectJAutoProxy//(proxyTargetClass=true)
//aspect 클래스를 공통기능으로 적용하기 위함
//aspect 빈 객체를 찾아 빈 객체의 Pointcut과 Around설정 사용
public class AppCtx {
	
	//이 메소드를 주석처리하면 프록시가 아닌
	//RecCalculator의 factorial이 호출됨
	@Bean
	public ExeTimeAspect exeTimeAspect() {
		return new ExeTimeAspect();
	}
	
	@Bean
	public Calculator calculator() { //공통 기능 적용됨
		return new RecCalculator();
		//실제로는 Calculator를 상속하는 프록시타입을 반환한다.
		//그러므로 MainAspect에서 getBean호출 시 RecCalculator타입을 명시할 수 없다.
		
		//위의 @EnableAspectJAutoProxy 애노테이션의 proxyTargetClass 속성을 true로 하면
		//인터페이스(Calculator)가 아닌 자바클래스(RecCalculator)를 상속하므로
		//MainAspect에서 getBean호출 시 RecCalculator타입을 명시할 수 있다.
		
		
		//return new ImpeCalculator();
	}
}

결과적으로 스프링은 @Aspect 애노테이션이 붙은 빈 객체를 찾아 빈 객체의 @Pointcut 과 @Around 를 사용한다.

 

이제 이들을 실행할 메인함수를 보자.

package main;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import chap07.Calculator;
//import chap07.RecCalculator;
import config.AppCtx;

public class MainAspect {

	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);

		Calculator cal = ctx.getBean("calculator", Calculator.class);
		long fiveFact = cal.factorial(5);
		System.out.println("cal.factorial(5) = " + fiveFact);
		System.out.println(cal.getClass().getName());
		ctx.close();
	}

}

메인에서 cal 은 RecCalculator 타입이 아닌 스프링이 생성한 프록시 타입이다.

프록시 타입은 RecCalculator 클래스가 상속받고 있는 Calculator 인터페이스를 상속받아 생성된다.

따라서 getBean 함수의 파라미터로 RecCalculator.class를 넘기면 오류가 생긴다.

 

만일 프록시를 자바 클래스(RecCalculator)의 상속을 받아 생성되도록 하고 싶다면

위의 AppCtx 클래스의 @EnableAspectJAutoProxy 애노테이션의 속성값으로 주석처리된 부분을 설정하면 된다.

그러면 프록시는 RecCalculator를 상속받으므로 getBean 에 RecCalculator.class를 넘길 수 있다.


여러 개의 Advice

한 Pointcut에 여러 Advice를 적용할 수도 있다.

package aspect;

import java.util.HashMap;
import java.util.Map;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import org.springframework.core.annotation.Order;

@Aspect
@Order(1) //Aspect 적용순서
public class CacheAspect {

	private Map<Long, Object> cache = new HashMap<>();
	
	//@Pointcut("execution(public * chap07..*(long))")
	//public void cacheTarget() {
	//}
	
	@Around("ExeTimeAspect.publicTarget()")
	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;
	}
}

위 클래스는 입력받은 인자에 대한 연산 결과를 저장하고, 이후 같은 인자를 입력받을 경우 계산하지 않고 저장된 값을 반환하는 간단한 캐시 역할을 한다.

 

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import aspect.CacheAspect;
import aspect.ExeTimeAspect;
import chap07.Calculator;
import chap07.RecCalculator;

@Configuration
@EnableAspectJAutoProxy
public class AppCtxWithCache {

	@Bean
	public CacheAspect cacheAspect() {
		return new CacheAspect();
	}
	
	@Bean
	public ExeTimeAspect exeTimeAspect() {
		return new ExeTimeAspect();
	}
	
	@Bean
	public Calculator calculator() {
		return new RecCalculator();
	}
}

위와 같은 설정 클래스를 새로 만들면 하나의 Pointcut(팩토리얼 메소드)에 캐시와 시간 출력 기능을 하는 Advice들을 적용할 수 있다.

 

이 때 실행 순서는 CacheAspect -> ExeTimeAspect -> 실제 대상 객체(팩토리얼 메소드) 이다.

CacheAspect 를 보면 인자에 대한 결과값이 HashMap에 존재하면 그 값을 반환하며 바로 종료한다.

때문에 ExeTimeAspect의 measure은 호출되지 않는다.

 

만약 두 Aspect의 순서를 바꾸고 싶다면 @Order 애노테이션을 이용해 순서를 정할 수 있다.


공통 Pointcut

위에서 설명한 CacheAspect 나 ExeTimeAspect 의 @Pointcut 애노테이션이 붙은 메소드를 보면 private으로 설정되어 있다. 이를 public으로 설정하면 다른 클래스에서도 이 Pointcut을 사용할 수 있다.

 

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

각 Aspect 클래스에서 해당 Pointcut을 사용하도록 구성하면 Pointcut 관리가 편해진다.

(Pointcut을 관리하는 클래스는 빈으로 등록할 필요가 없다. 다만 설정한 Pointcut들을 public으로 설정해야 한다.)

728x90
반응형
LIST

'Spring' 카테고리의 다른 글

[Spring] 빈 라이프사이클과 범위  (0) 2022.09.04
[Spring] 컴포넌트 스캔  (0) 2022.09.04
[Spring] 자동 주입  (0) 2022.08.31