본문 바로가기

Spring/Springboot

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

 

주요 개념

 

1. Aspect:반복해서 여러 곳에서 사용되는 공통 코드

2. Target: Aspect가 적용되는 곳

3. Advice: Aspect의 실질적인 기능에 대한 구현체

4. Joint point: Advice가 Target에 적용되는 시점(지점), 메서드 진입 / 생성자 호출 / 필드에서 값을 꺼낼 때 등, 스프링에서 Joint point는 항상 메서드 실행 시점을 의미함

5. Point cut: Joint point의 상세 스펙을 정의한 것(어디에 적용해야 하는지)

[RequestIP: 0:0:0:0:0:0:0:1] error Msg: Connection refused: localhost/127.0.0.1:8000

AOP 구현체

1. AspectJ

2. 스프링 AOP

 

AOP 적용방법

1. 컴파일 타임(AspectJ): 컴파일 시점(자바 파일을 클래스 파일로 만들 때)에 바이트 코드를 조작하여 AOP가 적용된 바이트 코드를 생성

2. 로드 타임(AspectJ): 컴파일은 기존 클래스 그대로 한 후, 클래스를 로딩하는 시점에 클래스 정보를 변경하는 방법

3. 런타임(스프링 AOP): 클래스를 Bean으로 만들 때 클래스 타입의 Proxy Bean을 감싸서 만들고, Proxy Bean이 Aspect 코드를 추가

 

스프링에서 제공하는 스프링 AOP

특징

프록시 패턴이라는 디자인 패턴을 사용해서 AOP 효과를 냄: // 프록시 패턴은 프록시 클래스를 직접 구현하는데 스프링 AOP는 프록시 객체를 자동으로 만들어 줌, 공통 기능을 구현한 클래스만 잘 구현하면 됨

스프링 빈에만 AOP 적용 가능

동적 프록시 빈을 만들어 등록시켜 줌

- 빈 라이프 사이클 중 실행되는 BeanPostProcessor 구현체를 구현

- AbstractAutoProxyCreator implements BeanPostProcessor

 

Proxy: 클라이언트와 타깃 사이에 투명하게 존재하며 부가기능을 제공하는 오브젝트. DI를 통해 타겟 대신 클라이언트에게 주입되며 클라이언트의 메서드 호출을 대신 받아서 타깃에 위임하며 이 과정에서 부가기능을 부여

 

스프링 AOP 구현 방법

 

1. 의존성 추가

maven

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

gradle

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

 

 

2. 스프링 부트 애플리케이션 클래스에 @EnableAspectJAutoProxy 어노테이션 추가

AspectJ 자동 프록시 활성화

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy
public class ProjectTemplateApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProjectTemplateApplication.class, args);
    }

}

 

 

3. Aspect 클래스 작성

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

@Aspect //  반복해서 여러 곳에서 사용되는 공통 코드임을 알리는 어노테이션
@Component // 스프링 Bean 등록
public class TimeTraceAop {
    
    // Aspect 기능 구현 부분
    // @Around: 메서드 실행 전/후에 실행되는 Advice 정의
    // ProceedingJoinPoint: Around Advice에서만 사용되는 파라미터로, 메서드 실행을 직접 제어할 수 있는 기능을 제공
    @Around("execution(* com.example.hello..*(..))") // execution으로 적용범위 지정
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        
        // 메서드 실행 전 현재 시간을 start 변수에 저장
        long start = System.currentTimeMillis();
        
        // 메서드 실행 전 현재 시간 출력
        System.out.println("START: " +  joinPoint.toString());
        
        try {        
        // 메서드 실행
            return joinPoint.proceed();           
            
        } finally { // finally를 사용하여 메서드가 에러가 나도 실행되도록 처리
        
            // 메서드 실행 후 현재 시간을 finish 변수에 저장
            long finish = System.currentTimeMillis();
            
            // 현재시간-시작시간을 연산하여 메서드 실행에 걸린 시간을 timeMs 변수에 저장
            long timeMs = finish - start;
            
            // 메서드 종료 후 실행
            System.out.println("END: " + joinPoint.toString() +" " + timeMs + "ms" );
        }
    }

}

 

1) 어노테이션 기반

어노테이션 설명
@Around Advice가 타겟 메서드를 감싸 타겟 메서드 호출 전, 후에 Advice 기능 수행
@Before Advice 타겟 메서드가 호출되기 전에 Advice 기능 수행
@After 타겟 메서드의 결과에 관계없이 타겟 메서드과 완료되면 Advice 기능 수행
@AfterRunning 타겟 메서드가 성공적으로 결과값을 반환 한 후에 Advice 기능 수행
@AfterThrowing 타겟 메서드가 수행 중 예외를 던지면 Advice 기능 수행

 

2) execution expression 

execution 패턴으로 경로 지정

구조

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
  • modifiers-pattern: 메서드의 접근 제어자 public, protected, private, static, final 등을 지정
  • ret-type-pattern: 메서드의 반환 타입을 지정, 클래스명이나 와일드카드(*) 사용
  • declaring-type-pattern: 메서드를 정의한 클래스나 인터페이스 지정
  • name-pattern: 메서드의 이름을 지정, 와일드카드(*)를 사용가능
  • param-pattern: 메서드의 매개변수를 지정, 와일드카드(*)를 사용하여 임의의 타입을 나타낼 수 있
  • throws-pattern: 메서드가 던지는 예외를 지정

 

 

응용하기

 

1. 커스텀 어노테이션이 붙은 pointcut에  Aspect 실행하기

1) @PerfLogging 커스텀 어노테이션 생성

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PerfLogging {
}

 

2) @Around 어노테이션을 사용하여 해당 어노테이션이 붙은 메서드에 성능 로깅 추가

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

@Aspect
@Component
public class PerfLoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(PerfLoggingAspect.class);

    @Around("@annotation(PerfLogging)") // 적용될 어노테이션을 명시할 수 있다.
    public Object logPerf(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        logger.info(joinPoint.getSignature() + " executed in " + executionTime + "ms");
        return result;
    }
}

 

3) 해당 메서드를 적용시킬 특정 메서드에 @PerfLogging 어노테이션을 붙여주기만 하면 logPerf() 기능 동작

import org.springframework.stereotype.Service;

@Service
public class MyService {

    @PerfLogging
    public void myMethod() {
        // 메서드 내용
    }
}

 

 

 

2. 특정 Bean 전체에 해당 기능을 적용시키기

 

1) @Around 어노테이션을 사용하여 특정 빈에 적용될 Aspect 정의

bean(simpleService)를 지정하여 simpleService라는 이름의 빈에만 해당 Aspect가 적용되도록 함

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

@Aspect
@Component
public class MyServiceAspect {

    private static final Logger logger = LoggerFactory.getLogger(MyServiceAspect.class);

    @Around("bean(simpleService)") //@Around 어노테이션에 bean(simpleServcieEvent)처럼 적용될 빈을 명시할 수 있음
    public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        logger.info(joinPoint.getSignature() + " executed in " + executionTime + "ms");
        r

 

해당 빈(simpleService)이 가지고 있는 모든 public 메서드는 위에서 정의된 logMethodExecution() 메서드를 실행하여 로깅 기능이 적용됨

import org.springframework.stereotype.Service;

@Service("simpleService")
public class SimpleService {

    public void method1() {
        // 메서드 내용(적용할 기능)
    }

    public void method2() {
        // 메서드 내용(적용할 기능)
    }
}