[Spring] AOP( Aspect Oriented Programming )

Jeini·2023년 5월 28일
0

🍃  Spring

목록 보기
20/33
post-thumbnail

💡 AOP


✔️ 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하는 것

  • 관점 지향 프로그래밍

  • 횡단 관심사(cross-cutting concerns)

  • 부가 기능을 동적으로 추가해주는 기술
    : 동적으로 추가한다는 말은, 우리가 프로그램 만들때 만드는 것이 아니라, 코드가 수행될 떄 자동으로 추가해준다.
    ➡️ 메서드의 시작 또는 끝에 자동으로 코드(advice)를 추가

  • AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 흩어진 관심사 (Crosscutting Concerns)라 부른다.

  • 위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지

  • 하나의 메서드에 서로 다른 관심사가 있다.
    : 핵심기능과 기능이 서로 다른 관심사이다. 그 둘을 분리한다.

  • 로깅, 트랜잭션, 시큐리티는 공통 관심사여서 저렇게 표현한다. 그래서 횡단 관심사라고도 말한다.

  • 횡단 관심사는 모듈별로 따로 추가해주기 보다는 advice로 별도로 만들어서 동적으로 추가해주는 기술이 aop이다.

💡 Spring AOP 특징


  • 프록시 패턴 기반의 AOP 구현체, 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위해서임

  • 스프링 빈에만 AOP를 적용 가능

  • 모든 AOP 기능을 제공하는 것이 아닌 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제(중복코드, 프록시 클래스 작성의 번거로움, 객체들 간 관계 복잡도 증가 ...)에 대한 해결책을 지원하는 것이 목적

💡 AOP 관련 용어


  • advice + target = Proxy
    : 실행중에 target안에 advice가 필요한 곳으로 들어가게 된다. 마치 원래 있던 것처럼 합쳐진다.
    : advice와 target은 원래 하나여야 하는 것들인데 분리 시켜놨다. 공통 코드니까 분리해 놓았다가 실행중에 다시 합친다. ➡️ 새로운 객체가 만들어짐

  • 합치는 기능 ➡️ weaving

✏️ AOP 기초 예시

package kr.ac.jipark09.aop;

import java.lang.reflect.Method;

public class AopMain {
    public static void main(String[] args) throws Exception {
        MyAdvice myAdvice = new MyAdvice();
        // MyClass의 객체를 생성해서 MyAdvice의 invoke메서드에 넘겨줄 것이다.
        Class myClass = Class.forName("kr.ac.jipark09.aop.MyClass");
        Object obj = myClass.newInstance();

        for (Method m : myClass.getDeclaredMethods()) {
            myAdvice.invoke(m, obj, null);
        }
    }
}

// 공통 코드를 메서드로 만들고 각각 호
class MyAdvice {
    void invoke(Method m, Object obj, Object ... args) throws Exception {
        System.out.println("[before]{");
        m.invoke(obj, args); // aaa(), bbb(). ccc()
        System.out.println("}[after]");
    }
}

class MyClass {
    void aaa() {
        System.out.println("aaa");
    }
    void bbb() {
        System.out.println("bbb");

    }
    void ccc() {
        System.out.println("ccc");
    }
}
[결과값]

[before]{
aaa
}[after]
[before]{
bbb
}[after]
[before]{
ccc
}[after]
  • MyAdvice의 invoke() 를 통해서 이 메서드들이 호출되었기 때문에 위아래 before, after가 다 붙었다.

  • 추가할 코드를 미리 작성해 놓고도 그 코드가 마치 추가된 것처럼 할려고 하는 것이 AOP

✏️ aaa() 메서드에다가만 before / after 붙게 하기

✔️ MyAdvice()에 패턴 추가

package kr.ac.jipark09.aop;

import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class AopMain {
    public static void main(String[] args) throws Exception {
        MyAdvice myAdvice = new MyAdvice();
        // MyClass의 객체를 생성해서 MyAdvice의 invoke메서드에 넘겨줄 것이다.
        Class myClass = Class.forName("kr.ac.jipark09.aop.MyClass");
        Object obj = myClass.newInstance();

        for (Method m : myClass.getDeclaredMethods()) {
            myAdvice.invoke(m, obj, null);
        }
    }
}

// 공통 코드를 메서드로 만들고 각각 호
class MyAdvice {
    Pattern pattern = Pattern.compile("a.*"); // 정규식을 사용하여 앞글자가 a인 메서드만 추가

    // matcher 추가
    boolean matches(Method m) {
        Matcher matcher = pattern.matcher(m.getName());
        return matcher.matches();
    }

    void invoke(Method m, Object obj, Object ... args) throws Exception {
        // 패턴에 일치하는 경우에만 이 메서드가 발동
        if(matches(m)) {
            System.out.println("[before]{");
        }
        m.invoke(obj, args); // aaa(), bbb(). ccc()

        if(matches(m)) {
            System.out.println("}[after]");
        }
    }
}

class MyClass {
    void aaa() {
        System.out.println("aaa");
    }
    void bbb() {
        System.out.println("bbb");

    }
    void ccc() {
        System.out.println("ccc");
    }
}
[결과값]

[before]{
aaa
}[after]
bbb
ccc
  • 코드 중복을 막기위해 메서드로 빼고 패턴을 지정하여 그 패턴에 해당하는 메서드만 나오게 함.

✏️ 패턴 대신 어노테이션 사용

package kr.ac.jipark09.aop;

import org.springframework.transaction.annotation.Transactional;

import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class AopMain {
    public static void main(String[] args) throws Exception {
        MyAdvice myAdvice = new MyAdvice();
        // MyClass의 객체를 생성해서 MyAdvice의 invoke메서드에 넘겨줄 것이다.
        Class myClass = Class.forName("kr.ac.jipark09.aop.MyClass");
        Object obj = myClass.newInstance();

        for (Method m : myClass.getDeclaredMethods()) {
            myAdvice.invoke(m, obj, null);
        }
    }
}

// 공통 코드를 메서드로 만들고 각각 호출
class MyAdvice {
    void invoke(Method m, Object obj, Object ... args) throws Exception {
        //  @Transactional이 붙은 메서드만 발동
        if(m.getAnnotation(Transactional.class) != null ) {
            System.out.println("[before]{");
        }
        m.invoke(obj, args); // aaa(), bbb(). ccc()

        if(m.getAnnotation(Transactional.class) != null) {
            System.out.println("}[after]");
        }
    }
}

class MyClass {
    @Transactional
    void aaa() {
        System.out.println("aaa");
    }
    void bbb() {
        System.out.println("bbb");
    }
    void ccc() {
        System.out.println("ccc");
    }
}
[결과값]

[before]{
aaa
}[after]
bbb
ccc
  • 메서드 이름이나 선언부가 특정 패턴에 일치하는 메서드만 호출하게 할 수도 있고, 특정 어노테이션이 붙었을 때만 추가적인 코드가 실행되게 할 수도 있다.

❓ 코드를 자동으로 추가한다면 어디에?


deleteUser() 라는 메서드가 있을 때, 코드를 자동으로 추가한다면 어디다가 넣을 수 있을까?

  • 메서드마다 라인 수가 다르기 때문에, 중간에 코드를 넣지는 못한다.
    : 맨앞과 맨끝은 정해져 있다. ➡️ 자동으로 추가 가능

💡 Advice의 종류


✔️ Advice의 설정은 XML과 어노테이션, 두 가지 방법으로 가능

  • after throwing은 catch블럭에 해당함

  • after throwing은 try 끝에 들어가는 문

💡 pointcut expression


✔️ advice가 추가될 메서드를 지정하기 위한 패턴

  • execution(반환타입 패키지명.클래스명.메서드명(매개변수 목록))
    : 앞에 접근제한자 넣을 수도 있고 생략도 가능하다.
    : ..으로 적으면 개수 상관 없음

  • @Around = @Before + @After

  • 메서드에 반환 타입이 있는경우 Object로 반환해야 한다. void는 void로 해도 됨

  • ProceedingJoinPoint: 메서드의 모든 정보를 가지고 있음
    : pjp.getSignature().getName() ➡️ 메서드 이름
    : pjp.getArgs() ➡️ 매개변수로 넘기는 값들을 불러옴

  • 메서드 호출결과를 반환하는 이유는 여러개가 적용될 수 있다. 그러면 다음 advice에게 호출 정보를 넘겨줘야 한다. 그래야 after에서 쓸 수 있다.
    ➡️ @Order 를 써서 호출 정보를 넘겨줄 advice의 순서를 정할 수도 있다.

✏️ AOP 사용


✔️ AOP를 사용할려면 3가지의 라이브러리가 필요하다.

  1. AspectJ
<!-- AspectJ -->
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjrt</artifactId>
	<version>${org.aspectj-version}</version>
</dependency>
  1. Spring AOP
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aop</artifactId>
	<version>${org.springframework-version}</version>
</dependency>
  1. AspectJ Weaver
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjweaver</artifactId>
	<version>1.9.19</version>
	<scope>runtime</scope>
</dependency>

✔️ target으로 사용할 클래스를 만듬

package kr.ac.jipark09.aop;

import org.springframework.stereotype.Component;

@Component
public class MyMath {
    public int add(int a, int b) {
        int result = a + b;
        return result;
    }

    public int add(int a, int b, int c) {
        int result = a + b + c;
        return result;
    }

    public int subtract(int a, int b) {
        int result = a - b;
        return result;
    }

    public int mutiply(int a, int b) {
        int result = a * b;
        return result;
    }
}

✔️ root-context_aop.xml 만들고 <aop:aspectj-autoproxy/>를 써준다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:mvc="http://www.springframework.org/schema/mvc"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xmlns="http://www.springframework.org/schema/beans"
	   xmlns:context="http://www.springframework.org/schema/context"
	   xmlns:aop="http://www.springframework.org/schema/aop"
	   xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

	<!-- Root Context: defines shared resources visible to all other web components -->

	<aop:aspectj-autoproxy/>
	<context:component-scan base-package="kr.ac.jipark09.aop" />
</beans>

✔️ 실행

package kr.ac.jipark09.aop;

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

public class AopMain2 {
    public static void main(String[] args) {
        ApplicationContext ac = new GenericXmlApplicationContext("file:src/main/webapp/WEB-INF/spring/**/root-context_aop.xml");
        MyMath myMath = (MyMath) ac.getBean("myMath");
        int add = myMath.add(3, 5);
        int mutiply = myMath.mutiply(3, 5);

        System.out.println(add + " " + mutiply);

    }
}

✏️ MyMath에 부가기능을 추가해 보자

✔️ 부가기능이 담긴 클래스를 하나 더 만든다.

package kr.ac.jipark09.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Arrays;

// 부가기능이 담긴 메서드를 만든다.
@Component
@Aspect
public class LoggingAdvice {
    // pointcut: 부가기능이 적용될 메서드의 패턴
    @Around("execution(* kr.ac.jipark09.aop.*.MyMath(..))")
    public Object methodCallLog(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("<<[start]" + pjp.getSignature().getName() + Arrays.toString(pjp.getArgs()));

        // target의 메서드가 호출돼서 넘어온다.
        Object result = pjp.proceed();

        System.out.println("result=" + result);
        System.out.println("[end]>>" + (System.currentTimeMillis() - start) + "ms");
        return result;
    }
}
[결과값]

<<[start] add[3, 5]
result=8
[end]>>12ms
<<[start]mutiply[3, 5]
result=15
[end]>>0ms
8 15

Reference
: https://engkimbs.tistory.com/746
: https://fastcampus.co.kr/dev_academy_nks

profile
Fill in my own colorful colors🎨

0개의 댓글