Spring AOP(Aspect Oriented Programming)
DI와 더불어 Spring Framework의 핵심 기능이 되는 것이 “AOP"라는 기술이다. 클래스 안에 외부에서 “처리"를 삽입하는 AOP의 구조와 기본적인 사용법에 대해 설명한다.
AOP이란?
Spring Framework에 있어서 DI(Dependency Injection 의존성 주입)와 더불어 중요한 근간이 되는 “AOP"이라는 기술이 있다.
AOP는 “Aspect Oriented Programming(관점 지향 프로그래밍)“의 약자이다. Aspect이라는 것은 일반적으로 “횡단적 관심사"라는 것이다. 즉, AOP는 문제를 바라보는 관점을 기준으로 프로그래밍하는 기법을 말한다.
- 문제를 해결하기 위한 핵심 관심 사항과 전체에 적용되는 공통 관심 사항을 기준으로 프로그래밍함으로써 공통 모듈을 여러코드에 쉽게 적용할 수 있도록 도와준다.
- AOP에서 중요한 개념은 “횡단 관점의 분리(Separation of Cross-Cutting Concern)“이다.
- OOP를 더욱 OOP답게 만들어 준다.
Tip
공통관심사항(cross-cutting concern)
- 공통기능으로 어플리케이션 전반에 걸쳐 필요한 기능 예) 로깅, 트랜잭션, 보안 등
핵심관심사항(core concern)
- 핵심로직, 핵심 비즈니스 로직
객체 지향 프로그램은 “클래스"를 기준으로 작성된다. 각각의 클래스마다, 그 클래스에 필요한 기능을 메서드로 구현하는 것이다. 이 수법은 사고방식으로써는 잘되어 있지만, 반대로 “클래스마다 완벽하게 결정되어 있어야 한다” 것은 몹시 번거려워질 수도 있다.
예를 들어, 프로그램의 개발 중에 작동 상황을 확인하기 위해 곳곳에 System.out.println
문장을 쓰고 값을 출력시키는 것은 누구든지 흔하게 하는 방법이다. 그런데, 이것은 생각해 보면 굉장히 귀찮은 방법이다. 다수의 클래스가 있으면, 각 클래스의 각 메소드마다 println을 쓰고 나가지 않으면 안된다. 또한, 그렇게 프로그램이 완성된 후에는 모든 println
을 제거하지 않으면 안된다.
이런 “다수의 클래스에 걸쳐 공통적으로 필요한 처리"가 횡단적 관심사이다. 만약 여러 클래스의 메서드에 println 문장을 자동으로 삽입 할 수 있는 기능이 있으면 상당히 편리 아니지 않을까? 그리고 필요가 없어지면 자동으로 삭제 할 수 있다면? 이것이 바로 AOP의 개념이다.
DI가 “의존성(값) 주입"이라면, AOP는 “처리 주입"이라고 해도 좋을 것이다. 외부에서 클래스의 특정 부분에 미리 준비 해둔 처리를 삽입하거나 제거하거나 하는 것이 AOP에서 실현되는 것이다.
pom.xml 준비
우선 프로젝트에 AOP 관련 라이브러리를 추가한다. pom.xml을 열고 <dependencies>
태그 안에 아래에 있는 내용을 추가한다.
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.10.RELEASE</version>
</dependency>
여기에 추가하는 것은 Spring AOP 라이브러리이다. groupId에 org.springframework를 지정하고, artifactId에 spring-aop를 지정한다. 또한 버전은 Spring Framework 4.3.10으로 이용하도록 맞춰 지정하고 있다. Spring Framework의 버전이 다르면 거기에 맞춰서 버전을 조정하면 된다.
AOP를 이용하는 Bean 클래스 생성
그럼 AOP를 이용해 보자. AOP는 특정 처리를 외부에서 클래스에 삽입하는 기능을 한다. 이를 위해서는 다음과 같은 준비해야 한다.
- AOP의 대상이 되는 클래스. 가장 일반적인 Bean 클래스를 준비한다.
- AOP에 삽입하는 처리를 하는 클래스. 여기에 삽입하는 작업을 준비한다.
- AOP에 대한 설정 정보. 이것은 Bean 설정 파일 또는 설정 클래스를 사용하여 준비한다.
먼저 AOP 대상이되는 클래스를 준비하자. 이번에는 com.devkuma.spring.aop라는 패키지를 준비하고, 이 안에 필요한 클래스들을 모으도록 하자. “SampleAopBean"라는 클래스를 아래와 같이 작성한다.
package com.devkuma.spring.aop;
public class SampleAopBean {
private String message;
public SampleAopBean() {
super();
}
public SampleAopBean(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public void printMessage() {
System.out.println("message:[" + message + "]");
}
}
이것은 먼저 만든 SampleBean과 거의 동일하다. 메시지를 저장하는 message 속성과 생성자, 그리고 printMessage라는 메소드를 만들었다. 이렇게, 사용하는 Bean 자체는 극히 간단한 POJO 클래스로써 Spring Framework의 특징이다.
MethodBeforeAdvice 클래스 생성
이어서 SampleAopBean에 AOP에 삽입하는 작업을 준비하자. 이것도 물론 Java 클래스로 정의한다.
com.devkuma.spring.aop 패키지에 “SampleMethodAdvice"라는 이름으로 클래스를 만들어 보자. 그리고 아래와 같이 코드를 작성한다.
package com.devkuma.spring.aop;
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
public class SampleMethodAdvice
implements MethodBeforeAdvice, AfterReturningAdvice {
@Override
public void before(Method method, Object[] args,
Object target) throws Throwable {
System.out.println("*before: " + method.getName() + "[" + target + "]");
}
@Override
public void afterReturning(Object returnValue, Method method,
Object[] args, Object target) throws Throwable {
System.out.println("*after: " + method.getName() + "[" + target + "]");
}
}
이번 작성한 SampleMethodAdvice는 두 개의 인터페이스를 구현하고 있다. 이 인터페이스는 처리의 삽입에 대한 메소드를 추가한다. 각각 간단히 정리를 하면 아래와 같다.
MethodBeforeAdvice
이것은 메소드가 실행하기 전에 처리를 삽입하기 위한 인터페이스이다. 이것은 ‘before’라는 메소드를 하나 가지고 있으며 다음과 같이 정의되어 있다.
public void before (Method method, Object [] args, Object target)
throws Throwable
Method는 대상 메소드, args는 그에 대한 인수, target은 대상이 되는 객체(인스턴스)가 각각 전달된다. 이러한 인수들은 어떤 인스턴스의 어떤 메서드를 호출하기 전에 이 처리를 수행했는지를 알 수 있다.
AfterReturningAdvice
이것은 메소드의 실행이 끝나고, 호출을 하고 원래대로 돌아오게 될 때 삽입하는 처리의 인터페이스이다. “afterReturning"라는 메소드가 준비되어 있다. 이것은 다음과 같이 정의되어 있다.
public void afterReturning (Object returnValue, Method method,
Object [] args, Object target) throws Throwable
메소드의 반환 값, 메소드, 메소드에 전달된 인수, 대상 인스턴스가 인수로 전달된다. 반환 값 이외는 위 before와 동일하므로 거의 같은 감각으로 처리하는 것이 가능하다.
여기에서는 각 메소드과 타겟을 System.out.println에서 출력하고 있을 뿐이다. AOP는 “처리 삽입"라고 했지만 어디라도 마음대로 삽입되는 것은 아니다. “이 타이밍에 삽입한다"는 것이 미리 몇 가지 준비되어 있을 것이다.
우선, 이 2개의 인터페이스를 익히면 “메소드를 호출하기 전과 호출 후"에 처리를 삽입할 수 있다. AOP의 기본을 알게 된것으로는 충분하다.
bean.xml 생성
다음에 해야 하는 것은 필요한 Bean의 설정을 준비하는 것이다. 우선 Bean 설정 파일을 사용해 보기로 하자.
먼저 “resources"폴더에 생성한 “bean.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- aop bean... -->
<bean id="sampleAopBean" class="com.devkuma.spring.aop.SampleAopBean">
<property name="message" value="this is AOP bean!" />
</bean>
<bean id="sampleMethodAdvice"
class="com.tuyano.libro.aop.SampleMethodAdvice" />
<bean id="proxyFactoryBean"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="sampleAopBean"/>
<property name="interceptorNames">
<list>
<value>sampleMethodAdvice</value>
</list>
</property>
</bean>
</beans>
이번에는 총 3개의 Bean을 등록한다. 각각 다음과 같은 내용으로 되어 있다.
SampleAopBean
먼저 만든 Bean이다. 여기에는 id=“sampleAopBean"라고 이름을 지정해서 준비해 둔다.
SampleMethodAdvice
방금 만든 AOP 처리 클래스이다. 이것은 id=“sampleMethodAdvice"라는 이름으로 준비해 둔다.
ProxyFactoryBean
이것이 여기에서의 포인트이다. 이것은 org.springframework.aop.framework 패키지에 포함되어 있는 Spring AOP의 클래스이다. 이렇게 라이브러리에 포함되어 있는 클래스도 Bean 설정 파일에 의해 인스턴스를 자동 생성할 수 있다.
이 ProxyFactoryBean은 <property> 태그를 사용하여 2개의 프로퍼티를 추가하고 있다. 각각 다음과 같다.
target : AOP의 대상이 되는 Bean을 지정한다. 여기에서는 sampleAopBean (<bean id = “sampleAopBean”>에서 준비한 것)를 지정하고 있다.
interceptorNames : 이것은 AOP에 삽입하는 처리 Bean을 지정한다. 복수를 지정할 수 있도록 <list>라는 목록 태그를 지정하고 그 안에 <value> 태그에서 Bean 이름을 지정한다. 여기에서는 그 앞에서 만들었던 sampleMethodAdvice를 지정하고 있다.
따라서, AOP의 대상이 되는 Bean, AOP 처리을 수행하는 Bean, 그리고 이러한 관계를 속성으로 설정한 ProxyFactoryBean까지 3개가 필요하게 된다.
AOP 실행하기
자, 이제 겨우 준비가 되었다. 그러면 실제로 AOP를 사용하여 보도록 하자. com.devkuma.spring.aop 패키지에 “App"클래스를 만들고 아래와 같이 소스 코드로 작성하자.
package com.devkuma.spring.aop;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
ApplicationContext app = new ClassPathXmlApplicationContext("bean.xml");
SampleAopBean bean1 = (SampleAopBean) app.getBean("sampleAopBean");
bean1.printMessage();
System.out.println("--------------------");
SampleAopBean bean2 = (SampleAopBean) app.getBean("proxyFactoryBean");
bean2.printMessage();
}
}
실행하면 bean.xml에서 SampleAopBean를 얻어 printMessage을 실행하는데, 자세히 보면 2번 반복하고 있다.
첫 번째 SampleAopBean는 getBean(“sampleAopBean”)에서 Bean을 얻고 있다. 이것은 지금까지대로의 방식이다. 그리고 두 번째는 getBean(“proxyFactoryBean”)에서 Bean을 얻고 있다. 이것으로얻어지는 Bean이 ProxyFactoryBean처럼 생각되지만, 정확히 SampleAopBean에 캐스팅하고 SampleBean으로서 기능을 한다.
이것은 ProxyFactoryBean의 신기한 기능이다. 이 Bean은 target 속성에 지정된 Bean으로 얻어 올 수 있는 것이 가능하다는 것이다.
그럼 얻어온 SampleAopBean의 printMessage를 호출 처리는 어떤 출력을하고있는 것일까? 보면 이렇게 되어 있는 것이다.
message : [this is AOP bean!]
--------------------
* before : printMessage [com.devkuma.spring.aop.SampleAopBean@de3a06f]
message : [this is AOP bean!]
* after : printMessage [com.devkuma.spring.aop.SampleAopBean@de3a06f]
첫 번째 SampleAopBean은 단순히 printMessage의 출력이 될뿐이다. 하지만 두 번째 SampleAopBean는 printMessage의 실행 전후에 SampleMethodAdvice 클래스에 제공되는 before / afterReturning의 실행 결과가 삽입되어 있는 것을 알 수 있다. 메소드의 실행 전후에 자동으로 다른 처리가 추가되어 있는 것이다.
이것이 AOP의 위력이다. getBean로 취득하는 Bean을 ProxyFactoryBean로 하는 것은 이런 방식으로 자동 처리가 추가 될 수 있다. 불필요하게 되면 getBean 인수를 SampleAopBean으로 돌아가면 된다.
어노테이션으로 AOP 설정 클래스 생성
이것으로 기본은 알았다. 이번에는 bean.xml를 클래스에 고쳐 써 보자. Spring Framework에서는 Bean 설정 파일을 사용하지 않고, 설정을 위한 클래스으로 같은 것이 가능하다.
그럼 com.devkuma.aop 패키지 “SampleAopConfig"라는 클래스를 만들어 보자. 그리고 아래와 같이 소스 코드를 작성한다.
package com.devkuma.spring.aop;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SampleAopConfig {
private SampleAopBean sampleAopBean =
new SampleAopBean("this is message bean.");
private SampleMethodAdvice sampleMethodAdvice =
new SampleMethodAdvice();
@Bean
SampleAopBean sampleAopBean() {
return sampleAopBean;
}
@Bean
SampleMethodAdvice sampleMethodAdvice() {
return sampleMethodAdvice;
}
@Bean
ProxyFactoryBean proxyFactoryBean() {
ProxyFactoryBean bean = new ProxyFactoryBean();
bean.setTarget(sampleAopBean);
bean.setInterceptorNames("sampleMethodAdvice");
return bean;
}
}
작성을 하면 bean.xml를 사용하던 것을 SampleAopConfig 클래스를 사용하도록 App 클래스의 코드를 수정한다. 다음 문장을 바꾸면 된다.
App 수정
ApplicationContext app = new ClassPathXmlApplicationContext("bean.xml");
↓
ApplicationContext app = new AnnotationConfigApplicationContext (SampleAopConfig.class);
이것으로 App을 실행하면 방금처럼 출력이 된다. bean.xml에 기술된 것과 동일한 Bean이 SampleAopConfig에서 취 할 수 있도록 되어있는 것을 알 수 있다.
여기에서 @Configuration 어노테이션을 붙여 SampleAopConfig 클래스를 선언하고, Bean 인스턴스를 반환하는 메서드에 @Bean 어노테이션 붙여서 Bean을 취득할 수 있도록 하고 있다. 주목을 해야 하는 것을 SampleMethodAdvice 인스턴스를 작성하고 있는 sampleMethodAdvice 메소드이다. 인스턴스 작성 후에 다음과 같이 필요한 속성을 설정하고 있다.
bean.setTarget(sampleAopBean);
bean.setInterceptorNames ("sampleMethodAdvice");
이는 “setTarget"과 “setInterceptorNames"가 먼저 bean.xml에서 기술하고 있었던 <property name="target">
와 <property name="interceptorNames">
에 해당하는 처리한다는 것이다.