리셋 되지 말자

AOP(Aspect-Oriented Programming) 본문

Java(폐지)/spring 책

AOP(Aspect-Oriented Programming)

kyeongjun-dev 2020. 8. 13. 10:47

관점 지향 프로그래밍

스프링 DI가 의존성(new)에 대한 주입이라면 스프링 AOP는 로직(code) 주입이라고 할 수 있다.

횡단 관심사와 핵심 관심사

  1. 횡단 관심사 예시1

입금, 출금, 이체 모듈에서 로깅, 보안, 트랜잭션 기능이 반복적으로 나타나는 것을 볼 수 있다.
프로그램을 작성하다 보면 이처럼 다수의 모듈에 공통적으로 나타나는 부분이 존재하는데, 바로 이것을
횡단 관심사라고 한다.

  1. 회당 관심사 예시2 - DB 연동 프로그램
DB 커넥션 준비
Statement 객체 준비

try{
    DB 커넥션 연결
    Statement 객체 세팅
    insert / update / delete / select 실행
} catch ... {
    //예외처리
} catch ... {
    //예외처리
} finaly {
    DB 자원 반납
}

위의 의사코드를 보면 'insert / update / delete / select'를 핵심 관심사 라고 하고 나머지 부분은 어떤 데이터베이스 연산을 하든 공통적으로 나타나기에 횡단 관심사 라고 한다.

코드 = 핵심 관심사 + 횡단 관심사

핵심 관심사는 모듈별로 다르지만 횡단 관심사는 모듈별로 반복되어 중복해서 나타나는 부분이다.
반복/중복이면 분리한다가 정석일텐데 AOP에서는 더욱 진보된 방법을 사용한다고 한다.

예시 - 남자와 여자의 삶

남자용 의사 코드
열쇠로 문을 열고 집에 들어간다.
컴퓨터로 게임을 한다.
소등하고 잔다.
문을 잠그고 집을 나선다.


예외상황처리: 집에 불남 - 119에 신고한다.

여자용 의사 코드
열쇠로 문을 열고 집에 들어간다.
요리를 한다.
소등하고 잔다.
문을 잠그고 집을 나선다.


예외상황처리: 집에 불남 - 119에 신고한다.

클래스 작성 - aop001

  • Boy.java
package aop001;

public class Boy {
    public void runSomething() {
        System.out.println("열쇠로 문을 열고 집에 들어간다.");

        try {
            System.out.println("컴퓨터로 게임을 한다.");
        }catch(Exception e) {
            if(e.getMessage().equals("집에 불남")) {
                System.out.println("119에 신고한다.");
            }
        }finally {
            System.out.println("소등하고 잔다.");
        }

        System.out.println("자물쇠를 잠그고 집을 나선다.");
    }
}
  • Girl.java
package aop001;

public class Girl {
    public void runSomething() {
        System.out.println("열쇠로 문을 열고 집에 들어간다.");

        try {
            System.out.println("컴퓨터로 게임을 한다.");
        }catch(Exception e) {
            if(e.getMessage().equals("집에 불남")) {
                System.out.println("119에 신고한다.");
            }
        }finally {
            System.out.println("소등하고 잔다.");
        }

        System.out.println("자물쇠를 잠그고 집을 나선다.");
    }
}
  • Start.java
package aop001;

public class Start {
    public static void main(String[] args) {
        Boy romeo = new Boy();
        Girl juliet = new Girl();

        romeo.runSomething();
        juliet.runSomething();
    }
}

AOP의 로직을 주입하는 곳

객체 지향에서 로직(코드)가 있는 곳은 메서드의 안쪽이다.
메서드에서 코드를 주입할 수 있는 곳은 아래 그림과 같이 Around, Before, After, AfterReturning, After Throwing 총 다섯 군데이다.

메서드 시작 전은 메서드 시작 직후, 메서드 종료 후는 메서드 종료 직전이라고 이해해도 된다.

일단 덤벼보자 실전편 - aop002


  • Person.java
package aop002;

public interface Person {
    public abstract void runSomething();
}
  • Boy.java (횡단 관심사 모두 제거)
package aop002;

public class Boy implements Person{
    public void runSomething() {
        System.out.println("컴퓨터로 게임을 한다.");
    }
}
  • Start.java
package aop002;

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/app02.xml");

        Person romeo = context.getBean("boy", Person.class);

        romeo.runSomething();
    }
}
  • aop002.xml (Namespace 탭에서 aop 체크)
<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">


    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    <bean id="myAspect" class="aop002.MyAspect"></bean>
    <bean id="boy" class="aop002.Boy"></bean>

</beans>
  • MyAspect.java
package aop002;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@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는 대상 메서드 실행 전에 이 메서드를 실행하겠다는 의미다.
    JoinPoint는 @Before에서 선언된 메서드인 aop002.Boy.runSomething()을 의미한다.

이제 열쇠로 문을 열고 집에 들어가는 것이 아니라 스프링 프레임워크가 사용자를 인식해 자동으로 문을 열어주게 된다.
Start.java에서 실행을 한다.

INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@6bc168e5: startup date [Thu Aug 13 11:33:58 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [aop002/aop002.xml]
WARN : org.springframework.context.support.ClassPathXmlApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'boy' defined in class path resource [aop002/aop002.xml]: BeanPostProcessor before instantiation of bean failed; nested exception is java.lang.NoClassDefFoundError: org/aspectj/weaver/reflect/ReflectionWorld$ReflectionWorldException
...

위와같은 에러가 발생하면, pom.xml에 아래와 같은 aspectjweaver를 추가한다.

        <!-- AspectJ -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>${org.aspectj-version}</version>
        </dependency>    
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>
  • 실행 결과
INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@2e5c649: startup date [Thu Aug 13 11:37:29 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [aop002/aop002.xml]
얼굴 인식 확인: 문을 개방하라
컴퓨터로 게임을 한다.

일단 덤벼보자 설명편


AOP 적용 전후의 Boy.java와 관련 코드를 비교해본다.

System.out.println("열쇠로 문을 열고 집에 들어간다") 만 AOP로 구현했음에도 코드의 양이 상당히 늘었났다. 하지만 주목할 것은 Boy.java의 코드의 변화이다.
횡단 관심사가 모두 사라졌고 오직 핵심 관심사만 남았다. = 유지보수가 편해졌다.
AOP를 적용하면서 Boy.java에 단일 책임 원칙(SRP)을 자연스럽게 적용하게 된 것이다.

실행 결과를 보면 @Before로 만들어진 before메서드가 런타임에 위의 그림과 같이 주입되는 것을 확인할 수 있다. 더욱 상세이 줄긋기를 해보면 아래 그림과 같다.

@Before("execution(public void aop002.Boy.runSomething())")

이 부분을 아래와 같이 수정해본다.

@Before("execution(* runSomething())")

똑같이 수행이 되는것을 확인할 수 있다. 이는 Girl.java의 runSomething() 메서드도 @Before를 통해 같은 로직을 주입해 줄 수 있다는 것을 의미한다.

  • AOP 전후의 Boy.java, Girl.java 코드

Boy.java와 Girl.java의 초록생 부분은 횡단 관심사이기 때문에 사라졌다.
붉은색 부분이 추가된 이유는 스프링 AOP가 인터페이스 기반으로 작동하기 때문에 그 요건을 충족하기 위해서다. (CGLiB 라이브러리를 사용하게 되면 인터페이스 없이도 AOP를 적용할 수 있지만 추천하는 방법은 아니다. CGLiB 라이브러리를 사용해야 할 경우는 코드를 변경할 수 없는 서드 파티 모듈 안에 final로 선언된 클래스에 AOP를 적용해야 하는 경우 정도다.)

Start.java에서 굵게 표시된 부분은 스프링 프레임워크를 적용하고 활용하는 부분이다.

Person.java는 스프링 AOP가 인터페이스기반이기 때문에 새롭게 생성되었다.

MyAspect.java는 Boy.java와 Girl.java에서 공통적으로 나타나는 횡단 관심사를 모두 삭제했지만 결국 누군가는 횡단 관심사를 처리해야 하는데 이 역할을 MyAspect.java가 맡게 된다.

aop002.xml 살펴보기

3개의 bean이 등록되어 있는데 boy 빈과 girl 빈은 AOP 적용 대상이라서 등록할 필요가 있고 myAspect 빈은 AOP의 Aspect이기에 등록할 필요가 있다.

aop:aspectj=autoproxy/에서 j는 자바인것 같고 auto는 자동, proxy는 프록시 패턴을 이용해 횡단 관심사를 핵심 관심사에 주입하는 것이다.

프록시 없이 직접 호출
프록시를 이용한 간접 호출

호출하는 쪽에서 romeo.runSomething() 메서드를 호출하면 프록시가 그 요청을 받아 진짜 romeo 객체에게 요청을 전달한다. 그런데 그림 하단 중앙의 runSomething() 메서드는 호출을 그냥 전달만 하는것이 아니라 주고받는 내용을 감시하거나 조작할 수 있다.

스프링 AOP는 프록시를 사용한다. 그런데 스프링 AOP에서 재밌는점은 호출하는 쪽(romeo.runSomething() 메서드 호출)에서나 호출 당하는 쪽(romeo객체), 그 어느쪽도 프록시가 존재하는지조차 모른다는 것이다. 오직 스프링 프레임워크만이 프록시의 존재를 안다. 사실 버퍼도 일종의 프록시이며 네트워크 속도를 햐상시키는 캐시 서버도 프록시의 한 예이다.

결국 aop:aspectj-autoproxy/는 스프링 프레임워크에게 AOP 프록시를 사용하라고 알려주는 지시자인 것이다. 게다가 auto가 붙었으니 자동으로 말이다.

스프링 AOP의 핵심

  1. 스프링 AOP는 인터페이스(interface) 기반이다.
  2. 스프링 AOP는 프록시(proxy) 기반이다.
  3. 스프링 AOP는 런타임(runtime) 기반이다.

일단 덤벼 보자 용어편


용어 영한 사전
Aspect 관점, 측면, 양상
Advisor 조언자, 고문
Advice 충고, 조언
JoinPoint 결합점
Pointcut 자르는 점

Pointcut - 자르는 지점? Aspect 적용 위치 지정자

package aop002;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@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("열쇠로 문을 열고 집에 들어간다.");
    }
}

MyAspect.java의 '* runSomething()' 이 바로 Pointcut이다.
@Before("execution(* runSomething())")의 의미는 지금 선언하고 있는 메서드(public void before)를 '* run something'메서드가 실행되기 전(@Before)에 실행하라는 의미다. 여기서 public void before는 횡단 관심사를 실행하는 메서드가 된다.

즉 Pointcut은 횡단 관심사를 적용할 타깃 메서드를 선택하는 지시자(메서드 선택필터)인 것이다.

'Aspect 위치 지정자'라고 한 이유

스프링의 AOP만 보면 Aspect를 메서드에만 적용할 수 있으니 '타깃 메서드 지정자'라는 말이 틀리지 않다. 하지만 AspectJ처럼 스프링 AOP 이전부터 있었고 지금도 유용하게 사용되는 다른 AOP 프레임워크에서는 메서드뿐만 아니라 속성 등에도 Aspect를 적용할 수 있기에 그것들까지 고려한다면 Aspect 적용 취치 지정자(지시자)가 맞는 표현이다.
Pointcut을 메서드 선정 알고리즘이라고도 한다.

타깃 메서드 지정자에는 익히 알려진 정규식과 AspectJ 표현식 등을 사용할 수 있다. 간단히 소개하자면 아래와 같다.

[접근제한자패턴]리턴타입패턴[패키지&클래스패턴]메서드이름패턴(파라미터팬턴)[throws 예외패턴]

대괄호는 생략이 가능하다는 의미이다. 필수 요소는 리턴 타입 패턴, 메서드 이름 패턴, 파라미터 패턴뿐이다.

위에서 사용했던 Pointcut

  1. public void aop002.BoyrunSomething()
    접근 제한자가 public이고
    리턴타입은 void이며
    aop002 패키지 밑의
    Boy 클래스 안에
    파라미터가 없으며
    던져지는 에러가 있든 없든
    이름이 runSomething인 메서드를(들을)
    Pointcut으로 지정하라.

  2. *runSomething()
    접근제한자는 무엇이라도 좋으며(생략됨)
    리턴타입도 무엇이라도 좋으며(*)
    모든 패키지 밑의(생략됨)
    모든 클래스 안에(생략됨)
    파라미터가 없으며
    던져지는 에러가 있든 없든
    이름이 runSomething인 메서드를(들을)
    Pointcut으로 지정하라

더 자세한 Pointcut 표현식은 다른 책이나 스프링 API 문서를 참고

JoinPoint - 연결점? 연결 가능한 지점

Pointcut은 JoinPoint의 부분 집합이다. 위에서 스프링 AOP는 인터페이스를 기반으로 한다고 설명했다. 인터페이스는 추상 메서드의 집합체다. 그럼 삼단 논법에 의해 스프링 AOP는 메서드에만 적용 가능하다는 결론에 도달한다.

Poincut의 후보가 되는 모든 메서드들이 JoinPoint, 즉 Aspect 적용이 가능한 지점이 된다. 그래서 다시 삼단 논법에 의해 JoinPoint란 "Aspect 적용이 가능한 모든 지점을 말한다" 라고 결론지을 수 있다. 따라서 Aspect를 적용할 수 있는 지점 중 일부가 Poincut이 되므로 Point은 JoinPoint의 부분집합인 셈이다.

스프링 AOP에서 JoinPoint란 스프링 프레임워크가 관리하는 빈의 모든 메서드에 해당한다. 이를 넓은 범위츼 JoinPoint 라고 한다. 좁은 의미의 JoinPoint는 코드 상에서 확인할 수 있다.

  • MyAspect.java
package aop002;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@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("열쇠로 문을 열고 집에 들어간다.");
    }
}

위 코드에서 'JoinPoint joinPoint'의 실체는 그때그때 다르다. (Start.java의 내용 참고)

romeo.runSomething() 메서드를 호출한 상태라면 JoinPoint는 romeo 객체의 runSomething() 메서드가 된다.
juliet.runSomething() 메서드를 호출한 상태라면 JoinPoint는 juliet 객체의 runSomething() 메서드가 된다.

JoinPoint 파라미터를 이용하면 실행 시점에 실제 호출된 메서드가 무엇인지, 실제 호출된 메서드를 소유한 객체가 무엇인지, 또 호출된 메서드의 파라미터는 무엇인지 등의 정보를 확인할 수 있다.

  • 넓은 범위의 JoinPoint란 Aspect 적용이 가능한 모든 지점이다.
  • 좁은 범위의 JoinPoint란 호출된 객체의 메서드다.

Advice - 조언? 언제, 무엇을

Advice는 pointcut에 적용할 로직, 즉 메서드를 의미한다. 여기에 더해 언제라는 개념까지 포함하고 있다.
결국 Advice란 Poincut에 언제, 무엇을 적용할지 정의한 메서드다. Advice를 타깃 객체의 타깃메서드에 적용될 부가 기능이라고 표현한 책도 있다.

  • MyAspect.java
package aop002;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@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("열쇠로 문을 열고 집에 들어간다.");
    }
}

MyAspect.java 코드를 보면 지정된 Pointcut이 시작되기 전(@Before)에 before() 메서드를 실행하라고 돼있음을 확인할 수 있다.

Aspect - 관점? 측면? Advisor의 집합체

AOP에서 Aspect는 여러 개의 Advice와 여러 개의 Pointcut의 결합체를 의미하는 용어다. 수식으로 표현하면 아래와 같다.

Aspect = Advice들 + Pointcut들

Advice는 언제(When), 무엇을(What)을 의미하는 것이었다. Pointcut은 어디에(Where)를 의미하는 것이었다.
결국 Aspect는 When + Where + What(언제, 어디에, 무엇을)이 된다. 위의 MyAspect.java를 기반으로 해석해보면
Pointcut인 public void aop002.Boy.runSomething() 메서드가 시작되기 전(@Before)에 before() 메서드를 실행하라고 되어 있는 것을 볼 수 있다.

Advisor - 조언자? 어디서, 언제, 무엇을

Advisor는 아래와 같은 수식으로 표현할 수 있다.

Advisor = 한 개의 Advice + 한 개의 Pointcut

Advisor는 스프링 AOP에서만 사용하는 용어이며 다른 AOP 프레임워크에서는 사용하지 않는다.
여러개의 Advice와 여러개의 Pointcut을 다양하게 조합해서 사용하는 방법인 Aspect가 나왔기 때문에 점점 사용하지 말라고 하는 추세이다.

아래에서는 이전에 작성한 코드를 어노테이션 없이 POJO와 XML 설정 기반으로 변경하면서 앞에서 설명한 개념을 하나씩 살펴본다.

일단 덤벼 보자 - POJO와 XML 기반 AOP


  • MyAspect.java
package aop003;

import org.aspectj.lang.JoinPoint;

public class MyAspect {
    public void before(JoinPoint joinPoint) {
        System.out.println("얼굴 인식 확인: 문을 개방하라");
        //System.out.println("열쇠로 문을 열고 집에 들어간다.");
    }
}

기존 코드와 변경된 코드를 비교하면 @Aspect 어노테이션과 @Before 어노테이션이 사라졌다.
이로써 aop003 패키지의 MyAspect.java는 스프링 프레임워크에 의존하지 않는 POJO가 된다.

  • aop003.xml (aop 관련 태그 추가)
<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">

    <bean id="boy" class="aop003.Boy"></bean>
    <bean id="girl" class="aop003.Girl"></bean>
    <bean id="myAspect" class="aop003.MyAspect"></bean>

    <aop:config>
        <aop:aspect ref="myAspect">
            <aop:before method="before" pointcut="execution(* runSomething())"></aop:before>
        </aop:aspect>
    </aop:config>
</beans>

  • 실행 결과
INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@2e5c649: startup date [Thu Aug 13 15:58:41 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [aop003/aop003.xml]
얼굴 인식 확인: 문을 개방하라
컴퓨터로 게임을 한다.
얼굴 인식 확인: 문을 개방하라
요리를 한다.

AOP 기초 완성

  • After 어드바이스
    After 어드바이스는 해당 JoinPoint 메서드를 실행한 후에 실행된다.

  • MyAspect.java (lockDoor 메서드 추가)

package aop004;

import org.aspectj.lang.JoinPoint;

public class MyAspect {
    public void before(JoinPoint joinPoint) {
        System.out.println("얼굴 인식 확인: 문을 개방하라");
        //System.out.println("열쇠로 문을 열고 집에 들어간다.");
    }

    public void lockDoor(JoinPoint joinPoint) {
        System.out.println("주인님 나갔다: 문 잠가");
    }
}
  • aop004.xml (after 어드바이스 추가)
<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">

    <bean id="boy" class="aop004.Boy"></bean>
    <bean id="girl" class="aop004.Girl"></bean>
    <bean id="myAspect" class="aop004.MyAspect"></bean>

    <aop:config>
        <aop:aspect ref="myAspect">
            <aop:before method="before" pointcut="execution(* runSomething())"></aop:before>
            <aop:after method="lockDoor" pointcut="execution(* runSomething())"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>
  • 실행 결과
INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@2e5c649: startup date [Thu Aug 13 16:21:04 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [aop004/aop004.xml]
얼굴 인식 확인: 문을 개방하라
컴퓨터로 게임을 한다.
주인님 나갔다: 문 잠가
얼굴 인식 확인: 문을 개방하라
요리를 한다.
주인님 나갔다: 문 잠가

어노테이션 기반 소스코드(aop005 패키지 새롭게 생성함)

  • MyAspect.java
package aop005;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class MyAspect {
    @Before("execution(* runSomething())")
    public void before(JoinPoint joinPoint) {
        System.out.println("얼굴 인식 확인: 문을 개방하라");
        //System.out.println("열쇠로 문을 열고 집에 들어간다.");
    }

    @After("execution(* runSomething())")
    public void lockDoor(JoinPoint joinPoint) {
        System.out.println("주인님 나갔다: 문 잠가");
    }
}
  • aop005.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"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    <bean id="boy" class="aop005.Boy"></bean>
    <bean id="girl" class="aop005.Girl"></bean>
    <bean id="myAspect" class="aop005.MyAspect"></bean>
</beans>
  • 실행 결과(aop004랑 동일함)
INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@2e5c649: startup date [Thu Aug 13 16:51:44 KST 2020]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [aop005/aop005.xml]
얼굴 인식 확인: 문을 개방하라
컴퓨터로 게임을 한다.
주인님 나갔다: 문 잠가
얼굴 인식 확인: 문을 개방하라
요리를 한다.
주인님 나갔다: 문 잠가

lockDoor() 메서드의 인자는 JoinPoint 객체 하나인데, 원한다면 대상 메서드인 runSomething()의 반환값도 인자로 받을 수 있고, 프록시를 통해 최종 반환값을 조작하는 것도 가능하다. 자세한 내용은 관련 서적을 참고하기 바람(젠장...)

XML 설정 파일 기반인 aop004.xml의 중복되는 부분 수정

  • aop004.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"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">

    <bean id="boy" class="aop004.Boy"></bean>
    <bean id="girl" class="aop004.Girl"></bean>
    <bean id="myAspect" class="aop004.MyAspect"></bean>

    <aop:config>
        <aop:pointcut expression="execution(* runSomething())" id="iampc"/>
        <aop:aspect ref="myAspect">
            <aop:before method="before" pointcut-ref="iampc"></aop:before>
            <aop:after method="lockDoor" pointcut-ref="iampc"></aop:after>
        </aop:aspect>
    </aop:config>
</beans>

어노테이션 기반의 aop005 수정 (aop006 새롭게 생성)

  • MyAspect.java
package aop006;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAspect {
    @Pointcut("execution(* runSomething())")
    private void iampc() {
        // 여긴 무엇을 작성해도 의미가 없어요
    }

    @Before("iampc()")
    public void before(JoinPoint joinPoint) {
        System.out.println("얼굴 인식 확인: 문을 개방하라");
        //System.out.println("열쇠로 문을 열고 집에 들어간다.");
    }

    @After("iampc()")
    public void lockDoor(JoinPoint joinPoint) {
        System.out.println("주인님 나갔다: 문 잠가");
    }
}

스프링 AOP를 적용할 수 있는 시점은 Before, After 외에도 세 가지 AfterReturning, AfterThrowing, Around가 있는데 Around만 빼고는 대동 소이하다. 이와 관련된 내용 역시 전문 서적을 통해 살펴보기를 바란다.(젠장...)

Comments