이 글에서는 Spring이 사용하고 있는 AOP(Aspect-Oriented Programming)에 대해서 알아볼 것이다.

Aspect-Oriented Programming을 번역하면 관점 지향 프로그래밍이다. 그렇다면 관점 지향 프로그래밍이란 무엇인지에 대해서 알아보자.


AOP(Aspect-Oriented Programming)


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

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

데이터베이스 연동 프로그램을 작성한 적이 있다면 insert 연산이든, update, delete, select 연산이든 항상 반복해서 등장하는 DB 커넥션 준비, Statement 객체 준비, DB 커넥션 연결, Statement 객체 세팅, 예외 처리, DB자원 반납등을 볼 수 있을 것이다. 여기서 데이터베이스 코드는 2가지의 관심사로 나눌 수 있다.

  • 핵심 관심사: 특정한 연산 코드에서 중복되지 않는 코드(insert, update, delete, select 연산 코드, …)
  • 횡단 관심사: 특정한 연산 코드에서 공통적으로 나타나는 코드(DB 커넥션 준비, DB자원 반납, …)

즉, 우리는 코드 = 핵심 관심사 + 횡단 관심사 라고 정의할 수 있다.

AOP를 활용해서 남자와 여자의 삶을 프로그래밍 해보자.

남자용 의사 코드

  • 열쇠로 문을 열고 집에 들어간다.
  • 컴퓨터로 게임을 한다.
  • 소등하고 잔다.
  • 자물쇠를 잠그고 집을 나선다.

여자용 의사 코드

  • 열쇠로 문을 열고 집에 들어간다.
  • 요리를 한다.
  • 소등하고 잔다.
  • 자물쇠를 잠그고 집을 나선다.

로직을 주입한다면 어디에 주입할 수 있을까? 그렇다. 객체 지향에서 로직(코드)이 있는 곳은 당연히 메서드 안쪽이다. 그럼 메서드에서 코드를 주입할 수 있는 곳은 몇 군데일까? 그림을 통해 알아보자.

위의 그림에서 볼 수 있듯이 로직을 주입할 수 있는 곳은 Around, Before, After, AfterReturning, AfterThrowing으로 총 5군데이다.

스프링 AOP를 사용하기 위해 aop패키지를 만들고 Person이라는 인터페이스를 추가하자. (스프링 AOP는 인터페이스 기반으로 작동한다.)

package aop;

public interface Person {
    void runSomething();
}

Person 인터페이스를 구현하도록 Boy.java와 Girl.java를 추가하자. 그리고 핵심 관심사 코드만 위치시키자.

package aop;

public class Boy implements Person {
    @Override
    public void runSomething(){
        System.out.println("컴퓨터로 게임을 한다.");
    }
}
package aop;

public class Girl implements Person {
    @Override
    public void runSomething() {
        System.out.println("요리를 한다.");
    }
}

이제 Spring Framework를 구동할 수 있는 Client클래스를 생성하자.

package aop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Client {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");

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

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

그리고 Spring Framework에서 AOP를 사용할 수 있도록 해주는 스프링 설정 파일(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 https://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:aspectj-autoproxy/>
    <bean id="myAspect" class="aop.MyAspect"></bean>
    <bean id="boy" class="aop.Boy"></bean>
    <bean id="girl" class="aop.Girl"></bean>
</beans>

boy와 girl Bean은 AOP 적용 대상이기 때문에 등록했고 myAspect Bean은 AOP의 Aspect이기에 등록했다.

<aop:aspectj-autoproxy/>는 스프링 프레임워크에게 AOP 프록시를 자동으로 사용하라고 알려주는 지시자 이다.

스프링 AOP는 프록시를 사용한다. 그런데 스프링 AOP에서 재미있는 것은 호출하는 쪽(romeo.runSomething() 메서드 호출)에서나 호출당하는 쪽(romeo객체), 그 누구도 프록시가 존재하는지 조차 모른다는 것이다. 오직 스프링 프레임워크만 프록시의 존재를 안다.

이제 Aspect를 활용하기 위한 MyAspect.java를 추가하자. 그리고 횡단 관심사 코드를 위치시키자.

package aop;

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

    @After("execution(* runSomething ())")
    public void after(JoinPoint joinPoint) {
        System.out.println("소등하고 잔다.");
        System.out.println("자물쇠를 잠그고 집을 나선다.");
        System.out.println();
    }
}
  • @Aspect는 해당 클래스를 AOP에서 사용하겠다는 의미이다.
  • @Before는 런타임에 대상 메서드가 실행되기 전 이 메서드를 주입하겠다는 의미이다. 대상 메서드가 실행되기 전에 주입된 메서드를 먼저 실행한다.
  • @After는 런타임에 대상 메서드가 실행되기 전 이 메서드를 주입하겠다는 의미이다. 대상 메서드가 실행되고 난 후 주입된 메서드가 실행한다.

Boy.java 코드를 주목해서 보면 횡단 관심사는 존재하지 않고 핵심 관심사만 존재한다. 즉, 확장과 유지 보수 관점에서 보면 무척이나 편한코드가 된 것을 알 수가 있다. AOP를 적용하면서 Boy.java에 단일 책임 원칙(SRP)을 자연스럽게 적용하게 된 것이다.

GGLiB 라이브러리를 사용하게 되면 인터페이스 없이도 AOP를 적용할 수 있지만 추천하는 방법은 아니다. GGLiB 라이브러리를 사용해야 할 경우는 코드를 변경할 수 없는 서드 파티 모듈 안에 final로 선언된 클래스에 AOP를 적용해야 하는 경우 정도이다.

스프링 AOP의 핵심은 아래 세 개의 문장으로 요약할 수 있다.

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

AOP(Aspect-Oriented Programming)에서 사용하는 용어


AOP 에서 사용하는 용어를 간단히 사전적 의미로 풀어보면 다음과 같다.

  • Aspect : 관점, 측면, 양상
  • Advisor : 조언자, 고문
  • Advice : 조언, 충고
  • JoinPoint : 결합점
  • Pointcut : 자르는 점

AOP에서 사용하는 용어들의 사전적 의미를 알아 보았으니 이제 직접 어떻게 쓰이는지에 대해서 알아가 보자.

Pointcut - Aspect 적용 위치 지정자!

package aop;

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

위 코드에서 * runSomething()부분이 바로 Pointcut이다.

@Before("execution(* runSomething())")의 의미는 횡단 관심사를 실행하는 메서드(public void before)를 Pointcut이 가리키는 메서드가 실행되기 전(@Before)에 실행하라는 의미이다.

즉, Pointcut은 Aspect를 적용할 타깃 메서드 및 속성의 위치를 지시하는 지시자이다.

Pointcut을 작성하는 방법은 다음과 같다.

  • [접근제한자패턴] 리턴타입패턴 [패키지&클래스패턴.]메서드이름패턴(파라미터패턴) [throws 예외패턴]
  • ex) public void aop.Boy.runSomething() -> 접근제한자가 public이고 리턴타입은 void이며 aop 패키지 밑의 Boy 클래스 안에 파라미터가 없으며 던져지는 에러가 있든 없든 이름이 runSomething인 메서드를(들을) Pointcut으로 지정하라.
  • ex) * runSomething() -> 접근제한자는 무엇이라도 좋으며(생략됨) 리턴타입도 무엇이라도 좋으며(*) 모든 패키지 밑의(생략됨) 모든 클래스 안의(생략됨) 파라미터가 없으며 던져지는 에러가 있든 없든 이름이 runSomething인 메서드를(들을) Pointcut으로 지정하라.

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

JoinPoint - 연결 가능한 지점!

Pointcut의 후보가 되는 모든 메서드들이 JoinPoint이다. 즉 Aspect 적용이 가능한 지점이 된다.

한마디로 JoinPoint란 Aspect 적용이 가능한 모든 지점을 말한다.

스프링 AOP에서 JoinPoint란 스프링 프레임워크가 관리하는 Bean의 모든 메서드에 해당한다. 이것이 광의의 JoinPoint이다.

협의의 JointPoint는 예제 코드를 통해서 알아보자.

package aop;

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

위의 코드에서 표시된 JoinPoint는 무엇일까? romeo.runSomething() 메서드를 호출한 상태라면 JoinPoint는 romeo 객체의 runSomething() 메서드가 되고 juliet.runSomething() 메서드를 호출한 상태라면 JoinPoint는 juliet 객체의 runSomething() 메서드가 된다.

JointPoint에 대해서 정리하면 다음과 같다.

  • 광의의 JoinPoint란 Aspect 적용이 가능한 모든 지점이다.
  • 협의의 JoinPoint란 호출된 객체의 메서드이다.

Advice - 언제, 무엇을!

Advice란 Pointcut에 언제, 무엇을 적용할지 정의한 메서드이다.

예제를 통해 알아보자.

package aop;

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

위의 예제에서는 @Before 애노테이션과 before() 메서드가 Advice라고 할 수 있다.

이러한 이유는 Pointcut인 * runSomething ()가 시작되기 전(@Before)에 before() 메서드를 실행하라고 정의되어 있기 때문입니다.

Aspect - Advisor의 집합체!!

AOP에서 Aspect는 여러 개의 Advice와 여러 개의 Pointcut의 결합체를 의미하는 용어이다.

수식으로 표현하면 Aspect = Advice들 + Pointcut들 이다.

Advice는 [언제(When), 무엇을(What)]를 의미하는 것이었다. Pointcut은 [어디에(Where)]를 의미하는 것이었다. 결국 Aspect는 When + Where + What(언제, 어디서, 무엇을)이 된다.

Advisor - 어디서 언제 무엇을!

Advisor는 다음과 같은 수식으로 표현할 수 있다.

Advisor = 한 개의 Advice + 한 개의 Pointcut

Advisor는 스프링 AOP에서만 사용하는 용어이며 다른 AOP 프레임워크에서는 사용하지 않는다. 스프링이 발전하면서 다수의 Advice와 다수의 Pointcut을 다양하게 조합해서 사용할 수 있는 방법인 Aspect가 나왔기 때문에 스프링은 Advisor를 사용하지 않을것을 권고한다.


POJO와 XML 기반의 AOP


처음 단락에서 제시된 Spring과 Annotation 기반의 AOP 예제 코드를 POJO와 XML 기반의 AOP로 변경한 코드는 다음과 같다.

변경되는 파일은 MyAspect.javaaop.xml 파일이이다.

변경된 MyAspect.java파일은 다음과 같다.

package aop;

import org.aspectj.lang.JoinPoint;

// MyAspect 클래스는 Spring Framework에 의존하지 않는 POJO가 된다.
public class MyAspect {
    public void before(JoinPoint joinPoint) {
        System.out.println("열쇠로 문을 열고 집에 들어간다.");
    }

    public void after(JoinPoint joinPoint) {
        System.out.println("소등하고 잔다.");
        System.out.println("자물쇠를 잠그고 집을 나선다.");
        System.out.println();
    }
}

변경된 aop.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 https://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:aspectj-autoproxy/>
    <bean id="myAspect" class="aop.MyAspect"></bean>
    <bean id="boy" class="aop.Boy"></bean>
    <bean id="girl" class="aop.Girl"></bean>

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

참조 : 스프링 입문을 위한 자바 객체 지향의 원리와 이해(저자: 김종민)