챕터 7: AOP 프로그래밍

binary_j·2023년 11월 13일
0

프록시와 AOP


프록시란?

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

책에 나온 예제대로 팩토리얼의 실행 시간을 측정하는 객체: 프록시
실제로 팩토리얼 연산을 수행하는 객체: 대상 객체
라고 표현한다.

프록시는 핵심 기능을 구현하지 않는다. 대신 여러 객체에 공통으로 적용할 수 있는 기능을 구현한다. 이렇게 공통 기능과 핵심 기능을 나누어 구현하는 것이 AOP의 핵심이다.

AOP

AOP(Aspect Oriented Programming)는 여러 객체에 공통으로 적용할 수 있는 기능을 분리하여 재사용성을 높여주는 프로그래밍 기법이다.
핵심 기능의 코드를 수정하지 않으면서 공통 기능을 추가할 수 있도록 해준다.

AOP에는 다음 세 가지 방법이 있다.

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

첫번째와 두번째 방법은 스프링 AOP에서는 지원하지 않으며 AspectJ와 같은 AOP 전용 도구를 사용해야 한다.

스프링이 제공하는 AOP 방식은 세 번째 방식이다. 스프링 AOP는 프록시 객체를 자동으로 만들어주기 때문에 공통 기능을 구현한 클래스만 알맞게 구현하면 된다.

AOP의 용어

  • Advice: 언제 공통 관심 기능을 핵심 로직에 적용할 지 정의
  • Jointpoint: Advice를 적용 가능한 지점
  • Pointcut: Jointpoint의 부분 집합으로서 실제 Advice가 적용되는 Jointpoint
  • Weaving: Advice를 핵심 로직 코드에 적용하는 것
  • Aspect: 여러 객체에 공통으로 적용되는 기능

Advice의 종류

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

Around Advice가 가장 널리 사용된다.

스프링 AOP 구현


  1. Aspect로 사용할 클래스에 @Aspect 애노테이션 붙임
  2. @Pointcut 애노테이션으로 공통 기능을 적용할 Pointcut 정의
  3. 공통 기능을 구현한 메서드에 @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;

@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 애노테이션이 붙은 클래스를 공통 기능으로 적용하려면 스프링 설정 클래스에 @EnableAspectJAutoProxy 애노테이션을 추가해야 한다.

프록시 생성 방식


스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성한다.

빈 객체가 인터페이스를 상속할 때 인터페이스가 아닌 클래스를 이용해서 프록시를 생성하고 싶다면 다음과 같은 코드를 추가해야 한다.

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)

이렇게 proxyTargetClass를 true로 설정하면 자바 클래스를 상속받아 프록시를 생성한다.

execution 명시자 표현식

위에서 Aspect를 적용할 위치를 지정할 때 사용한 Pointcut 설정을 보면 execution 명시자를 사용하였다.

이 명시자는 Advice를 적용할 메서드를 지정한다. 기본적인 형식은 다음과 같다.

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

*은 모든 값, ..는 0개 이상을 뜻한다.

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;

@Aspect
public class CacheAspect {

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

Pointcut 설정이 첫 번째 인자가 long인 메서드이므로 기존 Calculator의 factorial 메서드에도 적용된다.

어떤 Aspect가 먼저 적용될지는 스프링 프레임워크나 자바 버전에 따라 달라질 수 있기 때문에 순서를 지정하고 싶다면 @Order 애노테이션을 사용해야한다.

@Aspect
@Order(1)

숫자가 작은 Aspect가 먼저 적용된다.

Around의 Pointcut 설정과 @Pointcut 재사용

Pointcut 애노테이션이 아닌 @Around 애노테이션에 execution 명시자를 직접 지정할 수도 있다.

@Around("execution(public * chap07..*())")

다른 클래스에 위치한 @Around 애노테이션에서 같은 Pointcut을 재사용하고 싶다면 @Pointcut이 붙은 메서드를 public으로 지정하고 @Around 애노테이션에 해당 Pointcut의 완전한 클래스 이름을 포함한 메서드 이름을 사용하면 된다.

공통으로 사용되는 Pointcut들은 common 클래스로 빼서 따로 관리하는 것이 편리하다.

0개의 댓글