본문 바로가기
코딩 공부/web & Java

[Spring] AOP

by 현장 2024. 1. 5.

AOP (Aspect-Oriented Programming)

애플리케이션 전체에 걸쳐 사용되는 기능을 재사용하도록 지원하는 것으로 Aspect-Oriented Programming이란 단어를 번역하면 관점(관심) 지향 프로그래밍 이 됩니다. 프로젝트 구조를 바라보는 관점을 바꿔보자는 의미입니다.

각각의 Service의 핵심기능에서 바라보았을 때 User과 Order는 공통된 요소가 없습니다. 하지만 부가기능 관점에서 바라보면 이야기가 달라집니다.

부가기능 관점에서 바라보면 각각의 Service의 getXX 메서드를 호출하는 전후에 before과 after라는 메서드가 공통되는 것을 확인할 수 있습니다.

 

기존에 OOP에서 바라보던 관점을 다르게 하여 부가기능적인 측면에서 보았을 때 공통된 요소를 추출하자는 것입니다. 이때 가로(횡단) 영역의 공통된 부분을 잘라냈다고 하여, AOP를 크로스 컷팅(Cross-Cutting) 이라고 부르기도 합니다.

- OOP : 비즈니스 로직의 모듈화모듈화의 핵심 단위는 비즈니스 로직

- AOP : 인프라 혹은 부가기능의 모듈화
(ex. 로깅, 트랜잭션, 보안 등각각의 모듈들의 주목적 외에 필요한 부가적인 기능들)

간단하게 한줄로 AOP를 정리해 보자면, AOP는 공통된 기능을 재사용하는 기법 입니다.


OOP에선 공통된 기능을 재사용하는 방법으로 상속이나 위임을 사용합니다. 하지만 전체 애플리케이션에서 여기저기 사용되는 부가기능들은 상속이나 위임으로 처리하기에는 깔끔한 모듈화가 어렵습니다. 그래서 등장한 것이 AOP입니다.

✅ AOP 장점

▪️ 애플리케이션 전체에 흩어진 공통 기능이 하나의 장소에서 관리됩니다.
▪️ 다른 서비스 모듈들이 본인의 목적에만 충실하고 그 외 사항들은 신경 쓰지 않습니다.

✅ Gradle

implementation 'org.springframework.boot:spring-boot-starter-aop'

 

🏷️ AOP 용어

✅ Target 

부가기능을 부여할 대상(클래스)을 의미합니다. 위의 예시에서는 Service들을 의미합니다. 바로 아래서 설명하는 Aspect가 적용되는 대상을 의미합니다.

✅ Aspect

@Aspect
public class TestAspect {
	// 생략
}

객체지향 모듈을 오브젝트라 부르는 것과 비슷하게 부가기능 모듈을 Aspect라고 부르며, 핵심 기능에 부가되어 의미를 갖는 특별한 모듈입니다.


Aspect 안에는 부가기능을 정의하는 Advice, Advice를 어디에 적용할지 결정하는 PointCut이 있습니다. 즉, advice와 pointcut의 조합을 의미합니다. 참고로 AOP라는 뜻 자체가 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 Aspect라는 모듈을 만들어 설계하고 개발하는 방법을 뜻합니다.

 

implementation 'org.springframework.boot:spring-boot-starter-aop'

위와 같은 의존성을 설정했을 경우 @Aspect 애노테이션을 사용할 수 있게 되며 Advisor를 더욱 쉽게 구현할 수 있습니다.

 

해당 의존성을 추가하게 되면 자동 프록시 생성기(AnnotationAwareAspectJAutoProxyCreator)를 사용할 수 있게 되고, 이것이 Advisor 기반으로 프록시를 생성하는 역할을 합니다. 이와 더불어, 자동 프록시 생성기는 @Aspect를 보고 Advisor로 변환해서 저장하는 작업 을 수행합니다.

자동 프록시 생성기에 의해 @Asepct에서 Advisor로 변환된 Advisor는 @Aspect Advisor 빌더 내부에 저장됩니다.

✔️ 동작 과정

  1. 스프링 빈 대상이 되는 객체를 생성합니다.(@Bean, 컴포넌트 스캔 대상)
  2. 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달합니다.
  3. 모든 Advisor 빈을 조회합니다.
  4. @Aspect Advisor 빌더 내부에 저장된 모든 Advisor를 조회합니다.
  5. 3,4에서 조회한 Advisor에 포함되어 있는 포인트컷을 통해 클래스와 메서드 정보를 매칭하면서 프록시를 적용할 대상인지 아닌지 판단합니다.
  6. 여러 Advisor의 하나라도 포인트컷의 조건을 만족한다면 프록시를 생성하고 프록시를 빈 저장소로 반환합니다.
  7. 만약 프록시 생성 대상이 아니라면 들어온 빈 그대로 빈 저장소로 반환합니다.
  8. 빈 저장소는 객체를 받아서 빈으로 등록합니다.

❗주의사항

@Aspect는 Advisor를 쉽게 만들 수 있도록 도와주는 역할을 하는 것이지 컴포넌트 스캔이 되는 것은 아닙니다. 따라서 반드시 스프링 빈으로 등록해줘야 합니다.

✅ Advice

Aspect에서 실질적으로 어떤 일을 해야 할지에 대한 부가기능을 담은 구현체를 의미합니다. Advice는 Target Object에 종속되지 않기 때문에 순수하게 부가기능에만 집중할 수 있습니다. 쉽게 말해서 실행하려는 코드를 의미합니다.

 

✔️ Advice에 관련된 5가지 애노테이션

동작 순서

애노테이션은 메서드에 붙이게 되는데 해당 메서드는 advice의 로직을 정의하게 되고, 애노테이션의 종류에 따라 포인트컷에 지정된 대상 메서드에서 Advice가 실행되는 시점을 정할 수 있습니다. 또한 속성값으로 Pointcut을 지정할 수 있습니다.

 

1. @Around

  • 뒤에 나올 4가지 애노테이션을 모두 포함하는 애노테이션
  • 메서드 호출 전후 작업 명시 가능
  • 조인 포인트 실행 여부 선택 가능
  • 반환값 자체를 조작 가능
  • 예외 자체를 조작 가능
  • 조인 포인트를 여러 번 실행 가능(재시도)

2. @Before

  • 조인 포인트 실행 이전에 실행(실제 target 메서드 수행 전에 실행)
  • 입력값 자체는 조작 불가능
  • 입력값의 내부에 setter같은 수정자가 있다면 내부값은 수정 가능

3. @AfterReturning

  • 조인 포인트가 정상 완료 후 실행(실제 target 메서드 수행 완료 후 실행)
  • 반환값 자체는 조작 불가능
  • 반환값 내부에 setter같은 수정자가 있다면 내부값은 수정 가능

4. @AfterThrowing

  • 메서드가 예외를 던지는 경우 실행(실제 target 메서드가 예외를 던지는 경우 실행)
  • 예외 조작 불가능

5. @After

  • 조인 포인트의 정상, 예외 동작과 무관하계 실행(실제 target 메서드가 정상적 수행을 하든 예외를 던지든 수행 이후에 무조건 실행)

✔️ 예시 코드

// Around의 경우

@Aspect
@Configuration
public class PerformanceTrackingAspect {

    private Logger logger = LoggerFactory.getLogger(getClass());

    // @Around("com.in28minutes.learnspirngaop.aopexample.aspect.CommonPointcutConfig.businessAndDataPackageConfig()")
    @Around("com.in28minutes.learnspirngaop.aopexample.aspect.CommonPointcutConfig.trackTimeAnnotation()")
    public Object findExecutionTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 1: 타이머 시작
        long startTimeMillis = System.currentTimeMillis();

        // 2: 메소드 실행
        Object returnValue = proceedingJoinPoint.proceed();

        // 3: 타이머 멈추기
        long stopTimeMillis = System.currentTimeMillis();

        long executionDuration = stopTimeMillis - startTimeMillis;

        logger.info("Around Aspect - {} Method executed in {} ms"
                , proceedingJoinPoint
                , executionDuration);

        return returnValue;
    }
}
JoinPoint의 인터페이스의 주요 기능
- getArgs() : 메서드 인수 반환
- getThis() : 프록시 객체 반환
- getTarget() : 대상 객체 반환
- getSignature() : 조언되는 메서드에 대한 설명 반환
- toString() : 조언되는 방법에 대한 유용한 설명 인쇄
// 1: configuration
// 2: aop -> aspect
// advice와 pointcut의 조합
@Configuration
@Aspect
public class LoggingAspect {

    private Logger logger = LoggerFactory.getLogger(getClass());

    // point cut(when) -> 비지니스 레이어와 데이터 레이어 빈에서 호출 되는
    // 메소드 호출을 인터셉트해주는 것
    // excution(* PACKAGE.*.*(..)) -> 문법 주의
    // excution(* com.in28minutes.learnspirngaop.aopexample.bussiness.*.*(..))
    @Before("com.in28minutes.learnspirngaop.aopexample.aspect.CommonPointcutConfig.allPackageConfigUsingBean()")
    public void logMethodCallBeforeExecution(JoinPoint joinPoint) {
        // logic(what)
        // advice -> 실행하려는 코드
        logger.info("Before Aspect - {} is called with arguments: {}", joinPoint, joinPoint.getArgs());
    }

    @After("com.in28minutes.learnspirngaop.aopexample.aspect.CommonPointcutConfig.businessAndDataPackageConfig()")
    public void logMethodCallAfterExecution(JoinPoint joinPoint) {
        logger.info("After Aspect - {} has executed", joinPoint);
    }

    @AfterThrowing(
            pointcut = "com.in28minutes.learnspirngaop.aopexample.aspect.CommonPointcutConfig.businessAndDataPackageConfig()",
            throwing = "exception"
    )
    public void logMethodCallAfterException(JoinPoint joinPoint, Exception exception) {
        logger.info("AfterThrowing Aspect - {} has thrown an exception: {}",
                joinPoint, exception);
    }

    @AfterReturning(
            pointcut = "com.in28minutes.learnspirngaop.aopexample.aspect.CommonPointcutConfig.dataPackageConfig()",
            returning = "resultValue"
    )
    public void logMethodCallAfterSuccessfulExecution(JoinPoint joinPoint, Object resultValue) {
        logger.info("AfterReturning Aspect - {} has return: {}",
                joinPoint, resultValue);
    }
}

Advisor

스프링 AOP에서만 사용되는 용어로 advice + pointcut 한 쌍으로 되어있습니다.

✅ PointCut

public class CommonPointcutConfig {
    // 공용 포인트 컷
    // 보통 분리해서 한 곳에서 만들어두고 가져다가 사용합니다.
    @Pointcut("execution(* com.in28minutes.learnspirngaop.aopexample.*.*.*(..))")
    public void businessAndDataPackageConfig() {}

    @Pointcut("execution(* com.in28minutes.learnspirngaop.aopexample.bussiness.*.*(..))")
    public void businessPackageConfig() {}

    @Pointcut("execution(* com.in28minutes.learnspirngaop.aopexample.data.*.*(..))")
    public void dataPackageConfig() {}

    @Pointcut("bean(*Service*)")
    public void allPackageConfigUsingBean() {}

    @Pointcut("@annotation(com.in28minutes.learnspirngaop.aopexample.annotation.TrackTime)")
    public void trackTimeAnnotation() {}

}

인터셉트하려는 메소드 호출을 의미합니다. 해당 코드를 언제 실행할 것인가 인터셉트할 대상이 되는 메소드를 명시하는 표현식입니다.

✔️ Pointcut 종류

포인트컷 지시자의 종류는 여러 가지가 있지만 실질적으로는 execution과 @annotation 만 거의 사용하므로 2개만 살펴보겠습니다.

1. execution

execution(접근제어자? 반환타입 선언타입?메서드이름(파리미터) 예외?)

많은 종류의 포인트컷이 있지만 실질적으로는 가장 많이 사용하게 되는 종류입니다.

  • ?가 붙은 것들은 생략이 가능합니다. (접근제어자, 선업타입, 예외)
  • * 패턴을 통해 모든 타입 허용을 표현합니다.
  • ..을 통해 모든 타입 허용과 파라미터수가 상관없다는 것을 표현합니다.
  • 기본적으로 상위 타입을 명시하면 하위 타입에도 적용이 되지만, 하위 타입에만 메서드를 명시하는 경우 매칭이 불가능합니다. (즉, 타입은 상위타입으로 명시하고 메서드는 하위타입에만 있다면 적용 불가능)
  • 파라미터 타입의 경우 정확해야만 매칭됩니다. -> 부모타입을 허용하지 않습니다.

▪️ 예시

@Slf4j
class ExecutionTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello",String.class);
    }

    @Test
    void printMethod(){
        // public java.lang.String com.example.mvc.aop.member.MemberServiceImpl.hello(java.lang.String)
        log.info("helloMethod={}",helloMethod);
    }

    // 생략없이 정확한 매칭
    // 접근제한자 반환타입 타입(패키지를포함한클래스명).메서드명(인자타입)
    @Test
    void exactMatch(){
        pointcut.setExpression("execution(public String com.example.mvc.aop.member.MemberServiceImpl.hello(String))");
        Assertions.assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    // 모든 대상 적용
    // 생략 가능한 접근제한자, 타입 생략
    // 모든반환타입허용 모든메서드허용(모든인자허용)
    @Test
    void allMatch() {
        pointcut.setExpression("execution(* *(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // 특정 메서드 적용
    // 모든반환타입허용 hello메서드명만허용 모든파라미터허용
    @Test
    void nameMatch() {
        pointcut.setExpression("execution(* hello(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // 특정 메서드명으로 시작하는 모든 메서드에 적용
    // 모든반환타입허용 hel로시작하는모든메서드허용 모든파라미터허용
    @Test
    void nameMatchStar1() {
        pointcut.setExpression("execution(* hel*(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // 특정 단어를 포함한 모든 메서드에 적용
    // 모든반환타입허용 l이들어간모든메서드허용 모든파라미터허용
    @Test
    void nameMatchStar2() {
        pointcut.setExpression("execution(* *l*(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // 메서드명 불일치로 매칭 실패
    @Test
    void nameMatchFalse() {
        pointcut.setExpression("execution(* nono*(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isFalse();
    }

    // 모든반환타입허용 특정메서드명 모든파라미터허용
    @Test
    void packageMatch() {
        pointcut.setExpression("execution(* com.example.mvc.aop.member.MemberServiceImpl.hello(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // 모든반환타입허용 member패키지에있는모든타입허용(모든클래스허용) member패키지에있는모든타입의모든메서드허용 모든파라미터허용
    @Test
    void packageMatch2() {
        pointcut.setExpression("execution(* com.example.mvc.aop.member.*.*(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // MemberServiceImpl은 aop패키지가 아니라 aop의 하위패키지에 존재하므로 매칭 실패함
    // 모든반환타입허용 aop패키지에있는모든타입허용 aop패키지에있는모든타입의모든메서드허용 모든파라미터허용
    @Test
    void packageMatchFalse() {
        pointcut.setExpression("execution(* com.example.mvc.aop.*.*(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isFalse();
    }

    // ..가 타입명에 들어가있음 -> aop와 그의 모든 하위패키지를 허용함
    // 모든반환타입허용 aop패키지와그하위패키지의모든타입허용 모든메서드허용 모든파라미터허용
    @Test
    void packageMatchSubPackage() {
        pointcut.setExpression("execution(* com.example.mvc.aop..*.*(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // 모든반환타입허용 member패키지의MemberServiceImpl타입허용 모든메서드허용 모든파라미터허용
    @Test
    void typeExactMatch() {
        pointcut.setExpression("execution(* com.example.mvc.aop.member.MemberServiceImpl.*(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // MemberServiceImpl는 MemberService의 구현체 즉, 자식이기 때문에 허용된다.
    // 모든반환타입허용 member패키지의MemberService타입허용 모든메서드허용 모든파라미터허용
    @Test
    void typeExactSuperType() {
        pointcut.setExpression("execution(* com.example.mvc.aop.member.MemberService.*(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // 모든반환타입허용 member패키지의MemberService타입허용 모든메서드허용 모든파라미터허용
    // MemberService 인터페이스에는 없고 MemberServiceImpl구현체에만 따로 선언한 internal함수를 매칭하고 있으므로 매칭 실패
    // 부모타입을 명시했기 때문에 하위타입을 매칭할때도 부모타입에 존재하는 것만 매칭이 가능함
    @Test
    void typeExactNoSuperTypeInternal() throws NoSuchMethodException {
        pointcut.setExpression("execution(* com.example.mvc.aop.member.MemberService.*(..))");
        Method internalMethod = MemberServiceImpl.class.getMethod("internal",String.class);
        Assertions.assertThat(pointcut.matches(internalMethod,MemberServiceImpl.class)).isFalse();
    }

    // 모든반환타입허용 모든메서드허용 String파라미터1개만허용
    @Test
    void argMatch() throws NoSuchMethodException {
        pointcut.setExpression("execution(* *(String))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // 모든반환타입허용 모든타입허용(생략) 모든메서드허용 파라미터없어야만허용
    @Test
    void noArgMatch() throws NoSuchMethodException {
        pointcut.setExpression("execution(* *())");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isFalse();
    }

    // 모든반환타입허용 모든타입허용(생략) 모든메서드허용 단1개의모든타입파라미터허용
    @Test
    void argsMatchStar() throws NoSuchMethodException {
        pointcut.setExpression("execution(* *(*))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // 모든반환타입허용 모든타입허용(생략) 모든메서드허용 여러개의모든타입파라미터허용
    @Test
    void argsMatchAll() throws NoSuchMethodException {
        pointcut.setExpression("execution(* *(..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }

    // 모든반환타입허용 모든타입허용(생략) 모든메서드허용 1번째는String파라미터만허용 이후에는여러개의모든타입의파라미터허용
    @Test
    void argsMatchComplex() throws NoSuchMethodException {
        pointcut.setExpression("execution(* *(String, ..))");
        Assertions.assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }
}

2. @annotation

메서드가 주어진 애노테이션을 갖고 있는 경우 적용됩니다. @Target의 경우는 애노테이션이 붙어있는 클래스였는데 @annotation의 경우는 메서드입니다. @annotation의 경우는 종종 사용하는 경우가 있어 알아두는 게 좋습니다.

 

▪️ 예시

// 부착할 애노테이션
package com.example.mvc.aop.member.annotation;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
    String value();
}

public class MemberServiceImpl {
    // 대상 메서드에 부착
    @MethodAop("test value")
    public String hello(String param) {
        return "ok";
    }
}

// 적용
@Around("@annotation(com.example.mvc.aop.member.annotation.MethodAop)")
public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
    // 생략
}

 

✅ JoinPoint

Advice가 적용될 위치를 의미합니다. 다른 AOP 프레임워크와 달리 Spring에서는 메서드 JoinPoint만 제공하고 있습니다. 따라서 Spring 프레임워크에서 PointCut이 참이라면 Advice가 적용되는 메서드라고 생각하면 됩니다. 타 프레임워크에서는 예외 발생할 경우, 필드값이 수정될 경우 등도 지원하고 있습니다.

✅ Proxy

Target을 감싸서 Target에 들어오는 요청을 대신 받아주는 Warpping 오브젝트입니다. Client에서 Target을 호출하게 되면 Target이 아닌 Target을 감싸고 있는 Proxy가 호출되어, Target 메서드 실행 전에 전처리, Target 메서드 실행 후 후처리를 실행시킬 수 있도록 구성되어 있습니다. AOP에서 Proxy는 호출을 가로챈 후, Advice에 등록된 기능을 수행 후 Target 메서드를 호출합니다.

Introduction

Target 클래스에 코드 변경 없이 신규 메서드나 멤버변수를 추가하는 기능을 의미합니다.

✅ Weaving

지정된 객체에 Aspect를 적용해서 새로운 Proxy 객체를 생성하는 과정을 의미합니다. 예를 들면 A라는 객체에 트랜잭션 Aspect가 지정되어 있다면, A라는 객체가 실행되기 전 커넥션을 오픈하고 실행이 끝나면 커넥션을 종료하는 기능이 추가된 Proxy 객체가 생성됩니다. 이 Proxy 객체가 앞으로 A 객체가 호출되는 시점에서 사용됩니다. 이때의 프록시객체가 생성되는 과정을 Weaving이라 생각하시면 됩니다. Spring AOP는 런타임에서 프록시 객체가 생성됩니다.

weaver란
aop를 구현한 프레임워크를 의미로 어드바이스랑 포인트컷을 정의하고 난 다음 어드바이스가 적시에 구현되는지 확인하는 역할을 합니다. (ex. assertj, spring aop)

📖 Reference

backtony

'코딩 공부 > web & Java' 카테고리의 다른 글

[Docker] Docker Container와 Docker Image  (1) 2024.01.13
[Spring] Gradle  (0) 2024.01.07
[Java] 테스트 더블(Test Double) (feat. Mock, Stub)  (1) 2023.12.16
[Java] Mockito  (1) 2023.12.15
[Java] JUnit  (0) 2023.12.12