환경
윈도우즈 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>
핵심 기능의 구현과 실행을 다른 객체에 위임하고 공통기능(부가기능)을 제공하는 객체
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가 공통기능을 이르는 말이다.
어떤 객체가 본래 두 가지 기능으로 이루어졌다고 가정하자
이 상황에서 AOP는 다음과 같이 적용된다.
즉, AOP기법을 도입하면 핵심 기능에 수정을 가하지 않으면서 공통 기능을 구현할 수 있다.
삽입은 아래 세 가지 방법으로 수행한다.
참고
Spring에서 프록시객체를 자동 구현(Weaving)하기 위해 세 가지가 필요하다. 각 구성요소에 필요한 어노테이션을 같이 적어두었다.
@Aspect@Pointcut()@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를 살펴보면 다음 특징이 있다.
Counter)이다.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객체를 생성한다.
// 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를 생성할 수 있다.
설정 파일의 @EnableAspectJAutoProxy에 proxyTargetClass = 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();
}
}
참고
아래 코드에서 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으로 지정하기 위해 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가 적용된 경우 컴파일러가 자동으로 적용 순서를 정한다.
Aspect클래스에 @Order(정수)어노테이션을 붙여 임의 순서를 부여할 수 있다. (숫자가 작을 수록 먼저 적용)
아래 예시는 두 개의 메소드 NormalCounter.count(), StarCounter.count()에 대해 두 개의 Advice PrintResultAspect, TimerAspect를 차례대로 적용한다.
PrintResultAspect.java의 joinpoit.proceed()는 TimerAspect.run()이다.TimerAspect.java의 joinpoint.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;
}
}