AOP

심야·2022년 11월 23일
0

웹 개발

목록 보기
27/47

AOP

AOP는 Aspect-Oriented Programming 약자이고 관점 지향 프로그래밍이다. 스프링 DI가 의존성(new)에 대한 주입이라면 스프링 AOP는 로직(Code) 주입이라고 볼 수 있다. 프로그램에는 다수의 모듈에 공통적으로 나타나는 부분이 존재하고, 이것을 횡단 관심사(cross-cutting concern)라고 한다. 프로그래머는 반복/중복 부분을 분리해서 한 곳에서 관리해야 한다. 그런데 AOP는 더욱 진보된 방법을 사용한다.

아래 의사코드를 보며 반복/중복된 부분을 분리해보자.

남자 의사 코드

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

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

여자 의사 코드

열쇠로 문열 열고 집에 들어간다.
요리를 한다.
소등하고 잔다.
자물쇠를 잠그고 집을 나선다.
---------------------------
예외상황처리 : 집에 불남 - 119에 신고한다.

"스프링 AOP는 로직 주입이라고 볼 수 있다"고 설명하였다. 객체 지향에서 로직(코드)이 있는 곳은 메서드 안쪽이며 메서드에서 코드를 주입할 수 있는 곳은 5곳이다.

  1. 메서드 전 구역
  2. 메서드 시작 직후
  3. 메서드 종료 직전
  4. 메서드 정상 종료 후
  5. 메서드에서 예외가 발생하면서 종료된 후

스프링 AOP를 사용해 두 의사 코드에서 어떻게 횡단 관심사를 분리할 수 있는지, 분리된 횡단 관심사(로직)를 어떻게 실행 시간에 메서드에 주입할 수 있는지 알아보겠다.

AOP 맛보기

Person.java

public interface Person {
    void runSomething();
}

Person 인터페이스를 구현하도록 Boy.java를 구현하겠다. 이 때 횡단 관심사를 모두 지우겠다.

Boy.java

package aop002;

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

스프링 프레임워크 기반에서 작동할 수 있도록 Start.java를 구현한다.

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

<?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>

MyAspect.java

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 개념

AOP 적용 전후의 Boy.java 코드가 어떻게 변하였는지 비교해보겠다.

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("자물쇠를 잠그고 집을 나선다.");
    }
}

AOP 적용 후 Boy.java

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.javarunSomething()메서드만 사용할 수 있던 것을 Girl.javarunSomething() 메서드도 @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("열쇠로 문을 열고 집에 들어간다.");
    }
}

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("자물쇠를 잠그고 집을 나선다.");
    }
}

Girl.java

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("자물쇠를 잠그고 집을 나선다.");
    }
}

Start.java

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

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

AOP 적용 후

Person.java

public interface Person {
    void runSomething();
}

Boy.java

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

Girl.java

public class Girl implements Person{
    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.xml");

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

MyAspect

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

<?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.javaGirl.java에서 System.out.println("컴퓨터로 게임을 한다.");, System.out.println("요리를 한다."); 메서드를 제외하고 나머지 코드가 사라진 이유는 횡단 관심사이기 때문이다.

인터페이스

스프링 AOP는 인터페이스 기반으로 작동하므로 Person 인터페이스를 추가하였다.

Start.java

아래 코드는 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

MyAspect.java는 왜 필요할까? Boy.javaGirl.java에서 공통적으로 나타나는 횡단 관심사를 모두 삭제 했지만 누군가는 횡단 관심사를 처리해야 한다. 따라서 MyAspect.java가 횡단 관심사를 처리한다.

aop002.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/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 빈은 AOPAspect이니까 등록해야 한다.

<aop:aspectj-autoproxy />는 프록시 패턴을 이용해 횡단 관심사를 핵심 관심사에 주입하는 것이다.

romeo.runSomething(); 메서드를 호출하면 프록시가 요청을 받아 진짜 romeo 객체에게 요청을 전달한다. 그런데 중앙 빨간색 메서드 (그림에서는 homework())인 runSomething() 메서드는 전달을 넘어 주고받는 내용을 감시, 조작할 수 있다. 스프링 AOP는 호출하는 쪽(romeo.runSomething)에서나 호출 당하는 쪽(romeo 객체), 모두 프록시가 존재하는지조차 모른다. 오직 스프링 프레임워크만 프록시 존재를 안다. 그래서 <aop:aspectj-autoproxy />는 스프링 프레임워크에게 AOP 프록시를 자동으로 사용하라고 명령하는 지시자이다.

스프링 AOP를 3개의 문장으로 요약하면 아래와 같다.

  • 스프링 AOP는 인터페이스 기반이다.
  • 스프링 AOP는 프록시 기반이다.
  • 스프링 AOP는 런타임 기반이다.

출처

스프링 입문을 위한 자바 객체지향의 원리와 이해

profile
하루하루 성실하게, 인생 전체는 되는대로.

0개의 댓글