본문 바로가기
Programming/Spring

AOP 기초

by TinKerBellBass 2017. 8. 19.
728x90
반응형

참고도서

초보 웹 개발자를 위한 스프링4 프로그래밍 입문
국내도서
저자 : 최범균
출판 : 가메출판사 2015.03.02
상세보기



1. 프록시(Proxy)

팩토리얼의 결과를 구하기 위한 인터페이스와 두 개의 클래스

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
public interface Calculator {
 
    public long factorial(long num);
}
 
// for 문을 이용한 팩토리얼 계산
public class ImpeCalculator implements Calculator {
 
    @Override
    public long factorial(long num) {
        long result = 1;
        for (int i = 1; i <= num; i++) {
            result * = i;
        }        
        return result;
    }
}
 
// 재귀호출을 이용한 팩토리얼 계산
public class RecCalculator implements Calculator {
 
    @Override
    public long factorial(long num) {
        if (num == 0)
            return 1;
        else
            return num * factorial(num - 1);
    }
}
cs
 
실행 시간과 팩토리얼의 결과를 출력하는 메인 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CalMain {
 
    public static void main(String[] args) {
 
        ImpeCalculatort impeCal = new ImpeCalculator();
        long start1 = System.currentTimeMillis();
        long fourFactorial1 = impeCal.factorial(4);
        long end1 = System.currentTimeMillis();
        System.out.println("ImpeCalculator.factorial(4) 결과= "+fourFactorial1);
        System.out.printf("ImpeCalculator.factorial(4) 실행시간= %d\n", (end1-start1));
    
        ImpeCalculatort recCal = new RecCalculator();
        long start2 = System.currentTimeMillis();
        long fourFactorial2 = impeCal.factorial(4);
        long end2 = System.currentTimeMillis();
        System.out.println("RecCalculator.factorial(4) 결과= "+fourFactorial2);
        System.out.printf("RecCalculator.factorial(4) 실행시간= %d\n", (end2-start2));
    }
}
cs


반복문과 재귀호출을 이용한 팩토리얼의 결과와 실행시간의 출력을 제대로 출력할 수는 있지만, 이런 구조에는 문제가 있다.

팩토리얼 결과를 얻어오는 실행시간을 계산해서 출력하기 위한 코드가 변수 이름만 다를 뿐 중복되고 있다.

지금은 밀리초 단위로 구하고 있지만 나노초 단위로 구하고 싶으면 두 곳 다 변경해야 한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ExeTimeCalculator implements Calculator {
 
    private Calculator delegate;
  // 생성자를 통해 Calculator interface 를 구현한 클래스를 선택한다
    public ExeTimeCalculator(Calculator delegate) {
        this.delegate = delegate;
    }
 
    // 실행시간을 구하는 공통되는 기능을 하나로 뽑아냈다.
    @Override
    public long factorial(long num) {
        long start = System.nanoTime();
        // 팩토리얼의 결과를 구하는 기능은 Calculator Interface에 위임하였다
        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;
    }
}
//cs


Calculator 를 구현하는 클래스를 만들고 factorial( ) 메소드를 재정의 하였다. 

재정의 된 factorial( ) 메소드는 수행시간을 구하는 공통 기능(부가적인 기능)을 수행하며, 

팩토리얼을 구하는 기능(핵심기능)은 Calculator Interface 를 구현하고 있는 클래스에 위임해서 해서 실행하고 있다.


1
2
3
4
5
6
7
8
9
10
11
public class MainProxy {
 
    public static void main(String[] args) {
 
        ExeTimeCalculator ttCal1 = new ExeTimeCalculator(new ImpeCalculator());
        System.out.println(ttCal1.factorial(20));
 
        ExeTimeCalculator ttCal2 = new ExeTimeCalculator(new RecCalculator());
        System.out.println(ttCal2.factorial(20));
    }
}
cs


메인 클래스에 중복되고 있던 코드가 사라졌다.


이처럼 핵심기능은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체를 프록시(Proxy)라 부르고,

핵심 기능을 실행하는 객체를 대상 객체라고 부근다.


엄밀히 말하면 지금 작성한 클래스는 프록시라기 보다 데코레이터(Decorator)에 가깝다. 

프록시가 접근 제어 관점에 초점이 맞추어져 있다면, 데코레이터는 기능 추가와 확장에 초점이 맞추어져 있기 때문이다.

팩토리얼의 결과를 구하는 기존 기능에 시간 측정 기능을 추가하고 있기 때문에 데코레이터에 가깝지만,

스프링의 레퍼런스 문서에서 AOP를 설명할 때 프록시란 용어를 사용하기 때문에, 프록시라고 하였다.


2. AOP(Aspect Oriented Programming)

위와 같이 공통기능과 핵심 기능을 분리하는 것이 AOP의 핵심이다.

이런 분리 과정을 통해 핵심 기능을 구현한 코드의 수정 없이 공통 기능을 적용할 수 있다.

스프링은 이런 프록시를 통해 AOP를 구현하고 있다.


AOP의 기본 개념인 핵심 기능에 공통 기능을 삽입하는 방법으로는 크게 세 가지가 있다.


① 컴파일 시점에 코드에 공통 기능을 추가

② 클래스 로딩 시점에 바이트 코드에 공통 기능을 추가 

③ 런타임에 프록시 객체를 생성해서 공통 기능을 추가


①번 방법은 AOP 개발 도구가 소스 코드를 컴파일 하기 전에 공통 구현 코드를 소스에 삽입하는 방식이고,

②번 방법은 클래스를 로딩할 때 바이트 코드에 공통 기능을 클래스에 삽입하는 방식이다.

이 두 가지 방법은 스프링 AOP에서는 지원하지 않으며, AspectJ 와 같이 AOP를 위한 전용 도구를 사용해서 적용할 수 있다.


스프링이 제공하는 AOP 방식은 프록시를 이용하는 ③번 방식이고, 프록시 객체를 자동으로 만들어 준다.

스프링 AOP는 중간에 프록시 객체를 생성하고, 실제 객체의 기능을 실행하기 전후에 공통 기능을 호출한다.





AOP 관련 용어

 JoinPoint

 Advice를 적용 가능한 지점을 의미한다. 메소드 호출, 필드 값 변경 등이 JoinPoint에 해당한다. 

 스프링은 프록시를 이용해서 AOP를 구현하기 때문에 메소드 호출에 대한 JoinPoint만 지원한다.

 Pointcut

 JoinPoint의 부분집합으로 실제로 Advice가 적용되는 JoinPoint를 나타낸다.

 스프링에서는 정규표현식이나 AspectJ의 문법을 이용하여 Pointcut을 정의할 수 있다.

 Advice

 언제 공통 관심 기능을 핵심 로직에 적용할지를 정의하고 있다. 

예를 들어 '메소드를 호출하기 전'(언제)에 '트랜잭션 시작'(공통기능) 기능을 적용한다는 것을 정의하고 있다.

 Weaving

 Advice를 핵심 로직 코드에 적용하는 것을 weaving이라고 한다.

 Aspect

 여러 객체에 공통으로 적용되는 기능을 Aspect라고 한다. 트랙잭션, 보안 등이 Aspect의 좋은 예이다.


Advice 의 종류

 Before Advice 

 대상 객체의 메소드 호출 전에 공통 기능을 실행

 After Returning Advice

 대상 객체의 메소드가 예외 발생 없이 실행된 이후에 공통 기능을 실행

 After Throwing Advice

 대상 객체의 메소드를 실행하는 도중에 예외가 발생한 경우에 공통 기능을 실행

 After Advice

 대상 객체의 메소드를 실행하는 도중에 발생한 예외의 여부에 상관 없이 메소드 실행 후 공통 기능 실행

 try-catch-finally 의 finally 블록과 비슷

 Aroung Advice

 대상 객체의 메소드 실행 전후 또는 예외 발생 시점에 공통 기능을 실행

 주로 사용됨


3. AOP 구현(XML 방식)

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
<!-- XML 설정 파일, aopPojo.xml -->
<!-- 공통 기능을 제공할 클래스를 빈으로 등록 -->
<bean id="exeTimeAspect" class="aspect.ExeTimeAspect" />
 
<!-- Aspect 설정: Advice를 어떤 Pointcut에 적용할 지 설정 -->
<aop:config>
    <!-- 공통기능을 제공할 클래스의 빈 등록을 ref -->
    <aop:aspect id="measureAspect" ref="exeTimeAspect">
        <aop:pointcut id="publicMethod" expression="execution(public * chap07..*(..))" />
        <!-- ref한 pointcut에 공통 기능을 수행하는 method measure()를 around 방식으로 수행 -->
        <aop:around pointcut-ref="publicMethod" method="measure" />
    </aop:aspect>
</aop:config>
 
<bean id="impeCal" class="chap07.ImpeCalculator" />
<bean id="recCal" class="chap07.RecCalculator" />
 
 
public class ExeTimeAspect {
    // 빈에 등록한 공통기능을 수행하는 메소드
    // ProceedingJoinPoint 타입 파라미터는 대상 객체의 메소드(핵심 기능)를 호출할 때 사용
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        // 핵심 기능 수행 전
        long start = System.nanoTime();
        try {
            // joinpoint.proceed() 메소드는 핵심기능 메소드를 실행시킴, 핵심 기능 수행 됨
            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(),
                    sig.getName(), Arrays.toString(joinPoint.getArgs()), (finish - start));
        }
    }
}
/* 
ProceedingJoinPoint 의 메소드
Signature getSignature() - 호출되는 메소드에 대한 정보를 구한다.
Object getTarget() - 대상 객체를 구한다.
Object[] getArgs() - 파라미터 목록을 구한다.
Signature 의 메소드
String getName() - 메소드의 이름을 구한다.
String toLongString() - 메소드를 완전하게 표현한 문장을 구한다(리턴타입, 파라미터 타입 모두 표시).
String toShortString() - 메소드를 축약해서 문장을 구한다(기본 구현은 메소드의 이름만 구한다).
*/
 
 
public class MainXmlPojo {
 
    public static void main(String[] args) {
 
        GenericXmlApplicationContext ctx = new GenericXmlApplicationContext("classpath:aopPojo.xml");
 
        Calculator impeCal = ctx.getBean("impeCal", Calculator.class);
        // factorial() 메소드를 pointcut으로 설정 파일에 등록하였기 때문에 실행 되기 전후로 Aspect가 실행된다
        long fiveFact1 = impeCal.factorial(5); // impeCal 빈 객체는 프록시 객체
        System.out.println("impeCal.fatorial(5)= " + fiveFact1);
 
        Calculator recCal = ctx.getBean("recCal", Calculator.class);
        long fiveFact2 = recCal.factorial(5); // recCal 빈 객체는 프록시 객체
        System.out.println("recCal.factorial(5)= " + fiveFact2);
    }
}
cs





Calculator impeCal = ctx.getBean("impeCal", Calculator.class);

<bean id="impeCal" class="chap07.ImpeCalculator" />

코드를 보면 XML 설정에는 impeCal 이란 이름으로 ImpeCaculator 클래스를 등록했다.

그러나 빈 객체를 얻어 올 때는 Calculator 인터페이스를 설정하고 있다.

getBean("impeCal", Calculator.class); 에서 impeCal 객체는 ImpeCaculator 클래스의 인스턴스 객체가 아니다.

스프링 AOP 가 Calculator 인터페이스를 구현하여 만든 프록시 객체이다.

만약 getBean("impeCal", ImpeCalculator.class); 로 설정하여 실행하면 

getBean( ) 메소드에 사용한 타입이 ImpeCalculator 인데 반해, 실제 타입은 $Proxy2라는 에러가 발생한다.



$Proxy2는 스프링이 런타임에 생성한 프록시 객체의 클래스 이름으로, 

이 클래스는 ImpeCalculator 클래스가 구현한 Calculator 인터페이스를 상속 받는다.



스프링은 AOP를 위한 프록시 객체를 생성할 때 실제 생성할 빈 객체가 인터페이스를 상속하고 있으면 

인터페이스를 이용해서 프록시를 생성한다.

따라서 빈의 실제 타입이 ImpeCalculator 라고 하더라도, "impeCal" 이름에 해당하는 빈 객체의 타입은

Calculator 인터페이스를 상속받은 프록시 타입이 된다.


빈 객체가 인터페이스를 구현하고 있을 때, 인터페이스가 아닌 실제 클래스를 이용해서 프록시를 생성하는 방법


1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- proxy-target-class="true" 프록시 타겟을 클래스로 해라 -->
<aop:config proxy-target-class="true">
    <!-- 공통기능을 제공할 클래스의 빈 등록을 ref -->
    <aop:aspect id="measureAspect" ref="exeTimeAspect">
        <aop:pointcut id="publicMethod" expression="execution(public * chap07..*(..))" />
        <!-- ref한 pointcut에 공통 기능을 수행하는 method measure()를 around 방식으로 수행 -->
        <aop:around pointcut-ref="publicMethod" method="measure" />
    </aop:aspect>
</aop:config>
 
<bean id="impeCal" class="chap07.ImpeCalculator" />
<bean id="recCal" class="chap07.RecCalculator" />
 
<!-- ImpeCalculator impeCal = ctx.getBean("impeCal", ImpeCalculator.class); -->
cs

 

4. AOP 구현(@Aspect 애노테이션 방식)

@Aspcet를 붙여서 클래스에서 AOP 관련 사항 설정

 

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
<!-- XML 설정 파일, aopAspect.xml -->
    <!-- aop 관련 어노테이션을 인식할 수 있도록 설정 -->    
    <aop:aspectj-autoproxy />
 
    <bean id="exeTimeAspect" class="aspect.ExeTimeAspect2" />
    <bean id="impeCal" class="chap07.ImpeCalculator" />
    <bean id="recCal" class="chap07.RecCalculator" />
 
 
// AOP를 설정한 클래스라고 알림
@Aspect
public class ExeTimeAspect2 {
 
    // 포인트컷 설정
    @Pointcut("execution(public * chap07..*(..))")
    private void publicTarget() {
    }
 
    // publicTarget() 에 설정한 포인트컷을 사용해서 Around 방식으로 Aspect 실행
    @Around("publicTarget()")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        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(), sig.getName(), Arrays.toString(joinPoint.getArgs()), (finish - start));
        }
    }
}
 
 
public class MainXmlAspect {
 
    public static void main(String[] args) {
 
        GenericXmlApplicationContext ctx = new GenericXmlApplicationContext("classpath:aopAspect.xml");
 
        Calculator impeCal = ctx.getBean("impeCal", Calculator.class);
        long fiveFact = impeCal.factorial(5); // impeCal 은 프록시 객체 
        System.out.println("impeCla.factorial(5)= " + fiveFact);
    }
 
}
cs



스프링 컨테이너 생성을 XML 설정이 아닌 자바 설정을 사용


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
// AOP 어노테이션을 인식할 수 있게 @EnableAspectJAutoProxy를 붙임
@Configuration
@EnableAspectJAutoProxy
public class javaConfig {
 
    @Bean
    public ExeTimeAspect2 exeTimeAspect() {
        return new ExeTimeAspect2();
    }
 
    @Bean
    public Calculator recCal() {
        return new RecCalculator();
    }
}
 
 
// AOP를 설정한 클래스라고 알림
@Aspect
public class ExeTimeAspect2 {
 
    // 포인트컷 설정
    @Pointcut("execution(public * chap07..*(..))")
    private void publicTarget() {
    }
 
    // publicTarget() 에 설정한 포인트컷을 사용해서 Around 방식으로 Aspect 실행
    @Around("publicTarget()")
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        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(), sig.getName(), Arrays.toString(joinPoint.getArgs()), (finish - start));
        }
    }
}
 
 
public class MainJavaAspect {
 
    public static void main(String[] args) {
 
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(javaConfig.class);
 
        Calculator recCal = ctx.getBean("recCal", Calculator.class);
        long fiveFact = recCal.factorial(5); // impeCal 은 프록시 객체
        System.out.println("recCal.factorial(5)= " + fiveFact);
    }
}
cs



위 두 방식에서 빈 객체가 인터페이스를 구현하고 있을 때, 인터페이스가 아닌 실제 클래스를 이용해서 프록시를 생성하는 방법


1
2
3
4
5
6
7
8
9
<!-- @Aspect 방식 XML 설정 -->
<aop:aspectj-autoproxy proxy-target-class="true">
 
 
// @Aspect 방식 자바 설정
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class JavaConfig{
}
cs


5. execution 명시자 표현식

Advice를 적용할 메소드를 지정하는 Pointcut 의 execution 표현 방법


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


수식어 패턴은 생략 가능. 스프링 AOP는 public 메소드만 적용 가능하기 때문에 보통 생략한다.


 execution(public void set*(..)) 

 리턴타입이 void, 메소드 이름이 set으로 시작, 파라미터가 0개 이상인 메소드 호출

 파라미터 부분의 '..' -> 파라미터 0개 이상

 execution(* chap07.*.*())

 chap07 패키지의 타입에 속한 파라미터가 없는 모든 메소드 호출

 execution(* chap07..*.*(..))

 chap07 패키지 및 하위 패키지에 있는 파라미터가 0개 이상인 메소드 호출

 패키지 부분의 '..' -> 하위 패키지

 execution(Long chap02.Calculator.factorial(..))

 리턴 타입이 Long인 Calculator 타입의  factorial() 메소드 호출

 execution(* get*(*)

 이름이 get으로 시작하고 2개의 파라미터를 갖는 메소드 호출

 execution(* get*(*,*)

 이름이 get으로 시작하고 2개의 파라미터를 갖는 메소드 호출

 execution(* read*(Integer,..))

 메소드 이름이 read로 시작하고, 

 첫 번째 파라미터 타입이 Integer이며 1개 이상의 파라미터를 갖는 메소드 호출


6. Advice 적용 순서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CacheAspect {
 
    private Map<Long, Object> cache = new HashMap<Long, Object>();
 
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        Long num = (Long) joinPoint.getArgs()[0];
        if (cache.containsKey(num)) {
            System.out.printf("CacheAspect: Cache에서 구함[%d]\n", num);
        }
 
        Object result = joinPoint.proceed();
        cache.put(num, result);
        System.out.printf("CacheAspect: Cache에 추가[%d]\n", num);
        return result;
    }
}
cs


Map 을 사용해서 일종의 캐시를 구현한 Aspect 클래스로서,

실행 결과를 Map에 보관했다가 다음에 동일한 요청이 들어오면 Map에 보관한 결과를 리턴한다.


이 클래스에는 실행 시간을 구하는 코드가 없고, 실행 시간을 구하는 코드가 있는 Aspect가 있다고 가정하면

실행시간측정 프록시 -> 캐시 프록시 -> 실제 대상 객체 순으로 실행되어야 하기 때문에

프록시의 실행 순서를 설정해 줘야 한다.


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
// 실행시간을 측정하는 Aspect
public class ExeTimeAspect {
 
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        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(),
                    sig.getName(), Arrays.toString(joinPoint.getArgs()), (finish - start));
        }
    }
}
 
 
<!-- XML 설정 방식 -->
<!-- 공통 기능을 제공할 클래스를 빈으로 등록 -->
<bean id="exeTimeAspect" class="aspect.ExeTimeAspect" />
<bean id="cacheAspect" class="aspect.CacheAspect" />
 
<!-- Aspect 설정: Advice를 어떤 Pointcut에 적용할 지 설정 -->
    <aop:config>
    <aop:aspect id="calculatorCache" ref="cacheAspect" order="1">
        <aop:pointcut id="calculatorMethod" expression="execution(public * chap07.Calculator.*(..))" />
        <aop:around pointcut-ref="calculatorMethod" method="execute" />
    </aop:aspect>
 
    <aop:aspect id="measureAspect" ref="exeTimeAspect" order="0">
        <aop:pointcut id="publicMethod" expression="execution(public * chap07..*(..))" />
        <aop:around pointcut-ref="publicMethod" method="measure" />
    </aop:aspect>
</aop:config>
 
<bean id="impeCal" class="chap07.ImpeCalculator" />
 
<!-- @Aspect 어노테이션 설정 방식 -->
<!-- 
@Aspcet
@Order(1)
-->
 
 
public class MainXmlOrder {
 
    public static void main(String[] args) {
        GenericXmlApplicationContext ctx =
                new GenericXmlApplicationContext("classpath:aopOrder.xml");
 
        Calculator impeCal = ctx.getBean("impeCal", Calculator.class);
        impeCal.factorial(5);
        impeCal.factorial(5);
    }
}
cs


XML 설정 방식의 경우에는 order 속성을 쓰면 되고

@Aspect 설정 방식의 경우에는 @Order 어노테이션을 쓰면 된다.

728x90
반응형

'Programming > Spring' 카테고리의 다른 글

다국어 처리  (0) 2017.08.20
파일 업로드 / 예외처리  (0) 2017.08.20
Bean 라이프 사이클과 범위  (0) 2017.08.19
자바 코드를 이용한 설정  (0) 2017.08.19
의존 자동 주입  (0) 2017.08.18

댓글