본문 바로가기
개발 공부 중!/Spring

[스프링5 입문] Chap 07. AOP 프로그래밍

by 김evelyn 2024. 1. 8.

📚 프록시란?

  • 핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 부르는 말

 

프록시의 특징

  • 핵심 기능은 구현하지 않는 점
  • 아래와 같은 코드의 경우, ImpeCalculator, RecCalculator는 ‘팩토리얼 연산’이라는 핵심 기능을 구현한다. (각 클래스에서)
  • 반면, ExeTimeCalculator 클래스는 팩토리얼 연산 자체를 구현하고 있지 않다.
public class MainProxy {
    public static void main(String[]args) {
        // 기존 코드의 변경 없이 실행 시간을 출력할 수 있다.
        // factorial() 기능 자체를 구현하기 보다는 다른 객체에 factorial()의 실행을 위임한다.
        // 계산 기능 외에 다른 부가적인 기능을 실행한다. (여기서 부가적인 기능 : 실행시간측정)
        // 이와 같이 핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 프록시(proxy)라 부른다.
        ExeTimeCalculator ttCal1 = new ExeTimeCalculator(new ImpeCalculator());
        System.out.println(ttCal1.factorial(20));

        ExeTimeCalculator ttCal2 = new ExeTimeCalculator(new RecCalculator());
        System.out.println(ttCal2.factorial(20));
    }
}

 

=> 이와 같이 공통 기능 구현과 핵심 기능 구현을 분리하는 것이 AOP의 핵심이다.


📚 AOP (Aspect Oriented Programming)

  • 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법
  • 기본 개념: 핵심 기능에 공통 기능을 삽입하는 것 (즉, 핵심 기능의 코드를 수정하지 않으면서 공통 기능의 구현을 추가하는 것)

 

핵심 기능에 공통 기능을 삽입하는 3가지 방법

  1. 컴파일 시점에 코드에 공통 기능을 삽입하는 방법
  2. 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법
  3. 런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 방법

 

📚 AOP 주요 용어

Advice 언제 공통 관심 기능을 핵심 로직에 적용할 지를 정의
Joinpoint Advice 적용 가능한 지점을 의미 (메서드 호출, 필드값 변경 등)
Pointcut 실제 Advice가 적용되는 Joinpoint를 의미
Weaving Advice를 핵심 로직 코드에 적용하는 것을 의미
Aspect 여러 객체에 공통으로 적용되는 기능을 의미 (e.g. 트랜젝션, 보안)

 

스프링은 프록시를 이용해서 메서드 호출 시점에 Aspect를 적용하기 때문에 구현 가능한 Advice 종류가 몇 있는데 그 중 가장 널리 사용되는 것은 'Around Advice'이다. 

 

Around Advice란?

  • 대상 객체의 메서드를 실행하는 전/후, 익셉션 발생 시점 등 다양한 시점에 원하는 기능을 삽입할 수 있다. 때문에 널리 사용된다.

📚 스프링 AOP를 이용해서 공통 기능을 구현하고 적용하는 방법

  • Aspect로 사용할 클래스에 @Aspect 애노테이션을 붙인다.
  • @Pointcut 애노테이션으로 공통 기능을 적용할 Pointcut을 정의한다.
  • 공통 기능을 구현한 메서드에 @Around 애노테이션을 적용한다.
  • (@Aspect 애노테이션을 붙인 클래스를 공통 기능으로 적용하려면 @EnableAspectJAutoProxy 애노테이션을 설정 클래스에 붙여야함)

 

스프링은 AOP를 위한 프록시 객체를 생성할 때
실제 생성할 빈 객체가 인터페이스를 상속하면 인터페이스를 이용해 프록시를 생성한다.
1. Calculator cal = ctx.getBean("calculator", Calculator.class);

2. RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);

 

위 인용문에 따라, 2번의 경우, 프록시는 RecCalculator가 아닌, RecCalculator 클래스가 상속받은 Calculator 인터페이스를 상속 받게 된다. 즉, getBean 메서드에 사용한 타입은 RecCalculator인데 반해, 실제 타입은 Proxy라는 메세지가 뜨며 익셉션이 발생하게 된다.

 

 

이때, 인터페이스가 아닌 클래스를 이용해 프록시를 생성하고 싶다면 다음과 같이 설정하면 된다.

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppCtx {

 

위처럼 'proxyTargetclass = true'로 지정해주고 다시 실행해보면 프록시가 RecCalculator를 상속받아 성공적으로 실행됨을 확인가능!

 


📚 execution 명시자 표현식

- execution 명시자는 Advice를 적용할 메서드를 지정할 때 사용한다. 

execution(수식어패턴? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴))

 

수식어패턴 생략 가능, 주로 public이 온다.
리턴타입패턴 리턴 타입을 명시한다.
클래스이름패턴 및 메서드이름패턴 클래스 이름 및 메서드 이름을 패턴으로 명시한다.
파라미터패턴 매칭될 파라미터에 대해 명시한다.

 

각 패턴은 '*'을 이용하여 모든 값을 표현할 수 있고, '..'(점 두 개)를 이용하여 0개 이상이라는 의미를 표현할 수도 있다. 


여러 Aspect에서 공통으로 사용하는 Pointcut이 있다면 별도 클래스에 Pointcut을 정의하고, 각 Aspect 클래스에서 해당 Pointcut을 사용하도록 구현하면 관리가 편해진다. 

 

아래 코드는 각 Aspect 클래스인 CacheAspect, ExeTimeAspect에서 공통으로 사용하는 Pointcut을 별도 클래스(CommonPointcut)에 정의한 것입니다. 

public class CommonPointcut {
@Pointcut("execution(public * chap07..*(..))")
    public void commonTarget() {
    }
}

@Aspect
public class CacheAspect {

...

//    @Pointcut("execution(public * chap07..*(long))")
//    public void cacheTarget() {
//    }

@Around("CommonPointcut.commonTarget()"
...
}

@Aspect
public class ExeTimeAspect {

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

@Around("CommonPointcut.commonTarget()")
...
}