Aspect Oriented Programming의 약자로 관점지향 프로그래밍이다. IoC가 낮은 결합도와 관련된 것이라면, AOP는 높은 응집도와 관련되어 있다.
서비스들의 비즈니스 메소드들은 복잡한 코드로 구성되어있다. 하지만 이들 중 대부분은 로깅(logging), 트랜잭션 처리, 보안 등과 관련된 코드로 구성되어 있을 수 있다. 이럴 때 핵심 로직을 제외하고 꼭 필요하면서 모듈화 할 수 있는 부분을 분리하여 관리하는 것이다.
좀 더 쉽게 설명을하자면, 어플리케이션에서 관심사(기능)의 분리이자 핵심적인 기능에서 부가적인 기능을 분리하는 것이다. 분리한 부가 기능은 Aspect라는 독특한 모듈 형태로 만들어서 설계하고 개발하는 방법이라고 할 수 있다.
위의 A, B, C 클래스의 중복되는 메소드, 필드, 코드들이 나타나는 경우 Class A의 주황색 부분을 수정해야 할 경우, B와 C클래스의 주황색 부분을 찾아가 전부 코드를 수정해야 한다. 즉, SOLID 원칙도 위배하고 유지보수도 어려워지는데, 이러한 반복되는 코드를 흩어진 관심사(Crosscutting Concerns)라고 한다. 이렇게 흩어진 관심사를 AOP는 Aspect를 통해 해결한다. 위의 그림에서도 공통되는 부분을 Aspect X, Y, Z로 각각 모듈화한 것을 볼 수 있다. 결국 개발자는 모듈화시킨 Aspect를 어느 클래스(위 그림의 경우 Class A or B or C)에 사용하면 되는지 정의해주면 된다.
결국, Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지이다.
스프링 AOP를 공부하다보면 항상 나오는 실행 시간 출력 예제를 살펴보자.
public interface EventService {
void createEvent();
void publishEvent();
void deleteEvent();
}
@Service
public class SimpleEventService implements EventService {
@Override
public void createEvent() {
long begin = System.currentTimeMills();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Created an Event!");
System.out.println("System.currentTimeMills() - begin);
}
@Override
public void publishEvent() {
long begin = System.currentTimeMills();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Published an Event!");
System.out.println("System.currentTimeMills() - begin);
}
@Override
public void deleteEvent(){}
위와 같이 EventService 인터페이스를 구현한 SimpleEventService 클래스가 있다. 우리는 createEvent()와 publishEvent() 메소드의 실행 시간을 측정하고자한다. 이 때 두 메소드에서는 중복되는 코드가 발생한다. 예시와 다르게 실제 프로젝트에서는 메소드의 수가 많기 때문에 더 많은 중복이 발생할 수 있다. 이 때 우리는 AOP를 사용한다. AOP는 프록시 패턴을 사용하여 기존 코드의 수정 없이 메소드들의 성능을 측정할 수 있다.
클라이언트는 Subject 인터페이스 타입으로 프록시 객체를 사용하게 되고, 프록시는 Real Subject를 감싸서 클라이언트의 요청을 처리한다. 프록시 패턴의 목적은 기존 코드 변경 없이 접근 제어 또는 부가 기능을 추가하기 위해서이다. 여기서 위의 EventService 인터페이스를 구현하는 프록시 클래스를 만들어보자.
@Primary
@Service
public class ProxySimpleEventService implements EventService {
@Autowired
private SimpleEventService simpleEventservice;
@Override
public void createEvent() {
long begin = System.currentTimeMills();
simpleEventService.createEvent();
System.out.println(System.currentTimeMills() - begin);
}
@Override
public void publishEvent() {
long begin = System.currentTimeMills();
simpleEventService.publishEvent();
System.out.println(System.currentTimeMills() - begin);
}
@Override
public void deleteEvent() {}
}
우선 @Primary 어노테이션은 같은 타입의 빈이 여러개 존재할 때 우선순위를 높게 주는 어노테이션이다. 그리고 위의 Proxy 클래스의 코드를 보면 필드로 실제 핵심 코드를 담당하는 SimpleEventService를 가지고 있다. 이러한 구조를 프록시 패턴이라고 하는데, 기존의 SimpleEventService 클래스의 코드에는 성능을 측정하는 코드를 작성하지 않아도 된다는 장점이 생긴 것이다. 하지만 이런 방식도 메소드가 많다면 코드의 중복이 많이 일어날 것이고 매번 Proxy 클래스를 직접 만들어야하는 번거로움이 존재하기 때문에 Spring AOP를 사용한다.
스프링도 위에서 본 프록시를 이용하여 AOP를 구현하고 있다. AOP의 핵심 기능은 코드를 수정하지 않으면서 공통의 기능 구현을 추가하는 것이라고 강조한다. 핵심 기능에 공통 기능을 추가하는 방법에는 아래와 같이 3가지가 있다.
스프링에서 많이 사용하는 방식은 프록시를 이용하는 세번째 방법으로 스프링 AOP는 프록시 객체를 자동으로 만들어준다. 따라서 위에서 본 ProxySimpleEventService 클래스처럼 상위 타입의 인터페이스를 상속 받은 클래스를 직접 구현할 필요가 없다. 단지 공통 기능을 구현한 클래스만 잘 구현하면 된다.
maven 기준 아래와 같은 dependency를 추가하여 사용 가능하다. AspectJ도 있다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Component
@Aspect
public class PerfAspect {
@Around("execution(* com.example..*.EventService.*(..))")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMills();
Object reVal = pjp.proceed();
System.out.println(System.currentTimeMills() - begin);
return reVal;
}
}
위에서 실행 시간을 측정하는 공통 코드를 Aspect 모듈로 분리시켜서 작성해보았다. @Around 어노테이션에서 execution을 사용하여 Advice를 적용할 범위를 지정할 때 사용할 수 있다. 즉, 위의 코드를 해석해보면 com.example 패키지 밑에 있는 모든 클래스에 적용을하고, EventService 밑에 있는 모든 메소드에 적용하라는 뜻이 된다.
<어드바이스 동작시점>
실행시점 | 설명 |
---|---|
Before | 메소드 실행 전에 동작 |
After | 메소드 실행 후에 동작 |
After-returning | 메소드가 정상적으로 실행된 후에 동작 |
After-throwing | 예외가 발생한 후에 동작 |
Around | 메소드 호출 이전, 이후, 예외발생 등 모든 시점에서 동작 |
표현식 | 설명 |
---|---|
* | 모든 리턴타입 허용 |
void | 리턴타입이 void인 메소드 선택 |
!void | 리턴타입이 void가 아닌 메소드 선택 |
표현식 | 설명 |
---|---|
com.jjd.domain | 정확하게 com.jjd.domain 패키지만 선택 |
com.jjd.domain.. | com.jjd.domain 패키지로 시작하는 모든 패키지 선택 |
표현식 | 설명 |
---|---|
UserService | 정확하게 UserService 클래스만 선택 |
*Service | 이름이 Service로 끝나는 클래스만 선택 |
SampleObject+ | 클래스 이름 뒤에 '+'가 붙으면 해당 클래스로부터 파생된 모든 자식 클래스 선택 인터페이스 이름 뒤에 '+'가 붙으면 해당 인터페이스를 구현한 모든 클래스 선택 |
표현식 | 설명 |
---|---|
*(..) | 모든 메소드 선택 |
insert*(..) | 메소드명이 update로 시작하는 모든 메소드 선택 |
표현식 | 설명 |
---|---|
(..) | 모든 매개변수 |
(*) | 반드시 1개의 매개변수를 가지는 메소드만 선택 |
(com.jjd.domain.model.User) | 매개변수로 User를 가지는 메소드만 선택 꼭 풀 패키지 명이 있어야함 |
(!com.jjd.domain.model.User) | 매개변수로 User를 가지지 않는 메소드만 선택 |
(Integer, ..) | 한 개 이상의 매개변수를 가지고 첫번째 매개변수 타입이 Integer인 메소드만 선택 |
(Integer, *) | 반드시 2개 이상의 매개변수를 가지고 첫번째 매개변수 타입이 Integer인 메소드만 선택 |
어드바이스 메소드를 의미있게 구현하려면 클라이언트가 호출한 비즈니스 메소드의 정보가 필요하다. 예를 들면, 예외가 발생했는데 예외를 던진 메소드의 이름이 뭔지 등을 기록할 필요가 있을 수도 있다. 이럴 때 JoinPoint 인터페이스가 제공하는 유용한 API들이 있다.
메소드 | 설명 |
---|---|
Signature getSignature() | 클라이언트가 호출한 메소드의 시그니처(리턴타입, 이름, 매개변수) 정보가 저장된 Signature 객체 리턴 |
Object getTarget() | 클라이언트가 호출한 비즈니스 메소드를 포함하는 비즈니스 객체 리턴 |
Object[] getArgs() | 클라이언트가 메소드를 호출할 때 넘겨준 인자 목록을 Object 배열로 리턴 |
String getName() | 클라이언트가 호출한 메소드 이름 리턴 |
String toLongString() | 클라이언트가 호출한 메소드의 리턴타입, 이름, 매개변수(시그니처)를 패키지 경로까지 포함하여 리턴 |
String to ShortString() | 클라이언트가 호출한 메소드 시그니처를 축약한 문자열로 리턴 |
JoinPoint를 어드바이스 메소드 매개변수로 선언해야한다. 이 때 인자는 스프링 컨테이너가 넘겨준다. 예) method(JoinPoint jp)
이 때 Around 어드바이스와 약간 다른데, ProceedingJoinPoint 객체를 인자로 선언해야한다. proceed() 등이 추가로 구현되어있다. ProceedingJoinPoint는 JoinPoint를 상속받는다.