AOP는 Aspect-Oriented Programming
약자이고 관점 지향 프로그래밍
이다. 스프링 DI가 의존성(new)에 대한 주입이라면 스프링 AOP는 로직(Code) 주입이라고 볼 수 있다. 프로그램에는 다수의 모듈에 공통적으로 나타나는 부분이 존재하고, 이것을 횡단 관심사(cross-cutting concern)라고 한다. 프로그래머는 반복/중복 부분을 분리해서 한 곳에서 관리해야 한다. 그런데 AOP는 더욱 진보된 방법을 사용한다.
아래 의사코드를 보며 반복/중복된 부분을 분리해보자.
열쇠로 문을 열고 집에 들어간다.
컴퓨터로 게임을 한다.
소등하고 잔다.
자물쇠를 잠그고 집을 나선다.
---------------------------
예외상황처리 : 집에 불남 - 119에 신고한다.
열쇠로 문열 열고 집에 들어간다.
요리를 한다.
소등하고 잔다.
자물쇠를 잠그고 집을 나선다.
---------------------------
예외상황처리 : 집에 불남 - 119에 신고한다.
"스프링 AOP는 로직 주입이라고 볼 수 있다"고 설명하였다. 객체 지향에서 로직(코드)이 있는 곳은 메서드 안쪽이며 메서드에서 코드를 주입할 수 있는 곳은 5곳이다.
스프링 AOP를 사용해 두 의사 코드에서 어떻게 횡단 관심사를 분리할 수 있는지, 분리된 횡단 관심사(로직)를 어떻게 실행 시간에 메서드에 주입할 수 있는지 알아보겠다.
public interface Person {
void runSomething();
}
Person 인터페이스를 구현하도록 Boy.java를 구현하겠다. 이 때 횡단 관심사를 모두 지우겠다.
package aop002;
public class Boy implements Person{
@Override
public void runSomething() {
System.out.println("컴퓨터로 게임을 한다.");
}
}
스프링 프레임워크 기반에서 작동할 수 있도록 Start.java
를 구현한다.
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Start {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("aop002/aop002.xml");
Person romeo = context.getBean("boy", Person.class);
romeo.runSomething();
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/beans
http://ww.springframework.org/schema/beans/spring-beans.xsd">
<aop:aspectj-autoproxy/>
<bean id="myAspect" class="aop002.MyAspect" />
<bean id="boy" class="aop002.Boy" />
</beans>
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.JoinPoint;
@Aspect
public class MyAspect {
@Before("execution(public void aop002.Boy.runSomething())")
public void before(Joinpoint joinpoint){
System.out.println("얼굴 인식 확인: 문을 개방하라");
// System.out.println("열쇠로 문을 열고 집에 들어간다.");
}
}
@Aspect
: 이 클래스를 이제 AOP에서 사용하겠다는 의미
@Before
: 대상 메서드 실행 전에 이 메서드를 실행하겠다는 의미
public void before(Joinpoint joinpoint)
: JoinPoint는 @Before
에서 선언된 메서드인 aop002.Boy.runSomething()
을 의미한다.
얼굴 인식 확인: 문을 개방하라
컴퓨터로 게임을 한다.
AOP 적용 전후의 Boy.java
코드가 어떻게 변하였는지 비교해보겠다.
public class Boy {
public void runSomething(){
System.out.println("열쇠로 문을 열고 집에 들어간다.");
try{
System.out.println("컴퓨터로 게임을 한다.");
} catch (Exception ex){
if(ex.getMessage().equals("집에 불남")){
System.out.println("119에 신고한다.");
}
} finally {
System.out.println("소등하고 잔다.");
}
System.out.println("자물쇠를 잠그고 집을 나선다.");
}
}
public class Boy implements Person{
@Override
public void runSomething() {
System.out.println("컴퓨터로 게임을 한다.");
}
}
단순히 Boy.java
파일만 비교했을 때는 확인히 코드가 줄었음을 알 수 있다. 횡단 관심사는 모두 사라졌고 오직 핵심 관심사만 남았기 때문이다. 그런데 AOP
를 적용하면서 Boy.java
파일을 interface
, Aspect
, XML
, Boy.java
4개의 파일로 분할했으니 더 복잡해진 것이 아닌가 싶기도 하다. 하지만 추가 개발과 유지보수 관점에서 보면 더 편한 코드가 되었고 Boy.java
에 단일 책임 원칙(SRP)을 자연스럽게 적용하게 되었다.
얼굴 인식 확인: 문을 개방하라
컴퓨터로 게임을 한다.
실행 결과를 보면 MyAspect
파일의 @Before
로 만들어진 before
메서드가 런타임에 runSomething
메서드에 주입된다.
@Before("execution(public void aop002.Boy.runSomething())")
public void before(JoinPoint joinpoint){
System.out.println("얼굴 인식 확인: 문을 개방하라");
// System.out.println("열쇠로 문을 열고 집에 들어간다.");
}
@Before("execution(public void aop002.Boy.runSomething())")
코드를 @Before("execution(* runSomething())")
코드로 변경해도 정상적으로 작동한다. 이는 Boy.java
의 runSomething()
메서드만 사용할 수 있던 것을 Girl.java
의 runSomething()
메서드도 @Before
를 사용해 같은 로직을 주입할 수 있다는 의미이다. 너무 놀랍지 않은가?
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.JoinPoint;
@Aspect
public class MyAspect {
// @Before("execution(public void aop002.Boy.runSomething())")
@Before("execution(* runSomething())")
public void before(JoinPoint joinpoint){
System.out.println("얼굴 인식 확인: 문을 개방하라");
// System.out.println("열쇠로 문을 열고 집에 들어간다.");
}
}
public class Boy {
public void runSomething(){
System.out.println("열쇠로 문을 열고 집에 들어간다.");
try{
System.out.println("컴퓨터로 게임을 한다.");
} catch (Exception ex){
if(ex.getMessage().equals("집에 불남")){
System.out.println("119에 신고한다.");
}
} finally {
System.out.println("소등하고 잔다.");
}
System.out.println("자물쇠를 잠그고 집을 나선다.");
}
}
public class Girl {
public void runSomething(){
System.out.println("열쇠로 문을 열고 집에 들어간다.");
try{
System.out.println("요리를 한다.");
} catch (Exception ex){
if (ex.getMessage().equals("집에 불남")){
System.out.println("119에 신고한다.");
}
} finally {
System.out.println("소등하고 잔다.");
}
System.out.println("자물쇠를 잠그고 집을 나선다.");
}
}
public class Start {
public static void main(String[] args) {
Boy romeo = new Boy();
Girl juliet = new Girl();
romeo.runSomething();
juliet.runSomething();
}
}
public interface Person {
void runSomething();
}
public class Boy implements Person{
@Override
public void runSomething() {
System.out.println("컴퓨터로 게임을 한다.");
}
}
public class Girl implements Person{
public void runSomething() {
System.out.println("요리를 한다.");
}
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Start {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("aop002.xml");
Person romeo = context.getBean("boy", Person.class);
Person juliet = context.getBean("girl", Person.class);
romeo.runSomething();
juliet.runSomething();
}
}
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.JoinPoint;
@Aspect
public class MyAspect {
// @Before("execution(public void aop002.Boy.runSomething())")
@Before("execution(* runSomething())")
public void before(JoinPoint joinpoint){
System.out.println("얼굴 인식 확인: 문을 개방하라");
// System.out.println("열쇠로 문을 열고 집에 들어간다.");
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<aop:aspectj-autoproxy />
<!--<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" /> -->
<bean id="myAspect" class="aop002.MyAspect" />
<bean id="boy" class="aop002.Boy" />
<bean id="girl" class="aop002.Girl" />
</beans>
Boy.java
와 Girl.java
에서 System.out.println("컴퓨터로 게임을 한다.");
, System.out.println("요리를 한다.");
메서드를 제외하고 나머지 코드가 사라진 이유는 횡단 관심사이기 때문이다.
스프링 AOP는 인터페이스 기반으로 작동하므로 Person
인터페이스를 추가하였다.
아래 코드는 Start.java
에서 스프링 프레임워크를 적용하고 활용하는 부분이다.
```java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Start {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("aop002.xml");
Person romeo = context.getBean("boy", Person.class);
Person juliet = context.getBean("girl", Person.class);
}
}
MyAspect.java
는 왜 필요할까? Boy.java
와 Girl.java
에서 공통적으로 나타나는 횡단 관심사를 모두 삭제 했지만 누군가는 횡단 관심사를 처리해야 한다. 따라서 MyAspect.java
가 횡단 관심사를 처리한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<aop:aspectj-autoproxy />
<!--<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator" /> -->
<bean id="myAspect" class="aop002.MyAspect" />
<bean id="boy" class="aop002.Boy" />
<bean id="girl" class="aop002.Girl" />
</beans>
myAspect
, boy
, girl
세 개의 빈이 설정돼 있다. 빈이 설정되는 이유는 객체 생성과 의존성 주입을 스프링 프레임워크에 위임하기 위해서다. 스프링 프레임워크는 객체 생성뿐 아니라 객체 생명주기 전반에 걸쳐 빈 생성에서 소멸까지 관리한다. boy
빈과 girl
빈은 AOP
적용 대상이기에 등록해야 하고 myAspect
빈은 AOP
의 Aspect
이니까 등록해야 한다.
<aop:aspectj-autoproxy />
는 프록시 패턴을 이용해 횡단 관심사를 핵심 관심사에 주입하는 것이다.
romeo.runSomething();
메서드를 호출하면 프록시가 요청을 받아 진짜 romeo 객체에게 요청을 전달한다. 그런데 중앙 빨간색 메서드 (그림에서는 homework())인 runSomething()
메서드는 전달을 넘어 주고받는 내용을 감시, 조작할 수 있다. 스프링 AOP는 호출하는 쪽(romeo.runSomething)에서나 호출 당하는 쪽(romeo 객체), 모두 프록시가 존재하는지조차 모른다. 오직 스프링 프레임워크만 프록시 존재를 안다. 그래서 <aop:aspectj-autoproxy />
는 스프링 프레임워크에게 AOP 프록시를 자동으로 사용하라고 명령하는 지시자이다.
스프링 AOP를 3개의 문장으로 요약하면 아래와 같다.
스프링 입문을 위한 자바 객체지향의 원리와 이해