AOP는 Aspect Oriented Programming 의 약자로 관점 지향 프로그래밍
이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.
각 관점을 기준으로 로직을 모듈화할 때, 소스코드 상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는데, 이것을 흩어진 관심사(Crosscutting Concerns)
라고 부른다.
위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심 비즈니스 로직에서 분리해 재사용하겠다는 것이 AOP 사용의 취지이다.
AOP 개념
Advice
advice는 AOP에서 pointcut이 지정한 코드 영역에 적용될 공통 기능을 정의한다. pointcut은 어떤 조건에 따라 특정 코드 영역을 지정하고, advice는 이 코드 영역에 적용할 공통 기능을 정의한다.
Join point
join point는 공통 기능(advice)을 적용할 수 있는 시점을 의미한다. 예를 들어, 메소드 호출 시점, 예외가 발생할 때, 어떤 값을 읽거나 쓸 때 등이 join point가 될 수 있다. pointcut은 join point를 더욱 상세하게 지정하기 위해 사용된다.
Pointcut
pointcut은 advice(공통 기능)을 적용할 코드 영역을 지정할 수 있는 유용한 기능이다. pointcut을 정의할 때는 특정 조건을 지정해야 하는데, 특정 패키지의 모든 클래스와 메소드, 특정 애노테이션이 붙은 메소드, 특정 예외가 발생할 때 등을 지정할 수 있다.
pointcut을 정의한 후에는 이를 적용할 공통 기능을 정의해야 한다. 이 공통 기능은 어드바이스(advice)로 정의할 수 있으며, 적용할 수 있는 종류로는 before advice, after advice, around advice 등이 있다.
before advice
: pointcut이 지정한 코드 영역 전에 실행after advice
: pointcut이 지정한 코드 영역 후에 실행around advice
: pointcut이 지정한 코드 영역 전후에 실행. pointcut이 지정한 코드 영역을 감싸고 실행@AspectJ 지원
@AspectJ는 애노테이션이 달린 일반 자바 클래스로 aspect를 선언하는 스타일을 말한다.
@AspectJ 지원 활성화
Java @Configuration에서 @AspectJ 지원을 활성화하려면 아래와 같이 @EnableAspectJAutoProxy 주석을 추가해야 한다.
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
XML 기반 구성으로 @AspectJ 지원을 활성화하려면 다음 예제와 같이 aop:aspectj-autoproxy 요소를 사용하면 된다.
<aop:aspectj-autoproxy xmlns="http://www.springframework.org/schema/aop"/>
Aspect 선언
@AspectJ 지원이 활성화된 경우, 애플리케이션 컨텍스트에 정의된 Bean 중 @Aspect 어노테이션이 지정된 클래스가 있으면 Spring은 이를 자동 감지하여 Spring AOP를 구성한다.
아래와 같이 org.aspectj.lang.annotation.Aspect 어노테이션을 Aspect 선언할 클래스에 달아준다.
@Aspect
public class NotVeryUsefulAspect {
}
Aspect(@Aspect로 주석이 달린 클래스)는 다른 클래스와 마찬가지로 메서드와 필드를 가질 수 있다. 또한 포인트컷, 어드바이스 선언이 가능하다.
Pointcut 선언
pointcut은 관심있는 join point를 지정하고, 이를 이용해 advice가 언제 실행될지 제어할 수 있게 한다. Spring AOP는 Spring 빈의 메소드 실행 join point만 지원하기 때문에, pointcut은 Spring 빈에서 메소드가 실행될 때와 일치한다고 생각할 수 있다.
pointcut 선언은 이름과 파라미터가 포함된 시그니처와 정확히 어떤 메소드 실행에 우리가 관심이 있는지 결정하는 pointcut 표현식으로 구성되어 있다. pointcut 표현식은 @Pointcut 어노테이션을 사용하여 지정한다.
@Pointcut 주석의 포인트컷 표현식은 AspectJ 포인트컷 표현식이다.
다음은 Spring AOP에서 지원하는 AspectJ 포인트컷 표현식 지정자이다.
execution
Spring AOP를 사용할 때 주로 사용하는 기본 pointcut 지정자로, 메서드 명의 패턴으로 join point를 선택하는 방식
execution 표현식 규칙
다음은 execution 지시자를 활용해 포인트컷을 표현한 것이다.예를 들어 다음과 같은 클래스가 있다고 가정하자.
package com.example.user.UserService; public class UserService { //... }
다음은 해당 클래스를 가리키는 excution 지시자를 활용하는 예시이다.
//com.example.user.UserService 클래스에서 이름이 find로 시작하고 반환타입이 String인 메서드를 대상으로 한다. @Pointcut("execution(String com.example.user.UserService.find*(..))") //com.example.user.UserService 클래스에서 이름이 find로 시작하는 모든 메서드를 대상으로 한다. @Pointcut("execution(* com.example.user.UserService.find*(..))") //모든 패키지에 포함된 클래스 중, 클래스명이 Service로 끝나는 클래스에서 //이름이 find로 시작하는 모든 메서드를 대상으로 한다. @Pointcut("execution(* *..*Service.find*(..))")
예에서 알 수 있듯이 Pointcut 표현식에는 *, .., + 와 같은 와일드카드(wildcard)를 사용할 수 있다. 각각의 의미는 다음과 같다.
within
특정 타입 내에서 실행되는 메서드를 포인트컷으로 사용
//UserService 클래스에 있는 모든 메서드를 대상으로 함
@Pointcut("within(com.example.user.UserService)")
//와일드 카드 사용
@Pointcut("within(*..UserService)")
args
메서드의 인자가 주어진 타입의 인스턴스일 때 포인트컷으로 사용
@target
타깃 클래스에 포함된 어노테이션(Retention=RUNTIME)이 붙은 모든 클래스의 메서드를 포인트컷으로 사용
//@Service 어노테이션이 붙은 모든 클래스의 메서드를 대상으로 함
@Pointcut("@target(org.springframework.stereotype.Service)")
@within
주어진 어노테이션(Retention=CLASS)이 붙은 클래스의 모든 메서드를 포인트컷으로 사용
@annotation
주어진 어노테이션이 붙은 메서드를 포인트컷으로 사용
&&, ||, !
를 사용하여 포인트컷 표현식을 결합할 수 있고, 이름으로 참조할 수도 있다.
import org.aspectj.lang.annotation.Pointcut;
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.myapp.traiding..*)")
private void inTraiding() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
class 경로
사용표현식 이름
사용앞선 예제에서 봤듯이 더 작은 단위의 포인트 컷에 더 복잡한 포인트컷 표현식을 작성하는 것이 좋다.
Advice 선언
Advice는 포인트컷 표현식과 연관되며 포인트컷과 일치하는 메서드 실행 전, 후 또는 주변에서 실행된다.
@Before 어노테이션을 사용하여 Before Advice 선언이 가능하다.
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
@AfterReturning 어노테이션을 사용하여 정의할 수 있다. 이는 일치하는 메서드 실행이 정상적으로 반환된 후 실행된다.
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
때때로, advice 내에서 실제로 반환된 값에 액세스할 필요가 있다. 이 경우에는 @AfterReturning 어노테이션을 사용하여 반환 값을 연결하는 형태로 액세스할 수 있다.
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
returning 속성에 사용된 이름은 advice 메소드의 매개변수 이름과 일치해야 한다. 메서드 실행이 반환될 때, 반환 값은 일치하는 인자 값으로 advice 메서드에 전달된다. returning 구문은 지정된 타입(여기서는 Object, 어떤 반환 값과도 일치)의 값을 반환하는 메서드 동작 후에만 동작하도록 제한한다.
@AfterReturning advice를 사용할 때, 완전히 다른 참조를 반환하는 것은 불가능하다는 것을 유의해야 한다.
즉 결과를 가로채어 수정할 수는 있지만, 전체적인 결과를 바꿀 수는 없다는 뜻이다. 예를 들어 원래 함수에서 리턴되는 객체를 복사하여 변경을 할 수는 있지만, 새로운 객체를 리턴하는 것은 불가능하다.
@AfterThrowing 어노테이션을 사용하여 정의할 수 있다. 이는 일치하는 메서드 실행이 예외를 발생시켜 종료될 때 실행된다.
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
주어진 타입의 예외가 발생할 때만 advice를 실행하고, advice 내부에서 발생한 예외에 액세스할 수 있다. 이 때, throwing 속성을 사용하면 필요한 경우 일치하는 예외를 제한하고, 발생한 예외를 advice 매개변수에 연결할 수 있다.
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다. 메서드 실행이 예외를 발생시켜 종료할 때, 예외는 일치하는 인자 값으로 advice 메서드에 전달된다. 여기서는 DataAccessException 예외가 발생되는 메서드 종료 후에만 동작하도록 구현되어 있는 것이다.
@AfterThrowing은 일반적인 예외 처리 콜백을 나타내지 않는다는 점에 유의해야 한다. @AfterThrowing advice 메서드는 조인 포인트(사용자가 선언한 대상 메서드) 자체에서 발생한 예외만 받아야 하지만, 함께 작동하는 @After / @AfterReturning 메서드에서 발생한 예외는 처리하지 않는다.
@After 어노테이션을 사용하여 정의할 수 있다. 이는 일치하는 메서드 실행이 종료될 때 동작한다. 일반적으로 정상적인 상태와 예외 상태를 모두 처리해야 하므로, 자원 해제 등과 같은 목적으로 사용된다.
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
AspectJ에서 @After advice는 "after finally advice"로 정의되며, try-catch 문의 finally 블록과 비슷한 역할을 한다. 조인 포인트(사용자가 선언한 대상 메서드)에서 발생하는 어떠한 결과에도, 정상적인 반환이나 예외가 발생하더라도 호출된다.
@AfterReturning 과는 다르게 정상적인 반환에만 적용되는 것이 아니라 전체 결과에 적용한다는 차이가 있다.
Around advice는 일치하는 메서드의 실행 "주위"에서 실행된다. 메서드 실행 전과 후에 작업을 수행하며 메서드가 실제로 언제, 어떻게 실행되는지 여부를 결정할 수 있다.
Around advice는 메서드에 @Around 어노테이션을 추가하여 선언한다. 이 때, 메서드는 리턴 타입으로 Object를 선언해야 하며 메서드의 첫 번째 매개 변수는 ProceedingJoinPoint 타입이어야 한다.
advice 메서드 내부에서 ProceedingJoinPoint의 기본 메서드 proceed() 를 호출하면 타겟 메서드가 동작한다. around advice로 반환되는 값은 메서드 호출자가 볼 수 있는 반환 값이다. 위에서 Around advice 메서드의 리턴 타입이 Object여야 한다고 설명한 것이 이와 관련이 있다.
리턴 타입을 void로 선언하면 호출자에게 항상 null이 반환되므로 proceed() 호출 결과를 무시해버린다. 그러므로 around advice 메서드는 Object 타입을 리턴 타입으로 선언하는 것을 권장한다.
아래는 around advice의 구현 예시이다.
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
현재 JoinPoint에 접근
위에서 설명했듯 JoinPoint의 하위 클래스인 ProceedingJoinPoint를 매개변수로 선언하려면 around advice가 필요하다. JoinPoint 인터페이스는 여러 유용한 메서드를 제공한다.
getArgs()
: 타겟 메서드의 인자를 반환getThis()
: 프록시 객체를 반환getTarget()
: 대상 객체를 반환getSignature()
: 타겟 메서드에 대한 정보를 제공. 메서드명, 매개변수, 리턴타입 등을 문자열 형태로 반환toString()
: getSinature() 와 비슷한 정보를 제공. 디버깅에 사용advice에 타겟 메서드 매개변수 전달
타겟 메서드의 인자 값을 advice 내에서 사용하고 싶은 경우, 표현식에 타입 이름 대신 매개변수 이름을 사용하면 해당 타겟 메서드의 인자 값이 advice가 호출될 때 매개변수 값으로 전달된다.
@Around("@annotation(lims.util.Locale.SetLocale) && @annotation(setLocale)")
public Object localeManager(ProceedingJoinPoint joinPoint, SetLocale setLocale) {
// ...
}
@AspectJ 스타일과 XML 스타일 ?
Spring AOP를 사용할 경우 @AspectJ 스타일과 XML 스타일 중 어떤 것을 선택할지에 대해 고려해야 할 요구 사항이 있다.
XML 스타일은 Spring 사용자들에게 익숙하고, 진짜 POJO를 지원한다. 또한 설정으로부터 어떤 aspect가 시스템에 존재하는지 더 명확하게 알 수 있다.
하지만, XML 스타일은 두 가지 단점이 존재한다. 첫번째는 시스템 내에서 어떠한 요구사항이 구현되는지의 정보는 Bean 클래스의 선언과 XML 구성 파일로 분할된다. @AspectJ 스타일을 사용할 때는 이러한 정보가 aspect의 단일 모듈에 캡슐화된다.
두번째, XML 스타일은 @AspectJ 스타일보다 조금 더 제한적이다. @AspectJ 스타일은 포인트컷 이름을 이용해 조합이 가능하지만 XML 스타일은 조합이 불가능하다.
잘봤어요 감사합니다