[Spring] AOP 프로그래밍 (@Aspect, @Pointcut , @Advice)
2023. 9. 3.
반응형

이번 포스팅에서는 Spring의 3대 요소 중 하나인 AOP에 대해서 다룹니다.

 

AOP를 이해하기 위해서는 프록시라는 개념을 알아야 합니다.

 

프록시핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체입니다.

실제로 핵심 기능을 실행하는 객체대상 객체입니다.

 

쉽게 말하자면 은행에서 입출금 기능을 구현한다고 가정해 보겠습니다.

 

대상 객체입출금의 기능 구현 (핵심 기능)

프록시 객체: 대상객체를 생성해 로깅, 보안 검사, 캐싱 같은 부가적인 기능 구현

 

AOP의 핵심은 프록시처럼 공통 기능과 핵심 기능의 구현을 분리하는 것입니다.

AOPAspect Oriendted Programming의 약자로 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법입니다.

 

AOP의 기본 개념은 핵심 기능에 공통 기능을 삽입하는 것입니다.

즉, 핵심 기능의 코드를 수정하지 않으면서 공통 기능의 구현을 추가하는 것이 AOP입니다.

 

핵심 기능에 공통 기능을 삽입하는 방법에 대해서 알아보겠습니다.

 

1) 컴파일 시점에 코드에 공통 기능을 삽입하는 방법

2) 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법

3) 런타임에 프로시 객체를 생성해서 공통 기능을 삽입하는 방법

 

1,2번 방법은 AspectJ와 같이 AOP 전용 도구를 사용해 적용할 수 있고, 3번이 스프링이 제공하는 AOP 방식입니다.

프록시 방식은 중간에 프록시 객체를 생성하고 실제 객체의 기능을 실행하기 전, 후에 공통 기능을 호출합니다.

 

 

 

반응형

 

 

 

 

스프링 AOP는 프록시 객체를 자동으로 만들어줍니다. 그렇기 때문에 프록시 클래스를 직접 구현할 필요가 없고, 공통 기능을 구현한 클래스만 구현하면 됩니다.

 

AOP의 주요 용어에 대해 알아보겠습니다.

 

  • Advice: 언제 공통 관심 기능을 핵심 로직에 적용할지 정의 (ex. 메소드를 호출하기 전에 트랜잭션 시작)
  • Joinpoint: Advice 적용 가능한 지점을 의미. 메소드 호출, 필드 값 변경 등이 Joinpoint에 해당
  • Pointcut: Joinpoint의 부분 집합. 실제 Advice가 적용되는 Joinpoint를 나타냄
  • Weaving: Advice를 핵심 로직 코드에 적용
  • Aspect: 여러 객체에 공통으로 적용되는 기능 (ex. 트랜잭션, 보안 등)

 

그럼 스프링에서 구현 가능한 Advice 종류에 대해 알아보겠습니다.

 

종류 설명
Before Advice 대상 객체의 메소드 호출 전, 공통 기능 실행
After Returning Advice 대상 객체의 메소드가 익셉션 없이 실행된 후, 공통 기능 실행
After Throwing Advice 대상 객체의 메소드를 실행 중 익셉션이 발생한 경우, 공통 기능 실행
After Advice 익셉션 발생 여부에 관계없이 대상 객체의 메소드 실행 후, 공통 기능 실행(try-catch-finally의 finally와 유사)
Around Advice 대상 객체의 메소드 실행 전, 후 또는 익셉션 발생 시점에, 공통 기능 실행

 

이 중 가장 많이 사용되는 것이 Around Advice입니다. 다양한 시점에 원하는 기능을 삽입할 수 있기 때문입니다.

캐시, 성능 모니터링 기능과 같은 Aspect를 구현할 때에 주로 사용합니다.

 

우선 Spring에서 AOP를 기능을 사용하기 위해서는 의존 대상에 모듈을 추가해야 합니다.

spring-aop라는 모듈을 추가해야 하는데 spring-context 모듈이 추가되어 있다면 따로 추가하지 않아도 됩니다.

 

https://mvnrepository.com

위 링크에서 찾아서 추가하시면 되겠습니다.

 

 

 

반응형

 

 

 

그렇다면 스프링에서 AOP를 어떻게 구현할까요?

 

  • Aspect로 사용할 클래스에 @Aspect 어노테이션 사용
  • @Pointcut 어노테이션으로 공통 기능을 적용할 Pointcut 정의
  • 공통 기능을 구현한 메소드에 @Around 어노테이션 적용

 

ExeTimeAspect.java

 

 

@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));
		}
	}

}

 

@Aspect 어노테이션을 사용해 Aspect로 사용할 클래스를 명시했습니다. 이를 적용한 클래스에는 Advice와 Pointcut을 함께 제공합니다.

@Pointcut은 공통 기능을 적용할 대상을 설정합니다. (더 자세한 내용은 추후에 다룹니다.)

@Around 어노테이션은 Around Advice를 설정합니다.

publicTarget() 메소드에 정의한 Pointcut에 공통 기능을 적용한다는 것을 의미합니다.

즉, publicTarget()에서 특정 패키지에 속한 빈 객체의 public 메소드에 measure() 적용하게 됩니다.

 

execution 명시자는 Advice를 적용할 메소드를 지정할 때 사용합니다. @Pointcut뿐만 아니라 @Around에도 적용 가능합니다.

 

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

 

수식어패턴은 생략가능, public protected 등이 오고 스프링 AOP에서는 public만 가능합니다.

 

 

 

반응형

 

 

 

공통 기능을 적용하는데 필요한 코드를 구현했으니 스프링 설정 클래스를 작성해 보겠습니다.

 

AppCtx.java

 

@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
	@Bean
	public ExeTimeAspect exeTimeAspect() {
		return new ExeTimeAspect();
	}

	@Bean
	public Calculator calculator() {
		return new RecCalculator();
	}

}

 

@Aspect 어노테이션을 붙인 클래스를 공통 기능으로 적용하려면 @EnableAspectJAutoProxy 어노테이션을 설정 클래스에 붙여야 합니다. 이 어노테이션을 추가하면 스프링은 @Aspect 어노테이션이 붙은 빈 객체를 찾아 빈 객체의 @Pointcut, @Around 설정을 사용합니다.

 

예제 코드로 보자면 @Around 어노테이션은 Pointcut으로 publicTarget() 메소드를 설정했고, @Pointcut은 chap07 패키지나 그 하위 패키지에 속한 빈 객체의 public 메소드를 설정하므로  Calculator 타입이 해당 패키지에 속하므로 calculator 빈에 ExeTimeAspect 클래스에 정의한 공통 기능인 measure()를 적용합니다.

 

스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성합니다. 빈 객체가 인터페이스를 상속할 때 인터페이스가 아닌 클래스를 이용해서 프록시를 생성하는 방법도 있습니다.

 

@EnableAspectJAutoProxy(proxyTargetClass = true)

 

이렇게 설정하면 인터페이스가 아닌 자바 클래스를 상속받아 프록시를 생성합니다.

 

 

 

반응형

 

 

 

그럼 Advice의 적용 순서에 대해서 알아보겠습니다.

 

 

AppCtxWithCache.java

 

@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();
	}

}

 

설정 파일을 이렇게 작성했다고 가정해 보겠습니다. cacheAspect()는 캐시 기능을 수행하는 프록시 객체이고 exeTimeAspect()는 수행시간을 측정하는 프록시 객체입니다.

이를 실행하면 당연히 코드에 적힌 순서대로 CacheAspect -> ExetimeAspect -> 실제 대상 객체 순서대로 Advice가 적용되게 됩니다.

이 순서를 @Order 어노테이션을 적용하면 순서를 변경할 수 있습니다.

 

ExetimeAspect 클래스 앞에 @Order(1), CacheAspect 클래스 앞에 @Order(2)를 붙이게 되면

ExetimeAspect -> CacheAspect -> 실제 대상 객체 순서로 적용 순서를 변경 가능합니다.

 

 

 

반응형

 

 

 

@Pointcut은 재사용이 가능합니다.

예를 들어 Pointcut을 여러 Advice가 함께 사용한다면 공통 Pointcut을 재사용할 수도 있습니다.

 

CacheAspect, ExetimeAspect가 만약 같은 @Pointcut을 사용하고 있다면 이 코드를 따로 분리해 각 클래스에 공통으로 적용하는 것입니다.

 

 

ExeTimeAspect.java

 

@Aspect
public class ExeTimeAspect {

	@Around("CommonPointcut.commonTarget()")
	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 코드를 다른 클래스의 메소드로 따로 분리하고 불러와 재사용할 수 있습니다.

Aspect에서 공통으로 사용하는 Pointcut이 있다면 이렇게 사용하면 관리가 편해집니다.

이렇게 사용하면 CommonPointcut은 빈으로 등록할 필요 없이 @Around 어노테이션에서 해당 클래스에 접근 가능하면 해당 Pointcut을 사용할 수 있습니다.

 

 

 

출처: 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문

 

반응형
myoskin