Spring Boot - AOP!
AOP (Aspect Oriented Programming)
AOP: 관점 지향적 프로그래밍 기법(Aspect Oriented Programming)
스프링에서 모든 기능은 핵심 비지니스 로직과 공통 기능으로 나뉜다.
로그인 인증과정 같이 중복되고 공통적으로 사용되는 기능을 공통 기능이라 하며
cross-cutting concern(공통 관심사항)이라 표현한다.

스프링 부트의 수많은 기본 Filter, Interceptor 등이 모두 AOP 기법으로 개발되었다 할 수 있다.
AOP 라이브러리
AOP 기법을 위한 대표적인 라이브러리는 아래 2개
-
Spring AOP
간단한 AOP 구현을 위해 Spring 컨테이너가 관리하는 빈에만 적용 가능한 라이브러리 -
AspectJ
완전한 AOP 솔루션 제공, 모든 자바객체에 적용가능하지만 복잡한 구성을 가지는 라이브러리
Spring AOP 를 사용하게되면 자동으로 생성되는 코드로 인해 AspectJ 보다 훨씬 성능이 느리지만
간단한 사용방법과 설정으로 인해 Spring AOP 를 주로 사용한다.
두 라이브러리가 아예 분리되어있진 않고
Spring AOP코드에서AOP개념을 가져오기 위해AspectJ라이브러리에 의존중임.
AOP 주요 용어
| 용어 | 의미 |
|---|---|
Aspect |
여러 객체에 공통적으로 적용되는 기능을 Aspect라 한다. 인증, 트랜잭션, 보안 과 같은 공통적 기능을 의미한다. |
Advice |
공통적으로 사용되는 기능(Aspect)를 핵심 로직에 언제 적용하는지, 시점을 의미한다(핵심기능 시작 전, 후, 예외발생시 등). |
JoinPoint |
Advice를 적용 가능한 지점을 의미한다. 핵심 기능과 공통 기능이 어느지점에서 만나는지를 의미하며 메서드 호출, 필드값 변경 지점을 의미한다. |
Pointcut |
많은 JoinPoint중에 실제 핵심기능과 보조기능이 접하게 되는 지점을 Pointcut이라 한다. JoinPoint의 부분집합을 Pointcut이라 할 수 있다. |
Weaving |
프로그래머가 설정한 Advice대로 Aspect를 핵심로직 코드에 적용하는 것, 행위를 뜻한다. |
구현되는 Advice의 종류
| Advice명 | 설명 | 사용 클래스 |
|---|---|---|
Before Advice |
핵심기능 전에 수행하는 공통 기능 (로그인 체크 등) | MethodBeforeAdvice |
Around Advice |
핵심기능 수행 전 후에 수행하는 고통 기능 (시간 체크 등) | MethodInterceptor |
After Returning Advice |
예외 발생이 없을 경우 수행하는 기능 | AfterReturningAdvice |
After Throwing Advice |
예외 발생할 경우 수행하는 기능 | ThrowsAdvice |
After Advice |
예외 발생 상관없이 핵심기능 수행 후 수행하는 공통 기능 | AfterAdvice |
공통기능이 핵심기능 어느시점에 실행 되는지에 따라 위 5가지 Advice를 택해야 한다.
Weaving하는 시점
다음 3가지 시점에 Weaving(적용)한다.
- 컴파일시
- 클래스 로딩시
- 런타임시
Spring AOP에선 3번째 방법인 런타임시에Weaving할 수 있는데 Proxy기반의 AOP이다.

crossCuttingConcern: 공통 관심사항
coreConcern: 핵심 기능
핵심 기능을 하는 클래스, 보조(공통)기능을 하는 클래스가 있으면 따로 2개의 클래스가 올라가고
가상으로 만들어진 프로그래머가 설정한 대로 위의 여러 클래스(핵심, 보조 클래스들)가 적용(Weaving)된 Proxy클래스(가짜) 가 만들어진다.
이러한 Proxy 클래스를 만드는 방법은 다음 3가지가 있다.
- XML스키마 기반의 POJO 클래스를 이용한 AOP구현
- AspectJ에서 정의한 @Aspect 어노테이션 기반의 AOP구현
- 스프링API를 이용한 AOP구현
이중 XML스키마를 이용한 1번과 어노테이션을 사용한 2번을 알아보자
Proxy 객체 만들어보기
위의 3가지 방법은 스프링에서 제공하는 기능을 이용해 Proxy클래스를 만드는 것 인데
어떻게 Proxy 클래스가 만들어지는지 알기 위해 라이브러리를 사용하지 않고 Proxy클래스를 만들어 보자.
- 핵심기능을 계산기 기능
- 공통기능을 핵심기능이 처리되는 시간을 로그로 기록
먼저 계산기 뼈대를 가진 Calculator 인터페이스 작성
public interface Calculator {
int add(int x, int y);
int sub(int x, int y);
int mult(int x, int y);
int div(int x, int y);
}
그리고 이를 구현한 CalcultorImpl 클래스 작성
@Component
public class CaculatorImpl implements Calculator {
@Override
public int add(int x, int y) {
Log log = LogFactory.getLog(this.getClass());
StopWatch sw = new StopWatch();
log.info("add() start");
sw.start();
int result = x + y;
sw.stop();
log.info("add() stop");
log.info("add() 처리시간: " + sw.getTotalTimeMillis());
return result;
}
@Override
public int sub(int x, int y) {
Log log = LogFactory.getLog(this.getClass());
StopWatch sw = new StopWatch();
log.info("sub() start");
sw.start();
int result = x - y;
sw.stop();
log.info("sub() stop");
log.info("sub() 처리시간: " + sw.getTotalTimeMillis());
return result;
}
@Override
public int mult(int x, int y) {
int result = x * y; return result;
}
@Override
public int div(int x, int y) {
int result = x / y; return result;
}
}
public static void main(String[] args) {
Calculator calc = new CaculatorImpl();
System.out.println(calc.add(4,2));
System.out.println(calc.sub(4,2));
}
// CaculatorImpl add
// add() start
// CaculatorImpl add
// add() stop
// CaculatorImpl add
// add() 처리시간: 0
// 6
// CaculatorImpl sub
// sub() start
// CaculatorImpl sub
// sub() stop
// CaculatorImpl sub
// sub() 처리시간: 0
// 2
로그출력같은 공통적인 보조업무를 빼서 AOP 기능수행을 하는 Proxy 클래스를 생성
java.lang.reflect.InvocationHandler 인터페이스를 상속한 LogPrintHandler클래스를 정의
공통 기능을 구현한 보조클래스와 핵심 기능이 합쳐진 proxy 클래스 이다.
public class LogPrintHandler implements InvocationHandler {
private Object target;
public LogPrintHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 보조 기능1
Log log = LogFactory.getLog(this.getClass());
StopWatch sw = new StopWatch();
log.info(method.getName() + "() start"); //method 이름을 가져오는 메서드
sw.start();
int result = (int) method.invoke(target, args); // 핵심기능 수행
// 보조 기능2
sw.stop();
log.info(method.getName() + "() stop");
log.info(method.getName() + "() 처리시간: " + sw.getTotalTimeMillis());
return result;
}
}
AOP과정에서 위와같은 LogPrintHandler와 같은 java.lang.reflect.Proxy 인스턴스가 생기게 된다 보면 된다.
public static void main(String[] args) {
Calculator target = new CaculatorImpl();
Calculator calc_proxy = (Calculator)Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogPrintHandler(target));
System.out.println(calc_proxy.add(1, 2));
}
Spring AOP
런타임 AOP 에서 사용하는 방법은 아래 2가지
JDK Dynamic Proxycglib(Code Generator Library) Proxy
JDK Dynamic Proxy 는 위에서 살펴보았던 reflect 패키지의 클래스를 사용하는 방법이다.
java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler
cglib 는 바이트코드 조작을 통해 프록시 객체를 생성하기때문에 reflect 패키지의 인터페이스 구현 등의 별도의 추가작업을 하지 않아도 Proxy 패턴 구현이 가능하다.
org.springframework.cglib.proxy.Enhancerorg.springframework.cglib.proxy.MethodInterceptor
Spring AOP 에선 cglib Proxy 를 사용한다.
런타임시에 각종 어노테이션이 설정된 클래스들을 classpath 에서 찾은 뒤 cglib 클래스를 사용하여 동적으로 프록시 관련 코드를 생성 후 로직 사이사이에 삽입하는 과정을 거친다.
cglib Proxy 객체 생성
Spring AOP 를 사용하면 지금부터 소개할 cglib 클래스들이 자동으로 런타임시 생성된다고 보면 된다.
하지만 여기선 Spring AOP 를 사용하기 전에 직접 cglib 를 클래스를 생성하고 Bean 으로 등록하는 과정을 수행한다.
cglib 클래스를 사용하기 위해 아래 dependency 를 삽입.
implementation 'org.springframework.boot:spring-boot-starter-aop'
add 라는 핵심 기능 전 후 에 아래 3가지 AOP 작업을 수행하도록 설정한다.
- 실행 전 로그기록
- 실행 후 로그기록
- 소요시간 로그 기록
공통 기능인 Around Advice 역할을 수행할 cglib Proxy 클래스로 생성한다.
Around Advice기능을 가진 Proxy클래스를 정의하려면 먼저 공통기능 클래스를 정의하기 위한 MethodInterceptor 인터페이스를 구현해야한다.
@Component
public class LogPrintAroundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation method) throws Throwable {
String m_name = method.getMethod().getName();
Log log = LogFactory.getLog(this.getClass());
StopWatch sw = new StopWatch();
log.info(m_name + " start");
sw.start();
Object result = method.proceed();
sw.stop();
log.info(m_name + " stop");
log.info(m_name + " 처리시간: " + sw.getTotalTimeMillis());
return result;
}
}
Around Advice을 통해 핵심기능 함수 실행문으로 들어온 후 시작 바로후, 끝나기 바로 전에 공통기능 삽입이 가능하다.
만약 핵심기능 실행 전, 혹은 후에 공통기능을 먼저 삽입하고 싶다면,
핵심기능 수행전 매개변수, 핵심기능 수행 후 반환값을 사용해 공통 기능을 수행해야 한다면
Before Advice 역할을 하는 MethodBeforeAdvice 인터페이스를 구현,
After Returning Advice역할을 하는 AfterReturningAdvice 인터페이스를 구현한 공통 기능 클래스를 정의해야 한다.
그냥 Spring 프레임워크를 사용하게 될 경우
위와 같이 AOP 기법을 사용한 공통 기능 클래스를 모두 정의하였으면 핵심기능 클래스와 공통기능 클래스를 하나의 Proxy클래스로 만들어주면 된다.
Spring AOP 라이브러리
cglib 클래스를 사용하여 AOP 를 사용하는 방법을 알아보았는데 스프링에선 대부분 어노테이션을 사용해서 AOP 를 수행한다.
@Aspect
@Configuration
@RequiredArgsConstructor
public class LoggingAspect {
/**
* Retrieves the {@link Logger} associated to the given {@link JoinPoint}.
*
* @param joinPoint join point we want the logger for.
* @return {@link Logger} associated to the given {@link JoinPoint}.
*/
private Logger logger(JoinPoint joinPoint) {
return LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringTypeName());
}
/**
* Pointcut that matches all repositories, services and Web REST endpoints.
*/
@Pointcut("within(@org.springframework.stereotype.Service *)" +
" || within(@org.springframework.web.bind.annotation.RestController *)")
public void springBeanPointcut() {
// Method is empty as this is just a Pointcut, the implementations are in the advices.
}
/**
* Advice that logs when a method is entered and exited.
*
* @param joinPoint join point for advice.
* @return result.
* @throws Throwable throws {@link IllegalArgumentException}.
*/
@Before("springBeanPointcut()")
public void logBefore(JoinPoint joinPoint) throws Throwable {
Logger log = logger(joinPoint);
log.info("Enter: {}() with argument[s] = {}", joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs()));
}
}
AOP Advice annotation
AOP Advice 를 설정할 수 있는 어노테이션은 아래 5가지
@Before
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
// 로직
}
@After
@After("execution(* com.example.service.*.*(..))")
public void afterAdvice(JoinPoint joinPoint) {
// 로직
}
@AfterReturning
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
// 로직
}
@AfterThrowing
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "error")
public void afterThrowingAdvice(JoinPoint joinPoint, Throwable error) {
// 로직
}
@Around
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 메서드 호출 전 로직
Object result = joinPoint.proceed();
// 메서드 호출 후 로직
return result;
}
// org.springframework.aop.AfterReturningAdvice 클래스 상속
@Component
public class LogPrintAfterReturningAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
// 수행이 완료된 후 호출되는 Advice임으로 첫번째 매개변수로 method가 반환한 값이 들어간다.
String m_name = method.getName();
Log log = LogFactory.getLog(this.getClass());
log.info("<<< " + m_name + "() : AfterReturningAdvice called...");
}
}
직접 어노테이션 생성
@Inherited // 자식 클래스에도 상속
@Retention(value = RetentionPolicy.RUNTIME) // 어노테이션 보존 라이프타임, 런타임에도 유지
@Target(ElementType.METHOD)
public @interface CheckApiKey {
}
Spring AOP 의 문제점
- Proxy 객체의 외부 호출을 래핑하는 방식이기 때문에 내부 함수호출시에는 AOP 과정이 일어나지 않는다.
- 추가적인 클래스를 다량 작성해야한다.
컴파일 타임에선 어노테이션 사용의 문제가 있어도 알수 없어 런타임시에 확인이 필요하다.
빌드시 어노테이션 오류를 체크하기 위해 Annotation Processor 를 사용할 수 있다.
https://kouzie.github.io/cicd/gradle/gradle-Plugin,-buildSrc/#annotation-processor