Spring 05 : Proxy, AOP 프로그래밍

LeeWonjin·2022년 8월 7일

2022 백엔드스터디

목록 보기
15/20

환경
윈도우즈 10 / 이클립스 2022-06 (4.24.0) / Java SE 18 / 메이븐 3.8.6 / spring-context 5.3.22

교재
책 : 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 챕터 7

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>in.wonj</groupId>
  <artifactId>chap07-practice</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  
  <dependencies>
  	<dependency>
  		<groupId>org.springframework</groupId>
  		<artifactId>spring-context</artifactId>
  		<version>5.3.22</version>
  	</dependency>
  	<dependency>
  		<groupId>org.aspectj</groupId>
  		<artifactId>aspectjrt</artifactId>
  		<version>1.9.7</version>
  	</dependency>
  	<dependency>
  		<groupId>org.aspectj</groupId>
  		<artifactId>aspectjweaver</artifactId>
  		<version>1.9.7</version>
  	</dependency>
  </dependencies>
  
  <build>
  	<plugins>
  		<plugin>
  			<groupId>org.apache.maven.plugins</groupId>
  			<artifactId>maven-compiler-plugin</artifactId>
  			<version>3.10.1</version>
  			<configuration>
  				<release>18</release>
  			</configuration>
  		</plugin>
  	</plugins>
  </build>
  
</project>

Proxy

핵심 기능의 구현과 실행을 다른 객체에 위임하고 공통기능(부가기능)을 제공하는 객체

AOP proxy in Spring : an object created by the AOP framework in order to implement the aspect contracts (advise method executions and so on). In the Spring Framework, an AOP proxy will be a JDK dynamic proxy or a CGLIB proxy.
- Spring reference

프록시와 유사한 데코레이터가 있다. 주된 구현목적에 따라 구분한다.

  • 프록시 : 접근 제어
  • 데코레이터 : 기능 추가 및 확장

프록시가 핵심 기능을 위임한 객체를 대상 객체(Target Object)라 한다.
아래 코드는 프록시 객체 Proxy가 대상 객체의 메소드 Target.count()를 호출한다.

// Main.java
package main;
public class Main {
	public static void main(String[] args) {
		Proxy proxy = new Proxy(new Target());
		proxy.countWithTimer(100);
		proxy.countWithTimer(500);
		//	Begin : 1659863452440
		//	0 1 2 3 4 5 ...(생략)... 98 99 100 
		//	END : 1659863452448
		//	 ** Running Time : 8 ms
		//	Begin : 1659863452448
		//	0 1 2 3 4 5 ...(생략)... 498 499 500 
		//	END : 1659863452470
		//	 ** Running Time : 22 ms	
	}
}

// Proxy.java
package main;
public class Proxy {
	private TargetInterface delegate;
	
	public Proxy(TargetInterface delegate) {
		this.delegate = delegate;
	}
	
	public void countWithTimer(int n) {
		long begin = System.currentTimeMillis();
		System.out.println("Begin : " + begin);
		
		delegate.count(n);
		long end = System.currentTimeMillis();
		System.out.println("\nEND : " + end + "\n ** Running Time : " + (end-begin) + " ms");
	}
}

// TargetInterface.java
package main;
public interface TargetInterface {
	public void count(int n);
}

// Target.java
package main;
public class Target implements TargetInterface {
	public void count(int n) {
		for(int i=0; i<=n; i++)
			System.out.print(i + " ");
	}
}

참고

AOP (Aspect Oriented Programming)

공통기능을 분리 구현하여 재사용성을 높이는 프로그래밍 기법.
** AOP의 Aspect가 공통기능을 이르는 말이다.

개요

어떤 객체가 본래 두 가지 기능으로 이루어졌다고 가정하자

  • 공통 관심 기능
  • 핵심 기능

이 상황에서 AOP는 다음과 같이 적용된다.

  1. 객체에 핵심 기능만을 남기고 공통 기능은 분리한다.
  2. 이후 적당한 시점에 객체의 핵심 기능에 공통 기능을 삽입하여 본래와 동일한 동작을 수행케 한다.

즉, AOP기법을 도입하면 핵심 기능에 수정을 가하지 않으면서 공통 기능을 구현할 수 있다.

삽입은 아래 세 가지 방법으로 수행한다.

  • (AspectJ) 컴파일시점에 삽입
  • (AspectJ) 클래스 로딩시점에 바이트코드 조작
  • (Spring AOP) 런타임에 프록시 객체 생성

용어

  • Aspect : 공통 관심 기능 (e.g. 트랜잭션, 보안)
  • Advice : 공통 관심 기능을 언제 적용할 지에 대한 정의
    • Before : 대상 객체 메소드 호출 전
    • After Returning : 대상 객체 메소드가 예외 없이 실행된 이후
    • After Throwing : 대상 객체 메소드에서 예외가 발생한 이후
    • After : 대상 객체 메소드가 실행된 이후(또는 예외발생 이후)
    • Around : 대상 객체의 메소드가 실행되기 이전 + 실행된 이후(또는 예외발생 이후)
  • Joinpoint : Advice를 적용할 수 있는 지점 (스프링AOP는 메소드 호출에 대한 Joinpoint만 지원)
  • Pointcut : Joinpoint내에서 Advice를 적용활 범위를 고른 것
  • Weaving : Advice를 핵심 기능에 삽입하는 것 (스프링AOP는 런타임 프록싱으로 제공)

참고

Spring을 활용한 구현

Spring에서 프록시객체를 자동 구현(Weaving)하기 위해 세 가지가 필요하다. 각 구성요소에 필요한 어노테이션을 같이 적어두었다.

  • 대상 객체 코드 (핵심 기능 구현)
  • Aspect코드 (공통 관심 기능 구현)
    • @Aspect
    • @Pointcut()
    • Advice 어노테이션 ( e.g. @Around(), @Before(), ... )
  • 설정코드
    • @EnableAspectJAutoProxy

위 구성요소를 간단히 구현하면 아래와 같다.

// AppCtx.java (설정파일)
package config;

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

import aspect.TimerAspect;
import main.Counter;
import main.NormalCounter;
import main.StarCounter;

@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
	@Bean
	public TimerAspect timerAspect() {
		return new TimerAspect();
	}
	
	@Bean
	public Counter normalCounter() {
		return new NormalCounter();
	}
	
	@Bean
	public Counter starCounter() {
		return new StarCounter();
	}
}

// TimerAspect.java (공통 관심 기능 구현)
package aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class TimerAspect {
	@Pointcut("execution(public * main..*.*(..))")
	// All public method in package "main" (The method can have arguments)
	private void publicTarget() { }
	
	@Around("publicTarget()")
	public String run(ProceedingJoinPoint joinpoint) throws Throwable {
		// Before proceed()
		int num = (int)joinpoint.getArgs()[0];
		long begin = System.currentTimeMillis();
		System.out.println("run() for argument : " + num + " / " + joinpoint.getTarget());
		System.out.println("Begin : " + begin);
		
		// proceed() : Call method of Target Object
		String countString = (String)joinpoint.proceed();
		
		// After proceed()
		long end = System.currentTimeMillis();
		System.out.println("END : " + end);
		System.out.println(" ** Running Time : " + (end-begin) + " ms");
		System.out.println("--------------------------------------------");
		
		return countString;
	}
}

// Main.java (프로그램 진입점)
package main;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import config.AppCtx;

public class Main {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);	
		Counter normalCnt = ctx.getBean("normalCounter", Counter.class);
		Counter starCnt = ctx.getBean("starCounter", Counter.class);
		
		String s1 = normalCnt.count(20);
		System.out.println(s1);
		//		run() for argument : 20 / main.NormalCounter@821330f
		//		Begin : 1659869004037
		//		END : 1659869004042
		//		 ** Running Time : 5 ms
		//		--------------------------------------------
		//		0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 
		
		System.out.println("===========================");
		
		String s2 = starCnt.count(100);
		System.out.println(s2);
		//		run() for argument : 100 / main.StarCounter@20b2475a
		//		Begin : 1659869004043
		//		END : 1659869004044
		//		 ** Running Time : 1 ms
		//		--------------------------------------------
		//		0*1*2*3*4*5*...(생략)...97*98*99*100*
		
		ctx.close();
	}
}

// Counter.java (핵심기능 객체의 인터페이스)
package main;
public interface Counter {
	public String count(int n);
}

// NormalCounter.java (핵심기능 구현 객체)
package main;
public class NormalCounter implements Counter {
	public String count(int n) {
		String res = "";
		for(int i=0; i<=n; i++)
			res += (i + " ");
		return res;
	}
}

// StarCounter.java (핵심기능 구현 객체)
package main;
public class StarCounter implements Counter {
	public String count(int n) {
		String res = "";
		for(int i=0; i<=n; i++)
			res += (i + "*");
		return res;
	}
}

프록시 생성 방식

위 섹션의 구현 예시에서 Main.java를 살펴보면 다음 특징이 있다.

  • Bean객체를 받는 변수의 타입이 인터페이스(Counter)이다.
  • Bean객체의 이름은 실제 구현 클래스(normalCounter, starCounter)이다.
  • 불러오는 클래스파일은 인터페이스(Counter)이다.
public class Main {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);	
		Counter normalCnt = ctx.getBean("normalCounter", Counter.class);
		Counter starCnt = ctx.getBean("starCounter", Counter.class);
        
        ...
    }
}

아래와 같이 수정하면 타입이 잘못되었다는 예외를 띄운다.
getBean이 리턴한 것이 NormalCounter객체가 아닌 jdk Proxy객체이기 때문이다.

[ 이전 ]
Counter normalCnt = ctx.getBean("normalCounter", Counter.class);

[ 수정 후 ]
NormalCounter normalCnt = ctx.getBean("normalCounter", NormalCounter.class);

[ 예외 ]
Exception in thread "main" org.springframework.beans.factory.BeanNotOfRequiredTypeException: 
Bean named 'normalCounter' is expected to be of type 'main.NormalCounter' 
but was actually of type 'jdk.proxy2.$Proxy19'

Proxy객체에는 두 가지가 있다.
인터페이스를 구현한 클래스에 Aspect를 적용하면 기본적으로 Jdk Proxy객체를 생성한다.

  • Jdk Proxy 객체 : 인터페이스 상속
  • CGLib Proxy 객체 : 클래스 상속
// AppCtx.java
package config;

...

@Configuration
@EnableAspectJAutoProxy
public class AppCtx { ... }

// Main.java
package main;

...

public class Main {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);	
        
		Counter normalCnt = ctx.getBean("normalCounter", Counter.class);
		System.out.println(normalCnt.getClass().getName());
		//	jdk.proxy2.$Proxy19
		
		Counter starCnt = ctx.getBean("starCounter", Counter.class);
		System.out.println(starCnt.getClass().getName());
		//	jdk.proxy2.$Proxy19
		
		ctx.close();
	}
}

인터페이스 구현 클래스에 Aspect를 적용하면서도 CGLib Proxy를 생성할 수 있다.
설정 파일의 @EnableAspectJAutoProxyproxyTargetClass = true인수를 주면 된다.
출력 내용에서 CGLIB 문구를 확인할 수 있다.

// AppCtx.java
package config;

...

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

// Main.java
package main;

...

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

		NormalCounter normalCnt = ctx.getBean("normalCounter", NormalCounter.class);
		System.out.println(normalCnt.getClass().getName());
		//	main.NormalCounter$$EnhancerBySpringCGLIB$$7f27111a
		
		Counter starCnt = ctx.getBean("starCounter", Counter.class);
		System.out.println(starCnt.getClass().getName());
		//	main.StarCounter$$EnhancerBySpringCGLIB$$ecc4f719
		
		ctx.close();
	}
}

참고

Pointcut

아래 코드에서 private void publicTarget()메소드는 재사용 가능한 pointcut이다.

@Aspect
public class TimerAspect {
	@Pointcut("execution(public * main..*.*(..))")
	private void publicTarget() { }
	
	@Around("publicTarget()")
	public String run(ProceedingJoinPoint joinpoint) throws Throwable {
		...
	}
}

아래와 같이 재사용하지 않는 pointcut을 @Around어노테이션에 바로 인수로 줄 수 있다.

@Aspect
public class TimerAspect {
	@Around("execution(public * main..*.*(..))")
	public String run(ProceedingJoinPoint joinpoint) throws Throwable {
		...
	}
}

Pointcut 지시자

어떤 메소드를 pointcut으로 지정하기 위해 PCD(Pointcut Designator)를 사용한다.
e.g. execution, within

문법은 aspectJ패턴을 따른다. 특수기호로 두 가지를 사용한다.

  • * : 어떤 패턴에도 매칭(와일드카드)
  • .. : 0개 이상, 현재 패키지와 그 하위 패키지

execution 지시자는 아래 내용을 포함한다. [ ]표시가 있는 항목은 생략 가능하다.

@Pointcut("execution( [public] 리턴타입  [패키지명/클래스경로]메소드명(인수) [throws 타입]")

(e.g.)
execution("public int main.StarCounter.count(int)")
  --> main패키지 내 StarCounter클래스의 int count(int) 메소드

execution("* count(..))
  --> count라는 이름의 메소드

execution("* main..*(..)")
  --> main 패키지 내의 메소드

within 지시자는 특정 패키지/클래스를 지정한다.

@Pointcut("within( [패키지][클래스] )")

(e.g.)
within(main.NormalCounter)
  --> main패키지-NormalCounter클래스의 메소드
  
within(aa..*)
  --> aa패키지 내 모든 클래스의 메소드

within(*..*Counter)
  --> 모든 패키지 내 ~Counter로 끝나는 클래스의 메소드

@Pointcut메소드를 별도 파일로 분리하고, 이를 @Around의 인수로 줄 수 있다.

//CommonPointcut.java
package aspect;

import org.aspectj.lang.annotation.Pointcut;

public class CommonPointcut {
	@Pointcut("within(*..*Counter)")
	public void publicTarget() { }
}


// TimerAspect.java
package aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class TimerAspect {
	@Around("aspect.CommonPointcut.publicTarget()")
	public String run(ProceedingJoinPoint joinpoint) throws Throwable {
		...
	}
}

참고

Advice 적용 순서

한 메소드에 여러 개의 Advice가 적용된 경우 컴파일러가 자동으로 적용 순서를 정한다.
Aspect클래스에 @Order(정수)어노테이션을 붙여 임의 순서를 부여할 수 있다. (숫자가 작을 수록 먼저 적용)

아래 예시는 두 개의 메소드 NormalCounter.count(), StarCounter.count()에 대해 두 개의 Advice PrintResultAspect, TimerAspect를 차례대로 적용한다.

  • PrintResultAspect.javajoinpoit.proceed()TimerAspect.run()이다.
  • TimerAspect.javajoinpoint.proceed()Counter클래스.count()이다.
// AppCtx.java
package config;

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

import aspect.PrintResultAspect;
import aspect.TimerAspect;
import main.Counter;
import main.NormalCounter;
import main.StarCounter;

@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
	@Bean
	public TimerAspect timerAspect() {
		return new TimerAspect();
	}
	
	@Bean
	public PrintResultAspect printResultAspect() {
		return new PrintResultAspect();
	}
	
	@Bean
	public Counter normalCounter() {
		return new NormalCounter();
	}
	
	@Bean
	public Counter starCounter() {
		return new StarCounter();
	}
}

// Main.java
package main;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import config.AppCtx;

public class Main {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);	
		Counter normalCnt = ctx.getBean("normalCounter", Counter.class);
		Counter starCnt = ctx.getBean("starCounter", Counter.class);
		
		String s1 = normalCnt.count(20);
		System.out.println("\n\n");
		String s2 = starCnt.count(100);
		
		ctx.close();
	}
	//	==== PrintResultAspect : start
	//	==== TimerAspect : start
	//	run() for argument : 20 / main.NormalCounter@333d4a8c
	//	Begin : 1659890718801
	//	END : 1659890718802
	//	 ** Running Time : 1 ms
	//	--------------------------------------------
	//	==== TimerAspect : end
	//	0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 
	//	--------------------------------------------
	//	==== PrintResultAspect : end
	//	
	//	
	//	
	//	==== PrintResultAspect : start
	//	==== TimerAspect : start
	//	run() for argument : 100 / main.StarCounter@69c81773
	//	Begin : 1659890718803
	//	END : 1659890718803
	//	 ** Running Time : 0 ms
	//	--------------------------------------------
	//	==== TimerAspect : end
	//	0*1*2*3*4*5*6*7*8*9*10*11*12*13*14*15*16*17*18*19*20*21*22*23*24*25*26*27*28*29*30*31*32*33*34*35*36*37*38*39*40*41*42*43*44*45*46*47*48*49*50*51*52*53*54*55*56*57*58*59*60*61*62*63*64*65*66*67*68*69*70*71*72*73*74*75*76*77*78*79*80*81*82*83*84*85*86*87*88*89*90*91*92*93*94*95*96*97*98*99*100*
	//	--------------------------------------------
	//	==== PrintResultAspect : end
}


// CommonPointcut.java
package aspect;
import org.aspectj.lang.annotation.Pointcut;

public class CommonPointcut {
	@Pointcut("within(*..*Counter)")
	public void publicTarget() { }
}

// PrintResultAspect.java
package aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.aspectj.lang.annotation.Around;

@Aspect
@Order(1)
public class PrintResultAspect {
	@Around("aspect.CommonPointcut.publicTarget()")
	public void run(ProceedingJoinPoint joinpoint) throws Throwable {
		System.out.println("==== PrintResultAspect : start");
		String res = (String)joinpoint.proceed();
		System.out.println(res);
		System.out.println("--------------------------------------------");
		System.out.println("==== PrintResultAspect : end");
	}
}

// TimerAspect.java
package aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;

@Aspect
@Order(2)
public class TimerAspect {
	@Around("CommonPointcut.publicTarget()")
	public String run(ProceedingJoinPoint joinpoint) throws Throwable {
		System.out.println("==== TimerAspect : start");
		
		int num = (int)joinpoint.getArgs()[0];
		long begin = System.currentTimeMillis();
		System.out.println("run() for argument : " + num + " / " + joinpoint.getTarget());
		System.out.println("Begin : " + begin);
		
		// proceed() : Call method of Target Object
		String countString = (String)joinpoint.proceed();
		
		// After proceed()
		long end = System.currentTimeMillis();
		System.out.println("END : " + end);
		System.out.println(" ** Running Time : " + (end-begin) + " ms");
		System.out.println("--------------------------------------------");
		
		System.out.println("==== TimerAspect : end");
		
		return countString;
	}
}

// Counter.java
package main;
public interface Counter {
	public String count(int n);
}

// NormalCounter.java
package main;
public class NormalCounter implements Counter {
	public String count(int n) {
		String res = "";
		for(int i=0; i<=n; i++)
			res += (i + " ");
		return res;
	}
}

// StarCounter.java
package main;
public class StarCounter implements Counter {
	public String count(int n) {
		String res = "";
		for(int i=0; i<=n; i++)
			res += (i + "*");
		return res;
	}
}
profile
노는게 제일 좋습니다.

0개의 댓글