[Spring] 스프링 AOP란 무엇일까?

정민규·2023년 9월 17일
0

Spring AOP (Aspect Oriented Programming)

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를 이용한 개선

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에는 시간을 측정할 메서드가 들어오게 된다.
  • 시간을 측정할 메서드가 실행되기 직전의 시각을 startTime에 저장한다.
  • joinPoint.proceed()를 통해 시간을 측정할 메서드를 실행한다.
  • 실행을 마친 후의 시각을 endTime에 기록하고 두 시각 사이의 시간을 출력한다.

즉, 비즈니스 로직에 해당하는 메서드를 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. 공통 관심 사항을 적용할 대상을 유연하게 지정할 수 있다.

profile
조금이더라도 꾸준하게.

0개의 댓글