슬기로운 개발생활

[Spring] AOP(Aspect Oriented Programming)란?

by coco3o
반응형

관점지향 프로그래밍(Aspect - Oriented Programming)이란 무엇인가?

AOP는 OOP(Object Oriented Programming, 객체지향 프로그래밍)를 돕는 보조적인 기술로,

관심사의 분리(기능의 분리)의 문제를 해결하기 위해 만들어진 프로그래밍 패러다임 입니다.

AOP는 기능을 핵심 관심 사항(Core Concern)과 공통 관심 사항(Cross-Cutting Concern)으로 분리시키고

각각을 모듈화 하는 것을 의미합니다.

  • 업무 로직을 포함하는 기능을 핵심 기능(Core Concern)
  • 핵심 기능을 도와주는 부가적인 기능을 부가 기능(Cross-Cutting Concern) 이라고 부른다.
  • OOP를 적용하여도 핵심 기능에서 부가 기능을 쉽게 분리된 모듈로 작성하기 어려운 문제점을 AOP가 해결해준다.
  • AOP는 부가 기능을 애스펙트(Aspect)로 정의하여, 핵심 기능에서 부가 기능을 분리함으로써 핵심 기능을 설계하고 구현할 때 객체지향적인 가치를 지킬 수 있게 도와주는 개념이다.

[간단한 소스 코드로 AOP 살펴보기 ]

Class A {
	
    method a() {
    AAAA
    business Logic..
    BBBB
    }
    
    method b() {
    AAAA
    business Logic..
    BBBB
    }
}

Class B {
	
    method c() {
    AAAA
    business Logic..
    BBBB
    }
}

핵심 기능을 하는 business Logic 과 부가 기능을 하는 코드 AAAA,BBBB를 볼 수 있습니다.

AAAA,BBBB가 여기저기서 사용되고 흩어져있기 때문에 코드 변경이 필요한 경우 일일이 다 찾아서 바꿔줘야 합니다.

AOP는 그렇게 하지 않고 여러 곳에서 사용되는 중복된 코드를 떼어내서 분리하고,

method a,b,c는 각각 business Logic만을 갖고 있자. 라는 개념입니다.

여기서 AAAA,BBBB가 AOP에서 말하는 Aspect 입니다.

 

즉, 필수적이지만 어쩔 수 없이 반복적으로 사용되는 코드들을 리팩토링할 수 있도록 해줍니다.

이로인해 여러 곳에서 사용될만한 코드들이 한 곳에서 유지하고 관리할 수 있는 이점을 갖게 됩니다.

 


AOP의 특징

프록시 패턴 기반

- Spring은 타겟(Target) 객체에 대한 프록시를 만들어서 제공한다.

- 타겟을 감싸는 프록시는 실행시간(RunTime)에 생성된다.

- 프록시는 어드바이스(Advice)를 타겟 객체에 적용하면서 생성되는 객체

- 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위함이다.

 

프록시가 호출을 가로챔(Intercept)

- 프록시는 타겟 객체에 대한 호출을 가로챈 다음 Advice의 부가기능 로직을 수행하고난 후에 타겟의 핵심기능 로직을 호출한다.

(전처리 어드바이스)

- 타겟의 핵심기능 로직 메소드를 호출한 후에 부가기능을 수행하는 경우도 있다.

(후처리 어드바이스)

 

메소드 JoinPoint만 지원한다.

- Spring은 동적 프록시를 기반으로 AOP를 구현하므로 메소드 조인 포인트만 지원

- 핵심기능(타겟)의 메소드가 호출되는 런타임 시점에만 부가기능(어드바이스)를 적용할 수 있음

 

- 반면에 AseptJ같은 고급 AOP 프레임워크를 사용하면 객체의 생성, 필드값의 조회와 조작, static 메소드 호출 및 동기화 등의 

다양한 작업에 부가기능을 적용할 수 있다.


AOP 용어에 대해 알아보자.

1. Target

- 부가기능을 부여할 대상 (핵심기능을 담고 있는 모듈)

 

2. Aspect

- 부가기능 모듈을 Aspect라고 부른다. (핵심기능에 부가되어 의미를 갖는 모듈)

- 부가될 기능을 정의한 Advice와 Advice를 어디에 적용할지를 결정하는 PointCut을 함께 갖고 있다.

- 어플리케이션의 핵심적인 기능에서, 부가적인 기능을 분리해서 Aspect라는 모듈로 만들어서 설계하고 개발하는 방법

 

3. Advice 

- 실질적으로 부가기능을 담은 구현체

- 타겟 오브젝트에 종속되지 않기 때문에, 부가기능에만 집중할 수 있음

- Aspect가 무엇을 언제 할지를 정의

 

4. PointCut

- 부가기능이 적용될 대상(Method)을 선정하는 방법

- Advice를 적용할 JoinPoint를 선별하는 기능을 정의한 모듈

 

5. JoinPoint

- Advice가 적용될 수 있는 위치

- Spring에서는 메소드 조인포인트만 제공한다.

- 타겟 객체가 구현한 모든 메소드는 조인 포인트가 된다.

 

6. Proxy

- Target을 감싸서 Target의 요청을 대신 받아주는 랩핑 오브젝트.

- 클라이언트에서 Target을 호출하게되면, 타겟이 아닌 타겟을 감싸고 있는 Proxy가 호출되어,

  타겟메소드 실행 전에 선처리, 후처리를 실행한다.

 

7. Introduction

- 타겟 클래스에 코드변경없이 신규메소드나 멤버변수를 추가하는 기능

 

8. Weaving

- 지정된 객체에 Aspect를 적용해서, 새로운 프록시 객체를 생성하는 과정

- Spring AOP는 런타임에서 프록시 객체가 생성된다.


AOP은 어떻게 사용할까?

스프링부트에서 간단하게 AOP를 적용하는 법 3단계를 알아보겠습니다.

 

1. spring-boot-starter-aop dependency 적용하기

maven의 fom.xml

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

gradle의 build.gradle

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

AOP 의존성을 추가하고 빌드를 하였으면 AOP를 활성화하겠다는 어노테이션을 추가해주어야 합니다.

 

2. @EnableAspectJAutoProxy 어노테이션 추가하기

@EnableAspectJAutoProxy
@SpringBootApplication
public class AopApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(AopApplication.class, args);
    }
}

 

3. 실제 AOP의 로직을 작성합니다. (부가기능을 정의하고 부가기능이 사용될 시점을 정의한다.)

모든 API에 비즈니스 로직의 실행시간을 측정해야 한다고 가정해보겠습니다.

@Aspect
@Component
public class LogAspect {
    Logger logger =  LoggerFactory.getLogger(LogAspect.class);
    
    //모든 패키지 내의 aop package에 존재하는 클래스
    @Around("execution(**..aop.*.*(..))")
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {
    //해당 클래스 처리 전의 시간
    StopWatch sw = new StopWatch();
    sw.start();
    
    //해당하는 클래스의 메소드 핵심기능을 실행
    Object result = pjp.proceed();
    
    //해당 클래스 처리 후의 시간
    sw.stop();
    long executionTime = sw.getTotalTimeMillis();
    
    String className = pjp.getTarget().getClass().getName();
    String methodName = pjp.getSignature().getName();
    String task = className + ". " + methodName;
    
    log debug("[ExecutionTime] " + task + "-->" + executionTime + "(ms)");
    
    return result;
    }
}

AOP 클래스로 설정하기 위해 @Aspect 어노테이션을 추가해주고, Spring의 빈으로 등록하기 위해 @Component 어노테이션을

추가해주었습니다. (AOP 사용시 빈 등록은 꼭 해주어야 합니다.) 그리고 우리가 하고자하는건 모든 API의 실행 시간을 측정하는 것이므로,

@Around 어노테이션을 통해 aop 패키지에 존재하는 모든 클래스에 해당 AOP를 적용하겠다고 설정해주었습니다.

그리고 실행 시간 측정을 위해 StopWatch를 생성하여 측정을 시작했고,

pjp의 proceed를 통해 실제 핵심 로직을 실행하여 Object 클래스로 결과를 받았습니다.

(Object 로 결과를 받아야 함!) 이후에 StopWatch를 중단하여 실행 시간을 밀리세컨드로 계산해 로그를 출력하고 함수를 종료시킵니다.

만약 실행 시간 측정을 밀리세컨드가 아닌 세컨드로 변경한다고 했을 때 AOP를 적용하지 않았다면 관련 로직의 모든 코드를 수정해주어야겠지만,

AOP를 적용함으로써 핵심 로직에 대한 수정 없이 쉽게 이를 처리할 수 있게 되었습니다.

 

Annotation

사용자가 직접 Annotation을 만들어 Aspect를 적용할 수도 있습니다.

// 이 어노테이션을 부여하면 해당 메소드의 성능을 로깅합니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
@Aspect
@Component
public class LogAspect {
    Logger logger =  LoggerFactory.getLogger(LogAspect.class);
    
    @Around("@annotation(LogExecutionTime)")
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {
    //해당 클래스 처리 전의 시간
    StopWatch sw = new StopWatch();
    sw.start();
    
    //해당하는 클래스의 메소드 핵심기능을 실행
    Object result = pjp.proceed();
    
    //해당 클래스 처리 후의 시간
    sw.stop();
    long executionTime = sw.getTotalTimeMillis();
    
    String className = pjp.getTarget().getClass().getName();
    String methodName = pjp.getSignature().getName();
    String task = className + ". " + methodName;
    
    log debug("[ExecutionTime] " + task + "-->" + executionTime + "(ms)");
    
    return result;
    }
}

Around를 execution에서 @annotation으로 변경만 해줍니다.

이렇게 하고 시간측정 AOP를 적용하고 싶은 클래스에 가서 @LogExecutionTime 어노테이션을 붙이면 끝입니다.


참고 :

https://shlee0882.tistory.com/206?category=703459 

https://mangkyu.tistory.com/121?category=761302

https://galid1.tistory.com/498

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기