평범한 연구소

[Spring] AOP (관점 지향 프로그래밍) 본문

Spring Framework

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

soyeonisgood 2022. 11. 12. 23:35

AOP (Aspect Oriented Programming)

 - 관점 지향 프로그래밍은 객체 지향 프로그래밍(Object Oriented Programming)을 더욱 OOP답게 사용할 수 있도록 도와주는 개념이다. 

 - 핵심 비지니스 로직공통 모듈분리하여 개발자가 좀 더 비지니스 로직에만 집중해서 처리할 수 있는 방법을 제공한다.

 - 공통 모듈을 적용하면 의존 관계의 복잡성과 코드 중복을 해소할 수 있다. 즉, 공통 모듈(로깅, 보안인증, 트랜잭션 등)을 별도로 작성한 후 코드 밖에서 비지니스 로직 사이에 삽입하는 것이 AOP 기능을 사용한 개발이다.

 

AOP 용어

타겟 (Target)

 - 핵심 기능을 담고 있는 모듈. 

 - 공통 모듈을 부여할 대상.

어드바이스 (Advice)

 - 타겟에 제공할 공통 기능(부가 기능)을 담고 있는 모듈

 - Advice 종류

  • Before Advice : target의 메소드 호출 전에 적용
  • After returning : target의 메소드 호출 이후에 적용
  • After throwing : target의 예외 발생 후 적용
  • After : target의 메소드 호출 후 예외 발생에 관계 없이 적용
  • Around : target의 메소드 호출 이전과 이후 모두 적용

조인 포인트 (Join Point)

 - Advice가 적용될 위치.

 - Target 객체가 구현한 인터페이스의 모든 메소드는 Join Point.

포인트 컷 (Pointcout)

 - Advice를 적용할 Target의 메소드를 선별하는 정규 표현식

 - execution 으로 시작하고 메소드의 Signature를 비교하는 방법을 주로 이용한다.

에스펙트 (Aspect)

 - AOP의 기본 모듈

 - Advice + Pointcut

 - 싱글톤 형태의 객체로 존재.

어드바이저 (Advisor)

 - Advice + Pointcut

 - Spring AOP 에서만 사용되는 특별한 용어. Aspect와 동일.

위빙 (Weaving)

 - Pointcut 에 의해 결정된 Target 의 Join Point에 Advice를 삽입하는 과정

 - AOP가 핵심기능(Target)의 코드에 영향을 주지 않으면서 필요한 공통 모듈(Advice)을 추가할 수 있도록 해주는 핵심적인 처리 과정.

 

Pointcut 표현식

 1) bean 명시자

  • 스프링 빈을 이용하여 JoinPoint 설정
  • bean(userBean) : 이름이 userBean인 빈의 모든 메소드
  • bean(user*) : 빈의 이름이 user으로 시작하는 빈의 모든 메소드

 2) execution 명시자

  • 엑시큐션은 Advice를 적용할 모든 메소드를 명시할 때 사용.
  • execution(수식어패턴   리턴타입   클래스이름패턴   이름패턴(파라미터패턴))  예외 패턴
    • 수식어 패턴: public, protected ... 생략 가능
    • 리턴 타입 패턴
    • 클래스 이름 패턴: 패키지를 포함한 클래스명 명시. 생략 가능.(패키지를 생략하면 모든 패키지, 클래스명을 생략하면 모든 클래스에 적용)
    • 이름 패턴 : 메소드 이름 명시
    • 파라미터 패턴
    • 예외 패턴 : 생략 가능
  • 패턴 문자
    • * : 모든 값
    • .. : 0개 이상
  • 예시
    • execution(public ** (..)) : public 메소드
    • execution(* set*(..)) : 이름이 set으로 시작하는 모든 메소드
    • execution(public * com.sp.app.service.UserService.*(..)) : UserService 인터페이스의 모든 메소드
    • execution(public * com.sp.app.service.*.*(*)) : service 패키지의 파라미터가 하나인 모든 메소드
    • execution(public * com.sp.app.service.*.*(..)) : service 패키지의 모든 메소드
    • execution(public * com.sp.app.service..*.*(..)) : service 패키지와 하위 패키지의 모든 메소드
    • execution(public * com.sp.app.service..*Service.*(..)) : service 패키지와 하위 패키지의 Service로 끝나는 모든 메소드

 

AOP 의 특징

 - Spring은 프록시(Proxy) 기반 AOP 지원.

  • Target 객체에 대한 프록시를 만들어 제공한다.
  • Target을 감싸는 프록시는 실행시간(Runtime)에 생성된다.
  • 프록시는 Advice를 타겟 객체에 적용하면서 생성되는 객체.

 - 프록시(Proxy)가 호출을 가로챈다.(intercept)

  • 프록시는 Target 객체에 대한 호출을 가로챈 다음 Advice의 부가 기능 로직을 수행하고, 그 후 Target의 핵심 기능 로직을 호출한다. (전 처리 Advice)
  • Target의 핵심 기능 로직 메소드를 호출한 후에 공통 기능(Advice)를 수행한다. (후 처리 Advice)

 - Spring AOP는 메소드 Join Point 만 지원한다.

  • Spring은 동적 프록시를 기반으로 AOP를 구현한다. 따라서 메소드 Join Point 만 지원한다.
  • 핵심 기능(Target)의 메소드가 호출되는 런타임 시점에서만 공통 기능(Advice)을 적용할 수 있다.
  • AspectJ 같은 고급 AOP 프레임워크를 사용하면 객체의 생성, 필드 값의 조회와 조작, static 메소드 호출 및 초기화 등의 다양한 작업에 공통 기능을 적용할 수 있다.

 

AOP 구현

1) XML 기반

 스프링 API 이용

  • 공통 기능을 제공하는 Advice 클래스를 아래 인터페이스를 구현하여 작성.
MethodBeforeAdvice 대상 객체의 메소드를 실행하기 전에 공통 기능을 실행
AfterReturningAdvice 대상 객체의 메소드 실행 이후에 공통 기능 실행
ThrowsAdvice 대상 객체의 메소드를 실행하는 도중 예외가 발생한 경우
MethodInterceptor 세 가지 Advice를 하나로 묶은 Advice

AfterReturningAdvice 활용 예제

package ex01.aop.after;

import java.lang.reflect.Method;

import org.springframework.aop.AfterReturningAdvice;

// AfterReturningAdvice
//	: 대상 객체의 메소드 실행 이후에 공통 기능을 실행할 때 사용하는 Advice
//	: 예외가 없이 실행된 경우에만 실행
public class AfterLogAdvice implements AfterReturningAdvice{

	@Override
	public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
		System.out.println(target + " 클래스의  " + method.getName() + " 메소드 호출 후 ...");
		System.out.println("리턴 값 : " + returnValue);
	}
}

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:util="http://www.springframework.org/schema/util"
	xmlns:task="http://www.springframework.org/schema/task"
	xsi:schemaLocation="
	    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
	
	<!-- Target 클래스 -->
	<bean id="testServiceTarget" class="ex01.aop.after.TestServiceImpl"/>
	
	<!-- Advice 클래스 -->
	<bean id="afterAdvice" class="ex01.aop.after.AfterLogAdvice"/>
	
	<!-- Advisor : Advice와 Pointcut을 연결하는 작업 -->
	<bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
		<property name="advice" ref="afterAdvice"/>
		<property name="patterns">
			<list>
				<value>.*save.*</value>
				<value>.*write.*</value>
			</list>
		</property>
	</bean>
	
	<!-- AOP 적용 -->
	<bean id="testService" class="org.springframework.aop.framework.ProxyFactoryBean">
		<property name="target" ref="testServiceTarget"/>
		<property name="interceptorNames">
			<list>
				<value>myAdvisor</value>
			</list>
		</property>
	</bean>
	
	
</beans>

 

MethodBeforeAdvice 활용 예제

package ex01.aop.before;

import java.lang.reflect.Method;

import org.springframework.aop.MethodBeforeAdvice;

// MethodBeforeAdvice
// 	: 대상 객체의 메소드를 실행하기 전에 공통 기능을 실행할 때 사용하는 Advice
public class BeforeLogAdvice implements MethodBeforeAdvice{

	@Override
	public void before(Method method, Object[] args, Object target) throws Throwable {
		String s;
		
		s = target.getClass() + " 클래스의   " + method.getName() + " 메소드 실행 전에 실행...";
		s += "\n매개변수 : ";
		if(args != null) {
			for(int i=0; i<args.length; i++) {
				s += args[i] + " ";
			}
		}
		
		System.out.println(s);
		
	}
	
}

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:util="http://www.springframework.org/schema/util"
	xmlns:task="http://www.springframework.org/schema/task"
	xsi:schemaLocation="
	    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
	
	<!-- Target 클래스 : AOP를 적용할 클래스 -->
	<bean id="testServiceTarget" class="ex01.aop.before.TestServiceImpl"/>
	
	<!-- Advice 클래스 : 공통사항을 가지고 있는 클래스 -->
	<bean id="beforeAdvice" class="ex01.aop.before.BeforeLogAdvice"/>
	
	<!-- Pointcut : 실제 Advice가 적용될 Joinpoint(Advice가 적용될 지점) -->
	<!-- 하나의 패턴 --> 
	<!--   
	<bean id="myPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
		<property name="pattern" value=".*write.*"/>
	</bean>
	--> 
	<!-- 다수의 패턴 -->
	<bean id="myPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
		<property name="patterns">
			<list>
				<value>.*save.*</value>
				<value>.*write.*</value>
			</list>
		</property>
	</bean>
	
	
	<!-- Advisor : Advice와 Pointcut을 연결하는 작업 -->
	<bean id="testAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
		<property name="advice" ref="beforeAdvice"/>
		<property name="pointcut" ref="myPointcut"/>
	</bean>
	
	<!-- AOP 적용 -->
	<bean id="testService" class="org.springframework.aop.framework.ProxyFactoryBean">
		<property name="target" ref="testServiceTarget"/>
		<property name="interceptorNames">
			<list>
				<value>testAdvisor</value>
			</list>
		</property>
	</bean>

</beans>

 

② POJO 기반 AOP 구현

  • 공통 기능을 제공하는 Advice 클래스를 POJO 기반으로 작성.
  • XML 설정 파일에 <aop:config>를 이용해서 Aspect를 설정.

package ex04.aop.pojo;

import org.aspectj.lang.JoinPoint;

public class MyLogAspect {
	// JoinPoint : 호출한 비지니스 메소드의 정보를 가지고 있는 인터페이스
	public String beforeLogging(JoinPoint joinPoint) {
		String methodName = joinPoint.getSignature().getName();
			// getSignature(): 메소드의 시그니처(리턴타입, 이름, 매개변수)
		System.out.println("before : " + methodName);
		
		return methodName;
	}
	
	public void returningLogging(JoinPoint joinPoint, Object ret) {
		String methodName = joinPoint.getSignature().getName();
		
		System.out.println("returning : " + methodName + ", 리턴값: " + ret);
	}
	
	public void throwingLogging(JoinPoint joinPoint, Throwable ex) {
		String methodName = joinPoint.getSignature().getName();
		
		System.out.println("throwing: " + methodName + ", throws: "
				+ ex.getClass().getName());
	}
	
	public void afterLogging(JoinPoint joinPoint) {
		String methodName = joinPoint.getSignature().getName();
		System.out.println("after: " + methodName);
	}
}

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:util="http://www.springframework.org/schema/util"
	xmlns:task="http://www.springframework.org/schema/task"
	xsi:schemaLocation="
	    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
	
	<!-- Target 클래스 -->
	<bean id="testService" class="ex04.aop.pojo.core.TestServiceImpl"></bean>

	<!-- Advice -->
	<bean id="myLogAspect" class="ex04.aop.pojo.MyLogAspect"></bean>

	<!-- aop 네임스페이스를 이용한 AOP 설정 -->        <!-- .*Service.*(..)): Service 인터페이스 안에 있는 모든 것에 적용 -->
	<aop:config>
		<aop:pointcut expression="execution(public * ex04.aop.pojo.core.*Service.*(..))" 
			id="publicMethod"/>
		
		<aop:aspect id="loggingAspect" ref="myLogAspect">
			<aop:before method="beforeLogging" pointcut-ref="publicMethod"/>
			<aop:after-returning method="returningLogging" 
				pointcut-ref="publicMethod" returning="ret"/>	
			<aop:after-throwing method="throwingLogging"
				pointcut-ref="publicMethod" throwing="ex"/>
			<aop:after method="afterLogging" pointcut-ref="publicMethod"/>
		</aop:aspect>
	</aop:config>
	
	
</beans>

 

 

2) @Aspect 어노테이션

  • @Aspect 어노테이션을 이용해서 부가 기능을 제공하는 Aspect 클래스를 작성.
  • Aspect 클래스는 Advice를 구현하는 메소드와 Pointcut 을 포함.
  • XML 설정 파일에 <aop:aspectj-autoproxy/> 설정하여 Aspect 관련 어노테이션을 활성화.
package ex05.aop.anno;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

/*
 - @Aspect
 	: 설정 파일에 Advice 및 Pointcout 등의 설정을 하지 않고 자동으로 Advice를 적용
 	

*/
@Aspect
public class MyLogAspect {
	
	@Pointcut(value = "execution(public * ex05.aop.anno.core.*Service.*(..))")
	private void publicMethod() {
		// Pointcut를 위한 가명 메소드
		// private void이어야 하고, 메소드의 몸체는 없어야한다.
	}
	
	// JoinPoint : 호출한 비지니스 메소드의 정보를 가지고 있는 인터페이스
	
	// 메소드 호출 전
	@Before("publicMethod()")
	public String beforeLogging(JoinPoint joinPoint) {
		String methodName = joinPoint.getSignature().getName();
			// getSignature(): 메소드의 시그니처(리턴타입, 이름, 매개변수)
		System.out.println("before : " + methodName);
		
		return methodName;
	}
	
	// 메소드가 예외없이 실행 된 후
	@AfterReturning(value = "publicMethod()", returning = "ret")
	public void returningLogging(JoinPoint joinPoint, Object ret) {
		String methodName = joinPoint.getSignature().getName();
		
		System.out.println("returning : " + methodName + ", 리턴값: " + ret);
	}
	
	// 예외 발생할 때
	@AfterThrowing(value = "publicMethod()", throwing = "ex")
	public void throwingLogging(JoinPoint joinPoint, Throwable ex) {
		String methodName = joinPoint.getSignature().getName();
		
		System.out.println("throwing: " + methodName + ", throws: "
				+ ex.getClass().getName());
	}
	
	// 예외 발생 유무와 상관 없이 메소드 실행 후
	@After("publicMethod()")
	public void afterLogging(JoinPoint joinPoint) {
		String methodName = joinPoint.getSignature().getName();
		System.out.println("after: " + methodName);
	}
}

applicationContext.xml 

  • <aop:aspectj-autoproxy/> : AOP 관련 어노테이션 활성화.
<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:p="http://www.springframework.org/schema/p"
	xmlns:util="http://www.springframework.org/schema/util"
	xmlns:task="http://www.springframework.org/schema/task"
	xsi:schemaLocation="
	    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
	
	<!-- AOP 관련 어노테이션 활성화 -->
	<aop:aspectj-autoproxy/>
	
	<!-- Target 클래스 -->
	<bean id="testService" class="ex05.aop.anno.core.TestServiceImpl"></bean>

	<!-- Advice -->
	<bean id="myLogAspect" class="ex05.aop.anno.MyLogAspect"></bean>

	
	
	
</beans>