잼's Tech

[SPRING] AOP 적용 본문

FRAME WORK/SPRING

[SPRING] AOP 적용

차잼 2021. 8. 18. 19:32

[SPRING] AOP 적용

AOP

본 문에서는 Spring 환경에서 AOP 적용을 XML 방식 Annotation 방식으로 해볼 것이다.

우선, Spring은 Proxy Factory를 통해 인터페이스 유무에 따라 JDK Proxy, CGLIB Proxy를 이용하여 Auto Proxy를 지원한다. ( https://jaem-tech.tistory.com/19 참고 )

AOP의 장점 중 하나는 의존 관계가 없다는 것인데 그렇다면 Client가 요청을 할 때 어떻게 알 수 있을까?
그것은 Proxy가 Class의 이름을 가지는 것에 답이 있다.
내부적으로 보면 밑의 방식과 같다.

Proxy Creation Process

1) X라는 클래스가 있다
2) xml이나 Annotation을 읽어 bean이라면 컨테이너에 bean 객체를 생성한다.
3) Proxy로 설정이 되어 있다면 Proxy Factory는 Proxy를 생성한다.
4) Proxy의 이름을 X 클래스가 가지고 있던 이름으로 내부적으로 설정한 후 기존 클래스의 이름을 변경한다.
5) X가 요청이 되었다면 Proxy가 불린다.


1. Dependency

  • 스프링 관련
    더보기

    나는 이해를 돕기 위한 출력폼을 만들기 위해 TEST를 넣었으나 실질적으로 사용하실 분들은 안 넣으셔도 된다.

    ※ JUnit의 경우 Build Path를 통해 Apply 해줘야한다.

    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-context</artifactId>
    	<version>5.2.6.RELEASE</version>
    </dependency>
    		
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-aop</artifactId>
    	<version>5.2.6.RELEASE</version>
    </dependency>
    
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-test</artifactId>
    	<version>5.2.6.RELEASE</version>
    </dependency>

     

 

  • AspectJ
    더보기

    Aspectjrt: AspectJ 런타임 프로그램이며 AspectJ의 기능이 포함

    Aspectjweaver: Aspect와 Taget을 위빙

    ※ 위빙: Taget을 참고하여 Proxy를 생성하는 과정

    <!-- Aspectj -->
    <dependency>
    	<groupId>org.aspectj</groupId>
    	<artifactId>aspectjrt</artifactId>
    	<version>1.9.5</version>
    </dependency>
    
    <!-- Aspectjweaver -->
    <dependency>
    	<groupId>org.aspectj</groupId>
    	<artifactId>aspectjweaver</artifactId>
    	<version>1.9.5</version>
    </dependency>

스프링의 경우 총 3종류의 파일이 필요하다.
xml 파일 = aop를 설정해주고 bean 객체를 생성할 xml
핵심 기능 클래스 = 타겟(Target) 클래스(부가 기능을 붙여줄 클래스)
부가 기능 클래스 = Aspect 클래스(부가 기능이 들어있는 모듈 클래스)

2. XML 방식

ex)
xml 파일 = xmlTestConfig.xml
핵심 기능 클래스 = Printer.java
부가 기능 클래스 = AopXmlAspect.java
테스트용 클래스 = AopXmlTest.java

● XML 설정

// xmlTestConfig.xml 

// Taget 클래스 bean 올리기
<bean id="printer" class="co.jaem.aop.entity.Printer" />

// 부가 기능 클래스 bean 올리기
<bean id="AopXmlAspect" class="co.jaem.aop.advice.AopXmlAspect" />

// AOP 설정임을 명시
<aop:config>
	
    // bean AopXmlAspect가 Aspect임을 명시
    <aop:aspect id="testAspect" ref="AopXmlAspect">
    
    	// 포인트컷 지정 (나는 Printer 클래스 안의 모든 메서드를 지정)
        <aop:pointcut id="testPointCut" expression="execution(public * co.jaem.aop.entity.Printer.*(..))" />
    
    	// Advice Before 설정
    	<aop:before method="testBefore" pointcut-ref="testPointCut" />
    
    	// Advice Around 설정 <aop:around method="testAround" pointcut-ref="testPointCut" />
    
    	// Advice After Returning 설정
    	<aop:after-returning method="testAfterReturning" pointcut-ref="testPointCut" returning="returnValue"/>
    
    	// Advice After Throwing 설정
    	<aop:after-throwing method="testAfterThrowing" pointcut-ref="testPointCut" throwing="returnException" />
    
    	// Advice After 설정
        <aop:after method="testAfter" pointcut-ref="testPointCut" />
        
    </aop:aspect>
</aop:config>

 

● 핵심 기능 클래스

// Printer.java

public class Printer {
	public void print(String s) { System.out.println(s); }
	public void printThrowException() { throw new IllegalArgumentException("이셉션 생성"); }
}

 

● 부가 기능 클래스

// AopXmlAspect.java

public void testBefore(JoinPoint joinPoint) {
	String sig = joinPoint.getSignature().toString();
	System.out.println("AOP BEFORE : " + sig );
}

public Object testAround(ProceedingJoinPoint joinPoint) throws Throwable {
	String sig = joinPoint.getSignature().toString();
	
	System.out.println("AOP AROUND START : " + sig );
	try {
		Object result = joinPoint.proceed();
		return result;
	} finally {
		System.out.println("AOP AROUND END : " + sig );
	}
}

public void testAfter(JoinPoint joinPoint) {
	String sig = joinPoint.getSignature().toString();
	System.out.println("AOP AFTER : " + sig );
}

public void testAfterReturning(JoinPoint joinPoint, Object returnValue) {
	String sig = joinPoint.getSignature().toString();
	System.out.println("AOP AFTER RETURNING : " + sig );
}

public void testAfterThrowing(JoinPoint joinPoint, IllegalArgumentException returnException) {
	String sig = joinPoint.getSignature().toString();
	System.out.println("AOP AFTER Throwing : " + sig );
	System.out.println("AfterThrowing에서 찍은 ERROR : " + returnException.toString());
}

!!! 중요한 특징

around는 ProceedingJoinPoint를 사용, 타 advice는 JoinPoint를 사용한다.
around는 procced()를 사용해야한다.
※ procced() 사용에 try 구문이 필요하다. Exception 테스트를 위해 catch는 제외하였다.
→ 이 이유는 아래에 각 advice들의 작동 순서와 함께 설명하겠다.

!!! 주의할 점

xml에서의 after-returning의 returning의 이름은 부가 기능 클래스의 해당 메서드의 Object 매개변수의 이름과 일치해야한다.

xml에서의 after-throwing의 throwing의 이름은 부가 기능 클래스의 해당 메서드의 Exception 매개변수의 이름과 일치해야한다.

● 테스트 클래스

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:co/jaem/aop/xml/xmlTestConfig.xml")
public class AopXmlTest {
	
	@Autowired
	ApplicationContext xmlTestCtx; 
	
	@Test
	public void run1() {
		System.out.println("======================================");
		Printer printer = xmlTestCtx.getBean(Printer.class);
		printer.print("Exception이 없는 테스트");
		System.out.println("======================================");
		printer.printThrowException();
	}
}


결과

AOP Order 1

 

<순 서>

(예외 없을 시)
BEFORE - AROUND - 핵심기능 - AROUND - AFTER RETURNING & AFTER

(예외 있을 시)
BEFORE - AROUND - 핵심기능 - AROUND - AFTER THROWING & AFTER

 

=> AFTER와 AFTER RETURNING 또는 AFTER THROWING를 동시 사용할 경우 XML의 aop 설정 위치에 따라 달라진다.

 

이해를 위해서...

<Before>

AOP BEFORE

① Proxy 호출
② Proxy는 받은 인자나 파라미터, 타입 등으로 인터페이스 JoinPoint의 구현 객체를 생성
③ 구현객체를 공통 기능이 파라미터로 받음
④ 공통 기능 실행
⑤ Proxy를 거쳐
⑥ 핵심 기능에게 전달하고 핵심 기능을 수행
⑦ 핵심 기능의 결과를 Proxy에게 전달
⑧ 응답
결국, Target의 제어권 Proxy에게 있다.

<Around>

AOP AROUND

① Proxy 호출
② Proxy는 받은 인자나 파라미터, 타입 등으로 인터페이스 ProceedingJoinPoint의 구현 객체를 생성
③ 구현객체를 공통 기능이 파라미터로 받음
④ 공통 기능 실행
proceed()를 실행하여 핵심기능 호출
⑥ 핵심 기능 수행 후 공통 기능에 결과 전달
⑦ 공통 기능 수행 후 Proxy를 거쳐
⑧ 응답
결국, Target의 제어권 공통 기능 클래스에게 있다.

<After, AfterReturning, After Throwing>

AOP AFTER

① Proxy 호출
② Proxy는 받은 인자나 파라미터, 타입 등으로 인터페이스 JoinPoint의 구현 객체를 생성
③ 핵심 기능 수행
④ 핵심 기능 결과값 Proxy가 받음
⑤ 구현 객체를 공통 기능이 파라미터로 받음
이 때
After-Returning은 returning을 통해 핵심 기능(타겟 메서드)의 return 값을 Object Type로 받을 수 있음
After-Throwing은 throwing을 통해 발생 Exception을 Exception Type로 받을 수 있음
⑥ 공통 기능 수행 후 Proxy를 거쳐
⑦ 응답

※ 참고로 공통 기능에서의 JoinPoint 객체는 AOP의 JoinPoint 개념과 다르다.


3. Annotation 방식

ex)
xml 파일 = anoTestConfig.xml
핵심 기능 클래스 = Printer.java
부가 기능 클래스 = AopAnoAspect.java
테스트용 클래스 = AopAnoTest.java

● XML 설정

// anoTestConfig.xml

<aop:aspectj-autoproxy />
<context:component-scan base-package = "co.jaem.aop.advice,co.jaem.aop.entity" />
  • Autoproxy: Bean으로 등록된 클래스 중에서 @Aspect가 선언된 클래스를 모두 애스펙트로 자동 등록 해주는 역할
  • component-scan: 어노테이션이 선언된 클래스들을 스캔하기 위한 설정

 

● 핵심 기능 클래스

// Printer.java

@Component
public class Printer {
	public void print(String s) { System.out.println(s); }
    public void printThrowException() { throw new IllegalArgumentException("이셉션 생성"); }
}
  • XML 방식에서 bean으로 형성해주었듯이 @component를 통해 component-scan이 bean으로 만들어 준다.

 

● 부가 기능 클래스

// AopAnoAspect.java

@Aspect
@Component
public class AopAnoAspect {
	
	@Before("execution(public * co.jaem.aop.entity.*.*(..))")
	public void testBefore(JoinPoint joinPoint) {
		String sig = joinPoint.getSignature().toString();
		System.out.println("AOP BEFORE : " + sig );
	}
	
	@Around("execution(public * co.jaem.aop.entity.*.*(..))")
	public Object testAround(ProceedingJoinPoint joinPoint) throws Throwable {
		String sig = joinPoint.getSignature().toString();
		
		System.out.println("AOP AROUND START : " + sig );
		try {
			Object result = joinPoint.proceed();
			return result;
		} finally {
			System.out.println("AOP AROUND END : " + sig );
		}
	}
    
	@AfterReturning(pointcut = "execution(public * co.jaem.aop.entity.*.*(..))", returning = "returnValue")
	public void testAfterReturning(JoinPoint joinPoint, Object returnValue) {
		String sig = joinPoint.getSignature().toString();
		System.out.println("AOP AFTER RETURNING : " + sig );
	}
	
	@AfterThrowing(pointcut = "execution(public * co.jaem.aop.entity.*.*(..))", throwing = "returnException")
	public void testAfterThrowing(JoinPoint joinPoint, IllegalArgumentException returnException) {
		String sig = joinPoint.getSignature().toString();
		System.out.println("AOP AFTER Throwing : " + sig );
		System.out.println("AfterThrowing에서 찍은 ERROR : " + returnException.toString());
	}
	
	@After("execution(public * co.jaem.aop.entity.*.*(..))")
	public void testAfter(JoinPoint joinPoint) {
		String sig = joinPoint.getSignature().toString();
		System.out.println("AOP AFTER : " + sig );
	}
}
  • Aspect 모듈임을 명시해야하며 마찬가지로 bean으로 만들어주기위해 @component가 필요
  • 각 메서드마다 사용할 Advice와 PointCut을 명시해줘야한다.
  • AfterReturning의 returning 해당 메서드의 Object 매개변수의 이름과 일치해야한다.
  • AfterThrowing의 throwing의 이름은 해당 메서드의 Exception 매개변수의 이름과 일치해야한다.

 

● 테스트 클래스

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:co/jaem/aop/xml/anoTestConfig.xml")
public class AopAnoTest {
	@Autowired
	ApplicationContext anoTestCtx; 
	
	@Test
	public void run1() {
		System.out.println("======================================");
		Printer printer = anoTestCtx.getBean(Printer.class);
		printer.print("Exception이 없는 테스트");
		System.out.println("======================================");
		printer.printThrowException();
	}
}

결과

AOP Order 2

=> IDE마다 출력문의 순서가 다르게 찍히나 XML 방식에서의 출력 순서가 옳다.

     내부적으로는 아래의 순서대로 작동한다.

<순 서>

(예외 없을 시)
BEFORE - AROUND - 핵심기능 - AROUND - AFTER RETURNING & AFTER

(예외 있을 시)
BEFORE - AROUND - 핵심기능 - AROUND - AFTER THROWING & AFTER

 


JoinPointProceedingJoinPoint의 함수들

● JoinPoint

- getArgs() : 전달 인자들 return
- getSignature() : 호출 메소드 정보 return
- getTarget() : 타겟 객체 return
- getKind() : JoinPoint의 타입 return
- getThis() : Proxy return

● ProceedingJoinPoint

※ JoinPoint를 상속받았기에 JoinPoint의 메소드를 전부 사용 가능
- proceed() : 타겟 메소드 수행 후 메소드 반환값 return

'FRAME WORK > SPRING' 카테고리의 다른 글

[SPRING] DI  (0) 2021.08.16
[SPRING] AOP 이론  (0) 2021.08.13
Comments