AOP 프로그래밍

HeeSeong·2021년 7월 30일
1
post-thumbnail

AOP 프로그래밍


프록시와 AOP


핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 프록시(Proxy)라고 부른다. 실제 핵심 기능을 실행하는 객체는 대상 객체라고 부른다.

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

AOP의 기본 개념은 핵심 기능에 공통 기능을 삽입하는 것이다. 즉 핵심 기능의 코드를 수정하지 않으면서 공통 기능의 구현을 추가하는 것이 AOP이다. 핵심 기능에 공통 기능을 삽입하는 방법에는 세가지가 있다.


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

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

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


첫 번째 방법은 AOP 개발 도구가 소스 코드를 컴파일 하기 전에 공통 구현 코드를 소스에 삽입하는 방식으로 동작한다. 두번째 방법은 클래스를 로딩할 대 바이트 코드에 공통 기능을 클래스에 삽입하는 방식으로 동작한다.
이 두 가지는 스프링 AOP에서는 지원하지 않으며, AspectJ와 같이 AOP 전용 도구를 사용해서 적용할 수 있다.

스프링이 제공하는 AOP 방식은 프록시를 이용한 세 번째 방식이다. 스프링 AOP는 프록시 객체를 자동으로 만들어준다.


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

  • 스프링 Advice 종류
용어의미
Before Advice대상 객체의 메서드 호출 전에 공통 기능을 실행한다.
After Returning Advice대상 객체의 메서드가 익셉션 없이 실행된 이후에 공통 기능을 실행한다.
After Throwing Advice대상 객체의 메서드를 실행하는 도중 익셉션이 발생한 경우에 공통 기능을 실행한다.
After Advice익셉션 발생 여부에 상관없이 대상 객체의 메서드 실행 후 공통 기능을 실행한다.
Around Advice대상 객체의 메서드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능을 실행하는데 사용한다. 캐시, 성능 모니터링 등에 이용한다.

스프링 AOP 구현


스프링 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를 어디에 적용할지 설정하면 된다. Aspect는 @Aspect 애노테이션을 이용해서 구현한다. 프록시는 스프링 프레임워크가 알아서 만들어준다.

@Pointcut은 공통 기능을 적용할 대상을 설정한다. 위의 설정은 chap07 패키지와 그 하위 패키지에 위치한 타입의 public 메서드를 Pointcut으로 설정한다. @Around 애노테이션은 Around Advice를 설정한다. 값이 "publicTarget()"인데 이는 publicTarget() 메서드에 정의한 Pointcut에 공통 기능을 적용한다는 것을 의미한다.


  • 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 설정을 사용한다.


프록시 생성 방식


스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해서 프록시를 생성한다. 빈 객체가 인터페이스를 상속할 때 인터페이스가 아닌 클래스를 이용해서 프록시를 생성하고 싶다면 다음과 같이 설정하면 된다.

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppCtx {

}

@EnableAspectAutoProxy 애노테이션의 proxyTargetClass 속성을 true로 지정하면 인터페이스가 아닌 자바 클래스를 상속받아 프록시를 생성한다.


execution 명시자 표현식


@Pointcut("execution(public * chap07..*(..))")
private void publicTarget() {

}

execution 명시자는 Adivce를 적용할 메서드를 지정할 때 사용한다.

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

수식어 패턴은 생략 가능하며 public, protected 등이 온다. 스프링 AOP는 public 메서드에만 적용할 수 있기 때문에 사실상 public만 의미있다.

각 패턴은 '*'을 이용하여 모든 값을 표현할 수 있다. 또한 '..'을 이용하여 0개 이상이라는 의미를 표현할 수 있다.


어떤 Aspect가 먼저 적용될지는 스프링 프레임워크나 자바 버전에 따라 달라질 수 있기 때문에 적용 순서가 중요하다면 직접 순서를 지정해야 한다. 이때 사용하는 것이 @Order 애노테이션이다. @Order 애노테이션을 클래스에 붙이면 지정한 값에 따라 적용 순서를 결정한다. @Order 애노테이션의 값이 작으면 먼저 적용하고 크면 나중에 적용한다.


@Aspect
@Order(1)
public class ExeTimeAspect {
}

Aspect
@Order(2)
public class CacheAspect {
}

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


@Aspect
public class CacheAspect {

    @Around("execution(public * chap07..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    
    }
}

profile
끊임없이 성장하고 싶은 개발자

0개의 댓글