AOP(Aspect Oriented Programming : 관점 지향 프로그래밍)은 프로그램의 공통적인 관심사(공통적인 기능)을 핵심적인 관심사(핵심 비즈니스 로직)로부터 따로 분리해냄으로써 프로그램의 모듈성을 높이고자 하는 패러다임이다.
아무래도 정의만 보았을 때에는 감이 잘 잡히지 않으니, 간단한 코드 예제를 보면서 이해해 보도록 하자.
public class McDonaldEmployee {
public void receiveOrder(){
...
}
public void bakeBun(){
...
}
public void getVegetable(){
...
}
public void grillPatty(){
...
}
public void assemble(){
...
}
}
여기 맥도날드 직원을 구현해 놓은 클래스가 있다.
이 맥도날드 직원 클래스의 각 메서드는 다음과 같이 고유의 관심사, 즉 비즈니스 로직으로써 달성해야 하는 특수한 목적이 존재한다.
receiveOrder()
: 주문을 접수하는 것.bakeBun()
: 빵을 굽는 것.getVegetable()
: 적절한 야채를 가져오는 것.grillPatty()
: 패티를 굽는것.assemble()
: 준비한 재료들을 조립하여 햄버거 완성품을 만들어 내는 것.그런데 여기서 맥도날드 직원들의 능률 향상을 위해 가장 수행시간이 오래 걸리는 작업을 알아내기 위해서 각 메서드 실행시 실행 시작 시각과, 실행 완료 시각을 구하고, 총 실행 시간을 출력하는 기능을 추가해야 한다고 해 보자.
Spring AOP를 사용하지 않으면 아마 위 코드는 다음과 같이 바뀔 것이다.
public class McDonaldEmployee {
public void receiveOrder(){
long startTime = System.currentTimeMillis();
(receiveOrder 핵심 로직)
long endTime = System.currentTimeMillis();
System.out.println("execution Time : " + (endTime - startTime) / 1000 + "second.");
}
public void bakeBun(){
long startTime = System.currentTimeMillis();
(bakeBun 핵심 로직)
long endTime = System.currentTimeMillis();
System.out.println("execution Time : " + (endTime - startTime) / 1000 + "second.");
}
public void getVegetable(){
long startTime = System.currentTimeMillis();
(getVegetable 핵심 로직)
long endTime = System.currentTimeMillis();
System.out.println("execution Time : " + (endTime - startTime) / 1000 + "second.");
}
public void grillPatty(){
long startTime = System.currentTimeMillis();
(grillPatty 핵심 로직)
long endTime = System.currentTimeMillis();
System.out.println("execution Time : " + (endTime - startTime) / 1000 + "second.");
}
public void assemble(){
long startTime = System.currentTimeMillis();
(assemble 핵심 로직)
long endTime = System.currentTimeMillis();
System.out.println("execution Time : " + (endTime - startTime) / 1000 + "second.");
}
}
위 코드에는 2가지 문제점이 있다.
1. 시간측정 코드가 중복되고 있다
완전히 동일한 코드가 McDonaldEmployee 클래스의 각 메서드마다 따로따로 삽입되어있다.
만약에 McDonaldEmployee 클래스의 메서드가 훨씬 더 많거나, 시간측정 로직을 다른 클래스에도 적용시켜야 하는 경우, 메서드마다 시간측정 로직을 복사 + 붙여넣기 해야 할 것이며, 이는 어마어마한 양의 중복코드를 생산해 내게 될 것이다.
단순히 복사 + 붙여넣기만 해야 하는 것이라면 그나마 양반이다. 하지만, 만약 요구사항이 변경되어 System.out.println()
이 아니라 따로 로그 파일을 남기도록 구현해야 한다면, 변경된 코드를 또 모든 메서드에다가 업데이트 해 주어야 한다.
만약 1개의 메서드라도 빼먹게 되면, 결국 누락되는 로그 데이터가 발생할 것이고, 이것은 차후에 중대한 문제로 발전하게 될 가능성이 있다.
2. 메서드에 본인의 핵심 로직(관심사)외의 다른 부가적인 기능을 위한 코드가 끼어있다.
receiveOrder()
메서드에는 맥도날드 직원이 주문을 받기 위한 코드만 들어 있는 것이 바람직하다.정리하면, McDonaldEmployee라는 클래스에 "시간측정"이라는 메서드들 간의 공통 관심사가 생겼고, 해당 공통 관심사 로직(시간 측정)을 기존 비즈니스 로직(McDonaldEmployee의 각 메서드)에 끼워넣으면서 코드의 가독성과 유지보수성이 떨어지게 된 것이다.
Spring AOP는 위 상황에서 "시간측정"이라는 공통 관심사에 대한 로직을 따로 분리하는 기능을 제공해 준다.
시간 측정과 관련된 로직을 따로 분리해낼 클래스 하나를 새로 생성한다.
// com.example.TimerAop
@Component
@Aspect
public class TimerAop {
// McDonaldEmployee 클래스의 모든 메서드를 대상으로 실행 시간을 출력함
@Around("execution(* com.example.McDonaldEmployee.*(..))")
public Object printExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
// @Around 어노테이션을 사용하는 경우, joinPoint 메서드의 리턴값이 무엇이 될지 알 수 없으므로 Object를 리턴값으로 주어야 한다.
long startTime = System.currentTimeMillis();
try{
Object ret = joinPoint.proceed();
return ret;
}finally{
long endTime = System.currentTimeMillis();
System.out.println(joinPoint.toString() + " execution Time : " + (endTime - startTime) / 1000 + " second.");
}
}
}
위 코드를 간단히 설명하자면 다음과 같다.
TimerAop
라는 클래스는 앞 예제에서 보았던 코드에서 "시간측정"이라는 공통 관심사를 따로 분리해 놓은 클래스이다.printExecutionTime
메서드는 ProceedingJoinPoint joinPoint
를 인자로 받는다. joinPoint
에는 시간을 측정할 메서드가 들어오게 된다.joinPoint.proceed()
를 통해 시간을 측정할 메서드를 실행한다.즉, 비즈니스 로직에 해당하는 메서드를 jointPoint
로 추상화 하여 분리된 하나의 공통 로직에서 다양한 핵심 비즈니스 로직을 수행할 수 있게 만든 것이다.
// com.example.McDonaldEmployee
@Component // Spring Aop는 오직 스프링에서 관리하는 빈들에 대해서만 작동한다.
public class McDonaldEmployee {
public void receiveOrder(){
...
}
public void bakeBun(){
...
}
public void getVegetable(){
...
}
public void grillPatty(){
...
}
public void assemble(){
...
}
}
McDonaldEmployee
클래스의 각 메서드는 이제 "시간 측정"에 대한 로직을 포함할 필요 없이, 자신이 수행해야 할 핵심 로직만을 담을 수 있게 되었다.
이후, 실행시간 데이터를 단순 콘솔 출력이 아니라 별도의 로그 파일로 남겨야 하는 경우, TimerAop
클래스의 코드 1곳만 수정하는 것으로 다른 모든 메서드의 실행 시간을 로그 파일로 남길 수 있게 된다.
마지막으로, Spring AOP을 사용함으로써 얻을 수 있는 장점에 대해 정리해보겠다.
1. 공통 관심 사항(앞서 본 시간 측정)과 핵심 관심 사항(핵심 비즈니스 로직)을 분리할 수 있다.
2. 여러 클래스, 메서드에 들어가는 공통 관심 사항에 대한 코드를 한 곳에서 관리할 수 있다.
3. 핵심 관심 사항에 대한 메서드, 클래스는 온전히 핵심 관심 사항에 대한 로직에만 집중할 수 있게 된다.
4. 공통 관심 사항을 적용할 대상을 유연하게 지정할 수 있다.