AOP란 Aspect Oriented Programming의 약자로, 관점 지향 프로그래밍이라는 의미이다.
이게 무슨 의미냐면, 여러 개의 메소드가 있는데 그 여러 개에 메소드에서 어떤 똑같은 작업(중복된 코드)를 한다고 치자. 그런 것을 "횡단 관심사"라고 한다.
그런데 그 여러 개의 메소드가 각각의 메소드마다 다른 어떤 특별한 작업을 한다고 하자. (그것이 그 메소드의 핵심 작업이라고 할 수 있겠다.)
그런 것을 "종단 관심사"라고 한다.
AOP는 여러 메소드에서 중복되는 횡단 관심사를 한 메소드로 분리하고, 각각의 메소드에는 종단 관심사에 해당하는 코드만 남겨 그 메소드의 원래 목적(관심사)에 집중할 수 있게 프로그래밍하는 것을 의미한다.
예시를 들어 더 설명해보려고 한다.
출처: 예제로 배우는 스프링 입문 강의 자료 중
위 그림은 AOP가 적용되지 않은 경우이다.
여러 메소드에 AAAA, BBBB 이렇게 중복되는 부분이 있었다.
그런데 어떠한 이유로 AAAA를 AAA로 변경해야하는 일이 생겼다.
그렇다면... 저 AAAA가 적용된 메소드들을 모두 찾아 AAA로 변경해야하는 번거로움이 생긴다.
BBBB도 마찬가지로 BB로 변경해야하는 일이 생긴다면 BBBB가 쓰인 여러 메소드를 모두 찾아 BB로 변경해야한다.
저렇게 여러 메소드에 걸쳐있는 관심사로는 메소드 성능 측정 코드 같은 것이 대표적이다.
만약 어떤 특정 한 메소드가 실행되는 시간을 알고 싶어 StopWatch 객체를 만들어 다음과 같은 코드를 작성했다.
@GetMapping("/owners/new")
public String initCreationForm(Map<String, Object> model) {
StopWatch stopWatch = new StopWatch(); // 성능 측정
stopWatch.start(); // 성능 측정
Owner owner = new Owner();
model.put("owner", owner);
stopWatch.stop(); // 성능 측정
System.out.println(stopWatch.prettyPrint()); // 성능 측정
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
주석으로 //성능 측정
이라고 달려있는 부분이 모두 성능 측정에 관련된 코드이다.
그런데 저 initCreationForm
말고도 다른 메소드도 성능 측정을 하고 싶다면?
저 //성능 측정
주석이 달린 줄의 코드를 모두 복사/붙여넣기 해주면 된다. 성능 측정하고 싶은 메소드가 100개라면 100번 그 짓을 해주면 된다.
100번 반복해서 했다고 치자. 근데 저 성능 측정의 로그를 찍는 위치를 콘솔창이 아닌 로그파일로 바꾸고 싶어졌다고 하자.
그렇다면 다시 그 100개의 메소드를 찾아다니며 모두 파일에 출력하는 코드로 바꾸어주면 된다.(...)
이런 일을 하지 않기 위해 AOP가 생겼다.
만약 저 중복되는 코드(예시에서는 성능을 측정하는 코드)를 모두 한 메소드로 몰아넣고, 각각의 메소드에는 그 메소드의 핵심 기능만 남긴다면 어떨까?
이렇게 해주면, 기존 메소드에는 그 메소드의 핵심 기능만 들어가 있으므로 메소드의 기능을 파악하기 더 수월해진다.
그리고 무엇보다도, 성능 측정과 같은 횡단 관심사 코드 부분에 수정 사항이 생겨도, class AAAABBB
안에 메소드 부분만 한번 수정해주면 되기 때문에 유지보수가 훨씬 쉬워진다.
횡단 관심사를 수정해도 다른 메소드를 고칠 필요가 없다는 사실이 중요하다.
AOP를 구현하는 방법에는 아래와 같이 3가지 방법이 있다.
A.java라는 파일이 있다고 하자. 이 파일은 JDK에 의해 A.class 파일로 컴파일이 될 것이다.
이렇게 컴파일을 하는 과정에서 A.java 파일에는 없는 AOP 코드를 .class 파일에 끼워넣는 방식이다.
즉, A.java에는 AOP 코드가 없지만, A.class 파일에는 AOP 코드가 있다.
이런 기능은 aspectj가 제공하는 기능이다.
이 방법은 A.java ---> A.class로 컴파일되는 과정까지는 일반적인 컴파일 방법으로 컴파일이 된다.
그런데 이 A.class 파일이 JVM의 클래스 로더에 의해 JVM runtime data area(메인 메모리)로 로드될 때, 클래스 파일에 AOP 코드를 끼워넣는 방식이다.
이 기능 또한 aspectj가 제공하고 있다.
자바 디자인 패턴 중 프록시 패턴은 AOP 구현에서 많이 쓰이는 디자인 패턴이다.
프록시 패턴은 Client가 호출하려고 하는 Subject와 같은 인터페이스를 구현한 Proxy 클래스를 만들고, Client가 Subject의 메소드가 아닌 Proxy가 가지고 있는 같은 이름의 메소드를 호출함으로써 흐름을 제어하는 디자인패턴이다.
Proxy의 메소드 안에서는 Subject의 메소드를 호출하고 그 반환값을 변경하지 않고 그대로 반환하는데, Subject의 메소드가 호출되는 부분 앞뒤로 다른 작업을 할 수가 있다.
그 "다른 작업"이 들어가는 부분에 AOP 코드를 넣어서 AOP를 구현할 수 있는 것이다.
여기서는 @LogExecutionTime
이라는 어노테이션을 만들고, 이 어노테이션을 메소드에 붙이면 자동으로 해당 메소드의 성능 측정 로그를 출력하는 코드를 작성해보려고 한다.
우선 LogAspect.java
라는 파일을 만들고 그 안에 AOP 관련 코드를 작성해준다. 이 예시에서는 메소드가 실행되는 시간을 측정해 출력하는 코드를 작성했다.
LogAspect.java
package org.springframework.samples.petclinic.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
@Component
@Aspect
public class LogAspect {
Logger logger = LoggerFactory.getLogger(LogAspect.class);
@Around("@annotation(LogExecutionTime)") // "@LogExecutionTime" 어노테이션이 붙은 메소드 실행 중에 하라는 뜻
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start(); // 스톱워치를 만들고 시작
Object proceed = joinPoint.proceed(); // jointPoint란, 어노테이션이 붙은 타겟 메소드를 의미
stopWatch.stop();
logger.info(stopWatch.prettyPrint());
return proceed; // 프록시 패턴이므로, 원래 메소드의 실행결과를 그대로 리턴
}
}
위 코드에서 @Around("@annotation(LogExecutionTime)")
부분은 @LogExecutionTime
이라는 어노테이션이 붙은 메소드를 만나면 그 메소드 안에서 아래 메소드를 실행하라는 의미이다.
logExecutionTime
메소드의 매개변수로 넘어온 ProceedingJoinPoint joinPoint
즉 joinPoint
는 AOP 코드가 적용될 타겟 메소드를 의미한다. (즉, 여기에서는 @LogExecutionTime
이 붙은 메소드이다.)
메소드 안에서는 Stopwatch
객체를 만들고, 메소드의 시간을 측정하고 있다.
메소드 중간에 Object proceed = joinPoint.proceed();
라는 부분이 있는데 이 부분이 타겟메소드를 호출하는 부분이고, 메소드에 마지막에는 proceed를 리턴함으로써 타겟 메소드의 리턴 결과와 동일한 값을 리턴하는 것을 확인할 수 있다.
위에서 설명했던 프록시 패턴으로 AOP가 구현된 것이다.