본문 바로가기

Spring

5. 스프링 AOP

책<초보 웹 개발자를 위한 스프링 5>, 유튜브 김영한님의 AOP적용을 참고하여 정리하였습니다.

 

AOP(Aspect Oriented Programming) 관점 지향 프로그래밍

  • 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 재사용성을 높여주는 프로그래밍 기법
  • AOP는 핵심 기능과 공통 기능의 구현을 분리함으로써 핵심 기능을 구현한 코드를 수정하지 않고 공통기능을 적용할 수 있게 만들어준다.

 

AOP 어떻게 쓰일까?

좋은 코드를 짜기 위해 로직에 맞게 하나의 함수에 하나의 핵심 기능만 갖는 것을 지향한다. 추가적인 기능이 들어가거나 부가 기능을 넣는다면 기능 자체가 모호해지고 다른 곳에서 쓰기도 애매해진다. 그리고 유지보수의 어려움도 있다. AOP는 관점지향 프로그래밍의 줄임말이다(기능, 관심이라 이해하는 것이 좋음). 여기서 말하는 관점 지향이란, 어떠한 로직에서 핵심적인 기능을 담당하는 모듈들을 바라보는 관점과 핵심적이지 않은, 즉 부가적인 기능을 담당하는 모듈을 바라보는 관점으로 나누어서 보고, 그 관점을 기준으로 모듈화를 하는 것이다.  

객체지향 프로그래밍에서 클래스가 단일 책임을 가지도록 분리하여 모듈의 응집도를 높이고 결합도는 낮춰야한다.

여기서 AOP는 위의 그림처럼 다양한 클래스에서 사용되는 기능들을 관심사에 따라 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이다.

 

AOP가 언제 사용될까?

  • 메서드의 성능검사
  • 트랜잭션 처리
  • 예외반환
  • 아키텍처 검증
  • 보안 인증
  • 로깅

 

핵심 기능에 공통 기능을 삽입하는 방법

  1. 컴파일 시점에 코드에 공통 기능을 삽입하기
  2. 클래스 로딩 시점에 바이트 코드에 공틍기능을 삽입하는 방법
  3. 런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 방법
    • 스프링이 제공하는 방식
    • 중간에 프록시 객체를 생성하고 실제 객체의 기능을 실행하기 전 후에 공통 기능을 호출한다.
    • proxy : 핵심 기능의 실행은 다른 객체에 위임하고 부가적인 기능을 제공하는 객체 / 핵심기능은 구현하지 않는 것이 특징이다. 대신 여러 객체에 공통으로 적용할 수 있는 기능을 구현한다.(프록시 패턴에 대해 알아보자)

→ 위 두 가지는 스프링 AOP에서는 지원하지 않으며 AspectJ와 같이 AOP 전용 도구를 사용해서 적용할 수 있다.

 ※ AspectJ : PARC에서 개발한 자바 프로그래밍 언어용 관점 지향 프로그래밍 (AOP) 확장 기능이다. 이클립스 재단 오픈 소스 프로젝트에서 독립형 또는 이클립스로 통합하여 이용 가능하다.

 

  스프링 AOP AspectJ
목표 간단한 AOP 기능 제공 완벽한 AOP기능 제공
JoinPoint 메서드 레벨만 지원 생성자, 필드, 메서드 등 다양하게 지원
weaving 런타임 시에만 가능 런타임은 제공하지 않음
compile-time, post-compile, load-time 제공
대상 Spring Container가 관리하는 Bean에만 가능 모든 Java Object에 가능

 

AOP 주요 용어

용어 의미
Target 핵심기능을 담고 있는 모듈로 target은 부가기능을 부여할 대상이 된다.
Advice 언제 공통 관심 기능(부가기능)을 핵심 로직에 적용할지를 정의하고 있다.
예를 들어 ‘메서드를 호출하기 전’ (언제)에 ‘트랜잭션 시작’ (공통 기능) 기능을 적용한다는 것을 정의한다.
Joinpoint Advice를 적용 가능한 지점을 의미한다. 메서드 호출, 필드 값 변경 등이 Joinpoint에 해당한다.
스프링은 프록시를 이용해서 AOP를 구현하기 때문에 메서드 호출에 대한 Joinpoint만 지원한다.
Pointcut Joinpoint의 부분집합으로서 실제 Advice가 적용되는 Joinpoint를 나타낸다.(공통 기능을 적용할 대상; 해당 advice 를 어디에 넣을 것인지)
스프링에서는 정규 표현식이나 AspectJ의 문법을 이용하여 Pointcut을 정의할 수 있다.
Weaving Advice를 핵심 로직 코드(Pointcut)에 적용하는 것을 weaving이라고 한다.
즉 Pointcut에 의해서 결정된 타겟의 JoinPoint에 부가기능(Advice)을 삽입하는 과정을 뜻함
AOP의 핵심로직 코드에 영향을 주지 않으면서 필요한 부가기능을 추가할 수 있도록 해주는 핵심적인 처리과정
Aspect 여러 객체에 공통으로 적용되는 기능을 Aspect라고 한다. 트랜잭션이나 보안 등이 Aspect의 좋은 예이다.

 

스프링에서 구현 가능한 Advice 종류

종류 의미
Before Advice 대상 객체의 메서드 호출 전에 공통 기능을 실행한다.
After Returning Advice 대상 객체의 메서드가 익셉션 없이 실행된 이후에 공통 기능을 실행한다.
After Throwing Advice 대상객체의 메서드를 실행하는 도중 익셉션이 발생한 경우에 공통 기능을 실행한다.
After Advice 익셉션 발생 여부에 상관없이 대상 객체의 메서드 실행 후 공통 기능을 실행한다.
(try-catch-finally의 finally 블록과 비슷하다.)
Around Advice 대상객체의 메서드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능을 실행하는데 사용된다.
  • 이 중에서 널리 사용되는 것은 Around Advice이다. 이유는 대상 객체의 메서드를 실행하기 전/후, 익셉션 발생 시점 등 다양한 시점에 원하는 기능을 삽입할 수 있기 때문이다.
  • 캐시 기능. 성능 모니터링 기능과 같은 Aspect를 구현할 때에는 Around Advice를 주로 이용

 

스프링 AOP 구현

  • @Aspect : Aspect로 사용할 클래스에 사용 → 프록시는 스프링 프레임워크가 알아서 만들어 줌
  • @Pointcut : 공통 기능을 적용할 대상을 설정
  • @Around : 공통 기능을 구현한 메서드에 적용

 

예제 - factorial 동작 시간 구하기

0. 의존성 추가

maven

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

gradle

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

 

1. factorial 계산 메서드 

package chap07;
public class ImpeCalculator implements Calculator {

    @Override
    public long factorial(long num) {

        //long start = System.currentTimeMillis();
        long result = 1;
        for(long i = 1; i <=num; i++) {
            result *= i;
        }

        //long end = System.currentTimeMillis();
        //System.out.printf("ImpeCalculator.factorial(%d) 실행시간 = %d\n", num, (end - start));
        return result;
    }
}

함수 내에서 시간을 구하려면 위와 같이 해줘야한다. 하지만 시간계산은 핵심 기능이 아닌 부가 기능이므로 분리하고자 한다.

 

2. Aspect 설정

package aspect;

import java.util.Arrays;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect // AOP임을 알림
public class ExeTimeAspect {

    @Pointcut("execution(public * chap07..*(..))")    //chap07패키지에 해당하는 메서드들에게 모두 적용
    private void publicTarget() {
    }

    @Around("publicTarget()")    //AroundAdvice를 설정
    public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        try {
            Object result = joinPoint.proceed();  //AOP가 적용된 메소드 전체를 수행하고 리턴되는 데이터를 받는 메소드
            return result;
        } finally {
            long finish = System.nanoTime();
            Signature sig = joinPoint.getSignature();
            System.out.printf("%s.%s(%s) 실행시간 : %d ns\n",
                            joinPoint.getTarget().getClass().getSimpleName(),
                            sig.getName(),
                            Arrays.toString(joinPoint.getArgs()), (finish - start) );
        }
    }
}

@PointCut : 공통 기능을 적용할 대상을 설정

publicTarget()메서드에 정의한 Pointcut에 공통 기능을 적용

execution

설명
execution(public void set*(..)) 리턴 타입이 void이고, 메서드 이름이 set으로 시작하고, 파라미터가 0개 이상인 메서드 호출. 파라미터 부분에 '..'을 사용하여 파라미터가 0개 이상인 것을 표현
execution(* chap07.*.*()) chap07 패키지 타입에 속한 파라미터가 없는 모든 메서드 호출
execution(* chap07..*.*(..)) chap07 패키지 및 하위 패키지에 있는 파라미터가 0개 이상인 메서드 호출. 패키지 부분에 '..'을 사용하여 해당 패키지 또는 하위 패키지를 표현
execution(Long chap07.Calculator.factorial(..)) 리턴 타입이 Long인 Calculator 타입의 factorial()메서드 호출
execution(* get*(*)) 이름이 get으로 시작하고 파라미터가 한 개인 메서드 호출
execution(* get*(*, *)) 이름이 get으로 시작하고 파라미터가 두 개인 메서드 호출
execution(* read*(Integer, ..)) 메서드 이름이 read로 시작하고, 첫번째 파라미터 타입이 Integer이며, 한 개 이상의 파라미터를 갖는 메서드 호출

 

3. Aspect 메서드 빈 설정

- @Component로 빈 설정해도 된다.

package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import aspect.ExeTimeAspect;

import chap07.Calculator;
import chap07.RecCalculator;

@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
    @Bean
    public ExeTimeAspect exeTimeAspect() {
        return new ExeTimeAspect();
    }
    @Bean
    public Calculator calculator() {
        return new RecCalculator();
    }
}

 

4. 메인 함수 구현

package main;

import chap07.ExeTimeCalculator;
import chap07.ImpeCalculator;

public class MainProxy {
    public static void main(String[] args) {
        ExeTimeCalculator ttCal1 = new ExeTimeCalculator(new ImpeCalculator());
        System.out.println(ttCal1.factorial(5));

        ExeTimeCalculator ttCal2 = new ExeTimeCalculator(new ImpeCalculator());
        System.out.println(ttCal2.factorial(5));

        ExeTimeCalculator ttCal3 = new ExeTimeCalculator(new ImpeCalculator());
        System.out.println(ttCal3.factorial(5));
    }
}

 

 

5. 결과

ImpeCalculator.factorial(5) 실행 시간 = 5099
120
ImpeCalculator.factorial(5) 실행 시간 = 1300
120
ImpeCalculator.factorial(5) 실행 시간 = 1200
120

 

 

 

스프링 AOP 동작방식

1. 기존

 

 

2. 변경 후

프록시는 근본이 되는 Real Subject를 감싸서 대신 처리한다.

'Spring' 카테고리의 다른 글

7. JDBC, SQL Mapper, ORM  (2) 2022.07.15
6. 스프링 Servlet  (0) 2022.07.02
4. 람다함수의 개념 이해하기 + 스트림  (0) 2022.01.25
3. 템플릿 메소드 패턴 VS 전략패턴  (0) 2022.01.12
2. 토비 Chapter 4.템플릿 내용 정리  (0) 2022.01.09