새소식

인기 검색어

개인공부/SPRING

[Spring] 스프링 AOP (Spring AOP)란?

  • -

AOP (관점 지향 프로그래밍)

📌 1. AOP란?

문제를 바라보는 관점을 기준으로 프로그래밍을 하는 기법

AOP 구현 방법으로는, 여러 가지가 존재하는데, 일반적으로 공통 관심 사항을 구현한 코드를 핵심 로직을 구현한 코드에 삽입하는 것이다.

비슷한 동작을 하는 인터페이스와 필터가 있는데 이들은 에서 사용한다.

AOP 프로그래밍 기법 중 하나이며,
특정 패키지의 특정 클래스 전체, 특정 클래스의 특정 메서드 전체 등 적용 범위를 다양하게 설정하여 공통 기능으로 사용할 수 있다.

공통 기능 == 공통 관심 사항
핵심 로직 == 핵심 관심 사항

핵심 비즈니스 기능과 구분하기 위해 공통 기능을 공통 관심 사항(cross-cutting concern)이라고 표현하며, 핵심 로직을 핵심 관심 사항(core concern)이라고 표현한다.

📌 2. AOP 주요 용어

  • Joinpoint : Advice를 적용 가능한 지점을 의미한다. 메서드 호출, 필드 값 변경 등이 Joinpoint에 해당한다.
  • Pointcut : Joinpoint의 부분 집합으로서 실제로 Advice가 적용되는 Joinpoint를 나타낸다.
  • Advice : 언제 공통 기능을 적용할 지를 정의한다.
  • Weaving : Advice를 핵심 로직 코드에 적용하는 것을 weaving이라고 한다.
  • Aspect : 여러 객체에 공통으로 적용하는 기능을 Aspect라고 한다.

📌 3. 프록시 기반의 AOP 적용 과정

공통 기능 호출 == callModule()
핵심 기능 호출 == business()


📌 4. 스프링에서의 AOP

스프링은 자체적으로 프록시 기반의 AOP를 지원하므로,
스프링 AOP는 메서드 호출 Joinpoint만을 지원한다.

스프링은 완전한 AOP 기능을 제공하는 것이 아니라,
엔터프라이즈 어플리케이션을 구현하는 데 필요한 기능만을 지원한다.

스프링은 세 가지 방식으로 AOP를 구현할 수 있도록 한다.

  • XML 스키마 기반의 POJO 클래스를 이용한 AOP 구현
  • AspectJ에서 정의한 @Aspect 어노테이션 기반의 AOP 구현
  • 스프링 API를 이용한 AOP 구현

AspectJ 를 사용하기 위해서는 pom.xml에 dependency 설정이 필요하다.


📌 5. 구현 가능한 Advice 종류

공통 기능을 핵심 로직 '언제'(실행 전/후, 예외 발생 전/후) 적용할 것인가를 정의하고 있다.

  • ✨Around Advice는, 메서드 실행 전/후, 예외 발생 전/후 모두 원하는 기능을 삽입할 수 있어 가장 많이 사용되는 Advice 이다.

✍언제 공통 기능을 삽입할 것인가에 대해 구분할 수 있는 기준점이 필요한데, 파라미터로 받는 ProcedingJoinPoint 타입의 proceed() 메서드가 이러한 기준점 역할을 수행한다.
따라서, 반드시 첫번째 파라미터로 ProceedingJoinPoint 타입을 지정해야 한다.

Around Advice 를 제외한 Advice는 좀 더 자세하게 기능을 구현하기 위해서 사용하며, 이들의 구현 방법으로는 조금씩 다르다.

  • Before Advice를 구현한 클래스의 리턴타입은 일반적으로 "void" 이다.
    리턴 값을 갖더라도, 실제 Advice 적용 과정에서 사용되지 않기 때문이다.
  • After Advice를 구현한 메서드에서 대상 객체가 리턴한 값을 사용하고 싶다면, returning 속성을 사용하여 리턴 값을 전달 받을 파라미터의 이름을 명시한다.
  • After Throwing Advice 예외 객체를 사용하고 싶다면,
    throwing속성을 사용해 예외 객체를 전달받을 파라미터 이름을 명시한다.

✍리턴 값을 전달 받을 파라미터 이름을 명시하여 어떤 값이 리턴 되는지, 어떤 예외가 발생하는지에 따라 다른 Aspect를 적용하기 위해 파라미터 이름을 명시한다.

📌 6. XML 기반의 POJO 클래스를 이용한 AOP 구현

aop 네임스페이스의 XML 스키마를 지정했다면, 다음과 같이 <aop:config> 태그를 사용하여 AOP 관련 정보를 설정할 수 있다.

<bean id="profiler" class="com.spring.exercise.aop.Profiler"/>

<aop:config>
  <aop:aspect id="traceAspect" ref="profiler">
    <aop:pointcut id="publicMethod"
       expression="execution(public * com.spring.exercise..*(..))"/>
    <aop:around pointcut-ref="publicMethod" method="trace"/>
  </aop:aspect>
</aop:config>
 
<bean id="memberService" 
	class="com.spring.exercise.member.MemberServiceImpl" />

<aop:aspect> 태그의 ref 속성은 Aspect의 공통 기능을 제공할 빈을 설정할 때 사용된다. 위 코드는 Aspect 기능을 제공할 빈으로 "profiler" 빈을 지정하였다.

  • 이름이 "traceAspect"인 Aspect는 MemberServiceImpl 클래스의 public 메서드에 대해 Around Advice로 적용된다.
  • "traceAspect"의 구현으로 사용되는 profiler 빈의 "trace"메서드를 통해 공통 기능을 제공한다.

  • <aop:config> : AOP 설정 정보임을 나타낸다.
  • <aop:aspect> : Aspect를 설정한다.
  • <aop:pointcut> : Pointcut을 설정한다.
  • <aop:around> : Around Advice를 설정한다. 이 외에도 다양한 Advice를 설정할 수 있다.

📌 7. @Aspect 어노테이션 기반 AOP

  • @Aspect 어노테이션을 이용해서 Aspect 클래스를 구현한다. 이 때 Aspect 클래스는 Advice를 구현한 메서드와 Pointcut을 포함한다.
  • XML 설정에서 <aop:aspectj-autoproxy/>를 설정한다.

<aop:aspectj-autoproxy/> 태그를 등록해주지 않으면, @Aspect 관련 어노테이션은 사용할 수 없다.

7.1 @Pointcut

메서드에 @Pointcut 을 적용할 수도 있는데, 이렇게 적용된 메서드를 @Around 속성에서 호출하여 간단하게 설정할 수 있고, 여러 Advice에서 사용할 수도 있다.

@Pointcut 어노테이션이 적용된 메서드를 참조하려면, 아래와 같이 입력하면 된다.

  • 같은 클래스에 위치한 @Pointcut 메서드는 '메서드 이름'만 입력
  • 같은 패키지에 위치한 @Pointcut 메서드는 '클래스단순이름.메서드이름'을 입력
  • 다른 패키지에 위치한 @Pointcut 메서드는 '완전한클래스이름.메서드이름'을 입력

7.2 @Joinpoint

Joinpoint 는 Pointcut을 포괄하는 개념
따라서, Pointcut은 세세하게 지정하기 위해서 사용하는 것이다.

Around Advice는 핵심 기능 전/후가 지정되지 않았기 때문에, JoinPoint를 기준으로 전/후를 구분하므로, JoinPoint 타입을 반드시 첫 번째 매개 변수로 받아 사용해야 한다.
Around Advice를 제외한 나머지 Advice 타입을 구현한 메서드는 JoinPoint 객체를 선택적으로 사용할 수 있다.

ProceedingJoinPoint의 proceed() 메서드를 호출하면 프록시 대상 객체의 실제 메서드를 호출하게 된다. 실제 메서드를 호출하기 때문에, 실제 메서드 전/후를 구분할 수 있게 되는 것이다.

✍스프링 AOP는 인터페이스를 이용해서 프록시 객체를 생성되어, 인터페이스 구현 클래스를 instanceof 연산자로 검사하면 false를 리턴한다. 인터페이스 기준이 아닌 클래스 기준으로 프록시가 생성되게 하려면,
<aop:config proxy-class="true"> 태그를 등록한다. 등록한 뒤 구현 클래스를 instanceof 연산자로 검사하면 올바르게 true를 리턴한다.


📌 8. execution 명시자

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

스프링 AOP는 public 메서드에만 적용 가능하므로 '수식어' 부분은 생략이 가능하다.

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

📌 9. Advice 적용 순서

Advice 적용 순서를 명시적으로 지정하려면, @Order 어노테이션을 적용하거나, Ordered 인터페이스를 구현한다.

@Aspect
public class Exercise implements Ordered {
	@Override
    public int getOrder() {
    	return 2;
}

@Order 어노테이션을 사용할 경우 값으로 적용 순서를 지정한다.

@Order(3)

실행 전(Before)은 순차대로 적용되지만, 실행 후(After)는 역순으로 적용된다.

 

 


Proxy 객체

음 구조의 클래스 파일이 있다.

 

[그림 1] 클래스 구조

 

Calculator.java

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

ImpeCalculator.java (핵심 기능 구현 객체)

package chap07;

public class ImpeCalculator implements Calculator {

	@Override
	public long factorial(long num) {
		long result = 1;
		for (long i = 1; i <= num; i++) {
			result *= i;
		}
		return result;
	}

}

RecCalculator.java (핵심 기능 구현 객체)

package chap07;

public class RecCalculator implements Calculator {

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

}

ExeTimeCalculator.java (프록시 객체)

package chap07;

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;
    }

    public static void main(String[] args) {
        ExeTimeCalculator ex = new ExeTimeCalculator(new ImpeCalculator());
        ex.factorial(10);
        ExeTimeCalculator ex01 = new ExeTimeCalculator(new RecCalculator());
        ex01.factorial(10);
    }

}

핵심 기능 구현 객체들의 실행시간을 파악하기 위하여 프록시 객체를 이용했다. Proxy 객체는 핵심 기능 외에 공통적인 기능을 담은 객체를 말한다. 이렇게 공통 기능 구현과 핵심 기능 구현을 분리하는 것이 AOP의 핵심이다.

 

 

 

AOP (Aspect Oriented Programming)


AOP(Aspect Oriented Programming): 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법

 

위의 코드에서 팩토리얼 계산 기능(핵심 기능)의 코드의 수정 없이 계산 시간 측정 기능(공통)을 프록시(ExeTimeCalculator)를 사용해서 구현할 수 있었다. Spring MVC도 프록시를 이용해서 AOP를 구현하고 있다.

 

AOP의 기본 개념은 핵심 기능에 공통 기능을 삽입하는 것. 핵심 기능의 코드를 수정하지 않으면서 공통 기능의 구현을 추가하는 것이 AOP이다. Spring MVC의 AOP는 프록시 객체를 자동으로 만들어준다. 무슨 말이냐면, 상위 타입 인터페이스를 상속받거나 하는 등의 잡다한 일은 스프링이 알아서 해준다.

 

필수적으로 알아야 할AOP의 주요 용어는 다음과 같다.

    1. Aspect - AOP에서 공통 기능. 여러 객체에 공통으로 적용되는 기능.

    2. Advice - 공통 기능을 언제 핵심 기능에 삽입할지를 정의

    3. Joinpoint - 위의 Advide의 '언제'에 해당하는 부분의 리스트를 나타낸다. ex) 메서드 호출 전, 필드 값 변경 후 등

    4. Pointcut - Joinpoint의 부분 집합으로서 실제 Advice가 적용되는 Joinpoint를 나타낸다.

    5. Weaving - Advice를 핵심 로직 코드에 적용하는 것

 

 

개발자는 공통 기능을 제공하는 Aspect 구현 클래스를 만들고 자바 설정을 이용해서 Aspect를 어디에 적용할지 설정한다.

위의 프록시 객체를 스프링 AOP를 이용해서 새로 작성한 코드는 다음과 같다.

package aspect2;

import java.util.Arrays;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect // Aspect 구현 클래스임을 명시한다.
public class ExeTimeAspect {

    // 공통 기능을 적용할 대상을 설정한다.
    //chap07패키지와 그 하위 패키지에 위한 타입의 public 메서드를 Pointcut으로 설정한다
    @Pointcut("execution(public * chap07..*(..))")
    private void publicTarget() {
    }

    // Around Advice를 설정한다.
    // publicTarget() 메서드에 정의한 Pointcut에 공통 기능을 적용한다.
    //public 메서드를 Pointcut으로 설정하고 있으므로  chap07 패키지와 그 하위 패키지에 속한 빈 객체의 public 메서드에(factorial())에 @Around가 붙은 measure() 메서드를 적용한다
    @Around("publicTarget()")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        //ProceedingJoinPoint 파라미터는  프록시 대상 객체의 메서드를(factorial()) 호출할 떄 사용한다.
        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(),//실제객체(RecCalculator)를 구한다
                    sig.getName(),
                    Arrays.toString(joinPoint.getArgs()),//실제객체의 메서드를 호출할떄 넘겨받은 파라미터(5)의 목록을 구한다
                    (finish - start));
        }
    }
}

measure() 메서드의 ProceedingJoinPoint 타입 파라미터는 프록시 대상 객체의 메서드를 호출할 때 사용한다. 22행의 ProceedingJoinPoint#proceed() 메서드를 실행하면 실제 대상 객체의 메서드가 호출된다.

 

스프링 설정 클래스에 등록하는 코드는 다음과 같다.

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import aspect.ExeTimeAspect;
import chap07.Calculator;
import chap07.RecCalculator;

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

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

}
package main;

import chap07.RecCalculator;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import chap07.Calculator;
import config.AppCtx;

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

		Calculator cal = ctx.getBean("rec", Calculator.class);
		//Calculator cal = ctx.getBean("rec", RecCalculator.class);
		/*위의 구문이 에러가 나는 이유는 getBean으로 가쟈오는 객체가 RecCalculator가 아닌 프록시 개체이기떄문이다
		따라서 프록시객체는 Calculator인터페이스 상속받기에 빈 객체를 찾을떄는 Calculator로 찾으라고 해야한다*/
		long fiveFact = cal.factorial(5);
		System.out.println("cal.factorial(5) = " + fiveFact);
		System.out.println(cal.getClass().getName());
		ctx.close();
	}

}

AOP 사용시 실행결과
AOP 미사용 시

실행결과를 보면 프록시 객체 떄문에 클래스 이름이 chap07.RecCalculator가 아닌 프록시객체로 콘솔창에 찍힌다.

즉, 스프링은 AOP를 위한 프록시 객체를 생성할 떄 실제 생성할 빈 객체(RecCalculator)가 인터페이스(Calculator)를 상속하면 프록시 객체도 마찬가지로 인터페이스를 상속하여 생성한다.

 

📌TMI

더보기

스프링 AOP는 public 메서드에만 적용할 수 있다.

순서 값이 1인 Advice가 순서 값인 2인 Advice보다 우선 순위가 높다. 메서드 호출 전, 메서드 호출에 대해서느 우선 순위가 높은 Advice가 먼저 실행되고, 메서드 호출 후에 대해서는 우선 순위가 낮은 Advice가 먼저 실행

참고자료 : http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788980782970 

 

스프링5 프로그래밍 입문 | 최범균 - 교보문고

스프링5 프로그래밍 입문 | 스프링 프레임워크의 버전업으로 개선된 내용을 담았습니다.필자의 한마디 : 처음 스프링을 배우고자 하는 독자들이 입문할 때 필요한 것은 스프링의 방대한 내용이

product.kyobobook.co.kr

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.