본문 바로가기
Java/[스프링 5 프로그래밍 입문]

[Spring] AOP 프로그래밍(1) - 프록시와 AOP

2021. 8. 10.
반응형

AOP는 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법이다. 핵심 기능에 공통 기능을 삽입하여 핵심 기능의 코드를 수정하지 않고도 공통 기능의 구현을 추가할 수 있다. 스프링에서는 프록시 객체를 생성해서 공통 기능을 삽입한다.

 

프록시와 AOP의 관계

프록시(proxy)핵심 기능의 실행을 다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 의미한다. 실제 핵심 기능을 실행하는 객체를 프록시의 대상 객체라고 부른다. 프록시는 핵심 기능을 구현하지 않는 것이 특징이다. 대신 여러 객체에 공통으로 적용할 수 있는 기능을 구현한다.

 

AOP(Aspect Oriented Programming)여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법으로 관점 지향 프로그래밍으로 주로 번역하곤 하는데, 여기서 Aspect가 구분되는 기능이나 요소를 의미하기 때문에 관점보다는 기능이나 관심으로 표현하는 것이 더 알맞다. 공통 기능 구현과 핵심 기능 구현을 분리하는 것이 AOP의 핵심이다.

 

예제를 통해 프록시 객체 알아보기

N번째 피보나치수를 구하기 위한 예제를 만들어보자. 피보나치수를 구하기 위한 인터페이스 Calculator는 아래와 같다.

public interface Calculator {
	public long fibo(long num);
}

Calculator 인터페이스를 구현한 클래스 ImpeCalculator는 for문을 이용해서 num번째 피보나치수를 구한다. 그리고 RecCalculator 클래스는 재귀 호출을 이용하여 피보나치수를 구한다.

public class ImpeCalculator implements Calculator{
	
	@Override
	public long fibo(long num) {
		long result = 0;
		long pre = 0, current = 1;
		
		for (long i = 1; i<= num; i++) {
			result = pre + current;
			pre = current;
			current = result;
		}
		
		return result;
	}
	
}
public class RecCalculator implements Calculator{
	
	@Override
	public long fibo(long num) {
		if (num == 0 || num == 1) {
			return 1;
		}
		else {
			return fibo(num-2) + fibo(num-1);
		}
	}
	
}

앞서 구현한 피보나치 구현 클래스의 실행 시간을 출력해보자. 가장 단순한 방법은 피보나치수를 구하는 메서드의 시작과 끝에서 현재 시간을 구하고 둘의 차이를 출력하는 것이다. 아래는 ImpeCalculator 클래스에서 실행 시간을 구하도록 수정한 코드이다.

public class ImpeCalculator implements Calculator{
	
	@Override
	public long fibo(long num) {
		long start = System.currentTimeMillis();
		long result = 0;
		long pre = 0, current = 1;
		
		for (long i = 1; i<= num; i++) {
			result = pre + current;
			pre = current;
			current = result;
		}
		long end = System.currentTimeMillis();
		System.out.printf("ImpeCalculator.fibo(%d) 실행 시간 = %d\n",
				num, (end-start));
		
		return result;
	}
	
}

ImpeCalculator 클래스에서는 단순하게 실행 시간을 구할 수 있었지만 재귀 호출을 이용해 값을 구하는 RecCalculator 클래스에서는 좀 더 복잡해진다. 만약 위 코드처럼 메서드의 시작과 끝에서 시간을 구하고 그 차이를 출력하는 방식을 사용한다면, 메시지가 여러 번 출력된다. fibo(2)를 실행하면 내부적으로 fibo(1), fibo(0)이 실행되기 때문에 실행 시간을 출력하는 메시지가 세 번 출력되는 문제가 발생한다.

 

따라서 RecCalculator를 고려하면 실행 시간을 출력하기 위해 기존 코드를 변경하기보다는 메서드 실행 전후에 값을 구하는 게 나을 수도 있다. 그러나 이렇게 한다면 실행 시간을 구하는 단위를 변경할 때 시간을 구하는 모든 코드를 변경해야 한다. 우리는 기존 코드를 변경하지 않고 코드의 중복도 피하기 위해 프록시 객체를 사용할 수 있다.

 

프록시 객체 적용하기

public class ExeTimeCalculator implements Calculator{
	
	private Calculator delegate;	// delegate: 대리인, 대표자
	
	// Calculator를 구현하는 ExeTimeCalculator 객체에 또다른 Calculator 객체를 전달
	public ExeTimeCalculator(Calculator delegate) {
		this.delegate = delegate;
	}
	
	// factorial 기능을 직접 구현하지 않고 대리인에게 fibo() 메서드의 실행을 위임한다.
	// 대신 부가적인 기능을 실행한다. (핵심 기능 대신 여러 객체에 공통적으로 적용할 수 있는 기능을 구현)
	@Override
	public long fibo(long num) {
		long start = System.nanoTime();
		// 대리인의 factorial 메서드를 실행
		long result = delegate.fibo(num);
		long end = System.nanoTime();
		
		System.out.printf("%s.factorial(%d) 실행시간 = %d\n", 
					delegate.getClass().getSimpleName(),
					num, (end-start));
		return result;
	}
    
}

위 코드는 Calculator 인터페이스를 구현하는 ExeTimeCalculator 클래스이다. 이 클래스는 생성자를 통해서 다른 Calculator 객체를 전달받아 delegate 필드에 할당하고 fibo() 메서드 내에서 delegate.fibo() 메서드를 실행하고 있다. 또한 delegate.fibo() 메서드 실행 앞뒤에서 현재 시간을 구해서 두 값의 차이를 통해 실행 시간을 구하고 있다. 이 클래스를 사용하여 ImpeCalculator와 RecCalculator 클래스의 실행시간을 출력하는 코드와 실행 결과는 아래와 같다.

public class MainProxy {
	
	// 핵심 기능의 실행은 대리인에게 위임하고 부가적인 기능을 제공하는 객체를 프록시라고 한다.
	// 실제 핵심 기능을 실행하는 객체를 프록시의 대상 객체라고 한다.

	public static void main(String[] args) {
		ExeTimeCalculator impeTimeCalculator = new ExeTimeCalculator(new ImpeCalculator());
		System.out.println(impeTimeCalculator.fibo(12));
		
		ExeTimeCalculator recTImeCalculator = new ExeTimeCalculator(new RecCalculator());
		System.out.println(recTImeCalculator.fibo(12));
		
	}
}
ImpeCalculator.factorial(12) 실행시간 = 1746
233
RecCalculator.factorial(12) 실행시간 = 29041
233

우리는 기존 코드(ImpeCalculator, RecCalculator)를 변경하지 않고 fibo() 메서드의 실행 시간을 구해서 출력할 수 있었다. 또한 실행 시간을 구하는 코드가 중복되지 않았다. 나노 단위 대신 밀리 단위로 실행 시간을 구하고 싶다면 ExeTimeCalculator 클래스만 변경하면 된다. 이는 ExeTimeCalculator 클래스가 fibo() 기능 자체를 구현하지 않고 다른 객체에 fibo() 실행을 위임했으며, 계산 기능 외에 다른 부가적인 기능(실행 시간 측정)을 실행하도록 구현되었기 때문이다.

 

이렇게 핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체가 프록시 객체이다. 여기서는 ExeTimeCalculator가 프록시이고 ImpeCalculator와 RecCalculator가 프록시의 대상 객체이다. 핵심 기능을 구현하지 않는 것이 프록시의 특징인데, 여기서는 ImpeCalculator와 RecCalculator가 피보나치 수를 구하는 핵심 기능을 구현하고 프록시에서는 해당 연산을 구현하지 않는다.

 

핵심 기능인 피보나치수 계산 기능을 구현하는 대신 여러 객체에 공통으로 적용할 수 있는 실행 시간 측정 기능을 구현한다. 이렇게 공통 기능 구현과 핵심 기능 구현을 분리하는 것이 AOP의 핵심이다.

 


 

지금까지 프록시가 무엇이며 어떤 방식으로 사용되는지, AOP와는 어떤 관계가 있는지 알아보았다. 다음 포스트에서는 실제로 스프링 프레임워크에서 AOP가 어떻게 적용되는지에 대해 알아볼 것이다.

 

 

 

 

참고 서적: <초보 웹 개발자를 위한 스프링 5 프로그래밍 입문>

 

초보 웹 개발자를 위한 스프링5 프로그래밍 입문

COUPANG

www.coupang.com

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.


Spring Framework 시리즈

  1. 스프링 프로젝트 시작하기 (Maven)
  2. 스프링 컨테이너(Container) 의미
  3. Dependency, DI, Assembler (의존, 의존 주입, 주입기) 개념 정리
  4. 스프링에서의 의존 주입(DI)의 의미와 사용법
  5. 스프링 애노테이션을 사용한 의존 주입(DI)
  6. 의존 자동 주입(1) - @Autowired 애노테이션
  7. 의존 자동 주입(2) - 빈 이름과 한정사
  8. 의존 자동 주입(3) - @Autowired의 필수 여부, 자동 주입과 명시적 의존 주입
  9. 컴포넌트 스캔 - @Component, @ComponentScan 사용하기
  10. 빈 객체의 라이프사이클과 범위 (Life Cycle & Scope of Bean)
728x90
반응형

댓글