Server/Spring

Spring - AOP 프로그래밍 (관점 지향) [+ Proxy 패턴]

JaeHoney 2020. 9. 18. 16:17

기본 설정

<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjweaver</artifactId>
	<version>1.8.13</version>
</dependency>

스프링 프레임워크의 AOP 기능은 spring-aop 모듈이 제공하는데 이는 spring-context 모듈에 포함 되어 있고 spring-context 모듈은 프로젝트를 생성하면 기본으로 의존에 추가되기 때문에 srping-aop 모듈을 따로 추가하지 않아도 됩니다. pom.xml 파일에는 aspectjweaver 의존을 추가합니다 <dependencies> 태그 안에다가 넣으면 됩니다. 이 모듈은 스프링이 AOP를 구현할 때 사용하는 모듈이고 AOP를 설정하는데 필요한 애노테이션을 제공합니다.

 

프록시

public class RecCalculator implements Calculator {

	@Override
	public long factorial(long num) {
        if (num == 0)
            return 1;
        else
            return num * factorial(num - 1);
	}

}

위 클래스는 팩토리얼을 구하는 메서드가 정의되어 있습니다. 이 팩토리얼 메서드의 실행시간을 출력하려면 어떻게 해야 할까요?? 가장 쉬운 답은 메서드의 시작과 끝에서 시간을 구하고 차이를 출력하는 것입니다. 하지만 factorial() 메서드 안에서 구현한다면 시간차를 계속 더해야 하는 불편함은 물론, 출력도 한번만 해야하는데 재귀를 부를때마다 출력이 됩니다. 

 

그렇다고 메인 메서드에서 factorial() 메서드 앞뒤로 시간을 구하자니, 이 메서드가 한 번 실행되면 문제가 없지만 여러번 실행 되야 하는 구조에서, 수정사항이 생긴다면 메서드를 호출하는 곳을 일일이 하나하나 찾아가서 코드를 수정해야 합니다. 즉, 관리 차원에서 번거로운 코드가 됩니다.

public class ExeTimeCalculator implements Calculator {

    private Calculator delegate;

    public ExeTimeCalculator(Calculator delegate) {
        this.delegate = delegate;
    }

    @Override
    public long factorial(long num) {
        long start = System.nanoTime();
        long result = delegate.factorial(num);
        long end = System.nanoTime();
        System.out.printf("%s.factorial(%d) 실행 시간 = %d\n",
                delegate.getClass().getSimpleName(),
                num, (end - start));
        return result;
    }

}

그래서 위 같은 클래스를 둬서 생성자를 통해 factorial() 메서드를 가진 Calculator 객체를 전달 받아 필드에 할당하고 이를 실행하기 전, 후 시간차를 구해서 출력하게 하면, 메인에서 메서드를 아무리 많이 호출하더라도, 수정사항을 반영할 때 클래스 안에 내용 한번만 수정해 주면 문제없이 코드가 돌아가게 됩니다.

 

위 처럼 핵심 기능의 실행다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 프록시(proxy)라고 부릅니다. 그리고 실제 핵심 기능을 실행하는 객체대상 객체라고 부릅니다. 즉 위 예제 코드들을 본다면 ExeTimeCalculator가 프록시고 RecCalculator가 대상 객체가 됩니다.

 

AOP가 대두되기 전에는 프록시 패턴을 사용하여 공통 로직을 처리했습니다.

 

프록시의 특징

- 핵심 기능은 구현하지 않는다.

- 여러 객체에 공통으로 적용할 수 있는 기능을 구현한다. (위를 예제로 본다면 Caculator가 방식에 따라 여러개인 경우)

 

이렇게 공통 기능 구현핵심 기능 구현분리하는 것이 AOP 프로그래밍의 핵심 입니다.

 

AOP

 

AOP여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법입니다. AOP는 핵심 기능과 공통 기능의 구현을 분리해서 핵심 기능을 구현한 코드의 수정 없이 공통 기능을 적용할 수 있게 만들어 줍니다.

 

+우리말로는 '관점 지향 프로그래밍' 으로 직역하나 AOP의 Aspect는 여기서 기능이나 요소를 의미하기 때문에 관점보다는 기능이 더 알맞습니다.

 

AOP를 구현 하기 위한 방법은 세 가지가 있습니다.

- 컴파일 시점에 코드에 공통 기능을 삽입하는 방법

- 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법

- 런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 방법

 

스프링이 제공하는 AOP 방식은 프록시를 이용한 세 번째 방식입니다. 스프링 AOP프록시 객체를 자동으로 만들어줍니다. 따라서 위 ExeTimeCalculator 클래스 처럼 상위 타입의 인터페이스를 상속받은 프록시 클래스를 직접 구현할 필요가 없습니다.

 

스프링의 AOP

# 스프링에서는 아래의 개념들을 사용해서 AOP를 지원한다.

  • Advice - 공통 기능으로 동작할 기능을 의미한다.
  • Joinpoint - Advice를 적용 가능한 지점을 의미한다. 메서드 호출, 필드 값 변경 등이 이에 속하고 스프링은 프록시를 이용해서 AOP를 구현하기 때문에 메서드 호출에 대한 Joinpoint만 지원한다.
  • Pointcut - Joinpoint의 부분 집합으로 실제 Advice가 적용되는 Joinpoint를 나타낸다. 스프링에서는 정규 표현식이나 AspectJ의 문법을 이용하여 Pointcut을 정의할 수 있다.
  • Weaving - Advice(공통 기능)를 Pointcut(특정 시점)에 적용하는 과정을 weaving이라고 한다.
  • Aspect - 로직에서 분리해 여러 객체에서 공통으로 적용할 기능을 Aspect(관점)라고 한다. 트랜젝션이나 보안 등이 Aspect의 예이다. 일반적으로 Advice + ProintCut = Aspect 라고 한다.

# Advice의 동작 시점 5가지

  • Before Advice - 대상 객체의 메서드 호출 전
  • After Returning Advice - 대상 객체의 메서드가 익셉션 없이 실행된 이후
  • After Throwing Advice - 대상 객체의 메서드 실행 도중 익셉션이 발생 후
  • After Advice - 익셉션 상관없이 대상 객체의 메서드 실행 후
  • Around Advice - 대상 객체의 메서드 실행 전 과 후 또는 익셉션 발생 시점

스프링 AOP 구현

 

스프링 AOP를 이용해서 공통 기능을 구현하고 적용하려면 아래 절차를 따르면 된다.

  1. Aspect로 사용할 클래스에 @Aspect 애노테이션을 붙힌다.
  2. @Pointcut 애노테이션으로 공통 기능을 적용할 Pointcut을 정의한다.
  3. 공통 기능을 구현한 메서드에 @Around 애노테이션을 적용한다.
@Aspect
public class ExeTimeAspect {

	@Pointcut("execution(public * chap07..*(..))")
	private void publicTarget() {
	}

	@Around("publicTarget()")
	public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
		long start = System.nanoTime();
		try {
			Object result = joinPoint.proceed();
			return result;
		} finally {
			long finish = System.nanoTime();
			Signature sig = joinPoint.getSignature();
			System.out.printf("%s.%s(%s) 실행 시간 : %d ns\n",
					joinPoint.getTarget().getClass().getSimpleName(),
					sig.getName(), Arrays.toString(joinPoint.getArgs()),
					(finish - start));
		}
	}

}

 

@Aspect 애노테이션을 적용한 클래스는 AdvicePointcut함께 제공합니다. @Pointcut공통 기능을 적용할 대상을 설정합니다. @Around 애노테이션Around Advice를 설정합니다. 위 코드는 값이 "publicTarget()"인데 이는 publicTarget() 메서드에 정의한 Pointcut에 공통 기능을 적용 한다는 것을 의미합니다. publicTarget() 메서드는 chap07 패키지와 그 하위 패키지에 위치한 public 메서드를 Pointcut으로 설정하고 있으므로, 거기에 속한 public 메서드에 @Around가 붙은 measure() 메서드를 적용합니다.

 

measure() 메서드의 ProceedingJoinPoint 파라미터는 프록시 대상 객체의 메서드호출할 때 사용합니다. ProceedingJoinPoint 클래스proceed() 메서드를 사용해서 실제 대상 객체의 메서드를 호출하고 이전과 이후에 공통 기능을 위한 코드를 위치시키면 됩니다.

 

getSignature(), getTarget(), getArgs()는 각각 호출한 메서드의 클래스명, 객체명, 인자 목록 등을  구할 수 있습니다. (자바에서 메서드 이름과 파라미터를 합쳐서 메서드 시그니처라고 합니다. 메서드 파라미터의 개수나 타입이 다르거나 메서드 이름이 다르면 시그니처가 다르다고 표현합니다.)

 

+@Pointcut 애노테이션의 execution 명시자 표현식은 velog.io/@shson/%EC%8A%A4%ED%94%84%EB%A7%81Spring-AOP-AspectJ-Pointcut-%ED%91%9C%ED%98%84%EC%8B%9D-1-1-execution 여기서 잘 설명되어 있습니다.

 

@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
	@Bean
	public ExeTimeAspect exeTimeAspect() {
		return new ExeTimeAspect();
	}

	@Bean
	public Calculator calculator() {
		return new RecCalculator();
	}

}

 

@Aspect 애노테이션을 붙인 클래스를 공통 기능으로 적용하려면 @EnableAspectJAutoProxy 애노테이션설정 클래스에 붙혀야 합니다. 이 애노테이션을 추가하면 스프링이 @Aspect 애노테이션이 붙은 빈을 찾아서 해당 빈 객체의 @Pointcut 설정과 @Around 설정을 사용합니다. 

 

+스프링은 @EnableAspectJAutoProxy와 같이 이름이 Enable로 시작하는 다양한 애노테이션을 제공합니다. @Enable로 시작하는 애노테이션은 관련된 기능을 적용할 때 필요한 스프링 설정을 제공합니다.  

 

public class MainAspect {
	
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = 
				new AnnotationConfigApplicationContext(AppCtx.class);

		Calculator cal = ctx.getBean("calculator", Calculator.class);
		long fiveFact = cal.factorial(5);
		System.out.println("cal.factorial(5) = " + fiveFact);
		System.out.println(cal.getClass().getName());
		ctx.close();
	}

}

 

그리고 위의 메인 메서드를 실행하면 아래와 같은 결과가 출력됩니다.

 

RecCaculator.factorial([5]) 실행 시간 : 48791 ns

cal.factorial(5) = 120

com.sun.proxy.$Proxy17

 

첫 줄은 ExeTimeAspect 클래스의 measure()메서드 결과이고, 세 번째 줄은 메인 함수에서 cal.getClass().getName()을 출력한 결과인데 RecCaculator 타입이 아닌 Proxy 타입임을 알 수 있습니다. AOP를 적용 하지 않았다면 RecCaculator타입이었을 것입니다.

 

+ 반드시 RecCalculator 타입이 아닌 Calculator 타입 빈을 사용해야합니다. 스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체가 인터페이스를 상속한다면인터페이스를 이용해서 프록시를 생성합니다. 만약 그렇게 하고 싶지 않다면 설정 클래스에 @EnableAspectJAutoProxy(proxyTargetClass = true)로 지정하면 인터페이스가 아닌 자바 클래스를 상속받아 프록시를 생성합니다.

 

 

마무리

 

Spring AOP프록시 패턴을 이용해서 AOP를 구현합니다. 이는 실제 핵심 기능에는 공통 기능을 추가하지 않고, 따로 클래스를 작성한 뒤 어떤 공통 기능을 어떤 범위에서 사용할 것인지 설정해주면 스프링이 알아서 관리해줍니다. 따라서 소스코드가 간결해지고 나중에 유지보수를 하거나, 수정사항을 반영할 때 기존 코드를 갈아 엎지 않아도 새로운 기능을 추가하고 기존 기능을 제거하거나 수정하기 훨씬 용이할 것입니다.

 

 

참고 도서

초보 웹 개발자를 위한 스프링5 프로그래밍 입문
국내도서
저자 : 최범균
출판 : 가메출판사 2018.07.18
상세보기