본문 바로가기
컴퓨터 프로그래밍/Spring

[Spring] AOP

by 한33 2024. 9. 10.

AOP(Aspect Oriented Programming)

관점지향 프로그래밍은 복잡한 프로그래밍 세상에서 관심사를 분리하여 모듈로 사용하는데 목적이 있다.

 

AOP 는 핵심기능횡단 관심사 (부가기능) 를 분리해서 관리하는 것


AOP 의 키워드

1. 어드바이스 ( Advice )

2. 포인트컷 ( PointCut )

3. 타겟 ( Target )

4. 조인포인트 ( JoinPoint )

5. 애스팩트 ( Aspect )

 

a. 어드바이스

  • 어드바이스는 실제로 실행되는 횡단관심사(부가기능) 코드를 의미

b. 포인트컷

 

  • 포인트컷은 AOP 에서 어드바이스를 적용할 구체적인 범위를 선택하는 규칙.
  • 해당 범위는 특정 패키지 내의 모든 메서드들일 수도 있고, 특정 패키지내의 클래스의 모든 메서드들일 수도 있음.

c. 타겟

 

  • 타겟은 AOP 에서 어드바이스가 적용되는 객체를 의미.
  • 예를 들면 포인트컷으로 적용된 특정 클래스(CourseService) 가 타겟이 되면, 그 클래스 내의 모든 메서드들이 어드바이스의 적용을 받음.
execution(int createCourse(int, int)) // 반환타입 int, createCourse 이라는 매서드, 매개변수는 int 형 2개
execution(* createCourse(int, int)) // 반환타입 상관없음, createCourse 이라는 매서드, 매개변수는 int 형 2개
execution(* createCourse(..)) // 반환타입 상관없음, createCourse 이라는 매서드, 매개변수 상관없음
execution(* *(..)) // 반환타입 상관없음, 메서드이름 상관없음, 매개변수 상관없음

d. 조인포인트

  • 조인포인트는 어드바이스(횡단관심사) 가 적용될 수 있는 실행 지점을 의미.
  • 우리가 메서드 기준으로 어드바이스를 동작시킴으로 메서드들이 조인포인트가 됨.

e. 애스팩트

  • 애스팩트는 어드바이스(횡단관심사)와 포인트컷(횡단 관심사를 어디에 적용할지) 를 하나로 묶은 모듈.

 


AOP 적용

준비

@Slf4j
@Aspect
public class AspectPractice {

 

aop 폴더 생성, AOP 파일을 만들어준다.

 

@Configuration
public class WebConfig {

    @Bean
    public AspectPractice getAspectPracticeAop() {
        return new AspectPractice();
    }

 

WebConfig 가 있으면 추가하고 없으면 만들어주어서 등록해주자.


패키지 범위 기반 포인트컷 만들기

/**
 * 포인트컷 : 서비스(Service) 패키지 기반
 */
@Pointcut("execution(* com.standard.sparta.service..*(..))")
// 모든 반환타입, 경로 안에 있는 모든 메서드, 매개변수 상관없음
private void serviceLayer() {}

 

어드바이스

Before

/**
 * 어드바이스: @Before
 * 메서드 실행 전에 수행되는 로직을 처리할 때 사용되는 AOP
 */
@Before("serviceLayer()")
public void beforeMethod() {
    log.info("::: BEFORE 실행 :::");
}

 

AfterReturning

/**
 * 어드바이스: @AfterReturning
 * 메서드가 정상적으로 반환된 후에 실행
 * 예외가 발생하지 않고 정상적으로 결과값이 반환됐을 때 동작
 */
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturningMethod(Object result) {
    // result 관련 로직을 짤 수 있음
    log.info("::: AFTER RETURNING :::");
}

 

AfterThrowing

/**
 * 어드바이스: @AfterThrowing
 * 메서드 실행 중 예외가 발생했을 때에만 실행
 */
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void afterThrowingMethod(Throwable ex) {
    // ex <- 예외가 발생했을 때 필요한 조작
    log.info("::: AFTER THROWING :::");
}

 

After

/**
 * 어드바이스: @After
 * 메서드가 정상적으로 실행되건, 예외가ㅣ 발생하건, 항상 실행
 */
@After("serviceLayer()")
public void afterMethod() {
    log.info("::: AFTER :::");
}

 

Around

/**
 * 어드바이스: @Around
 * 가장 강력한 어드바이스, 전체 흐름을 제어할 수 있는 어드바이스
 */
@Around("serviceLayer()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    log.info("::: BEFORE :::");
    try {
        Object result = joinPoint.proceed();
        log.info("::: AFTER RETURNING :::");
        return joinPoint;
    } catch (Exception e) {
        log.info("::: AFTER THROWING :::");
        throw e;
    } finally {
        log.info("::: AFTER :::");
    }
}

 

패키지 범위 기반 어드바이스 적용

/**
 * 포인트컷 : 서비스(Service) 패키지 기반
 */
@Pointcut("execution(* com.standard.sparta.service..*(..))")
// 모든 반환타입, 경로 안에 있는 모든 메서드, 매개변수 상관없음
private void serviceLayer() {}

@Around("serviceLayer()")
public Object advicePackageMethod(ProceedingJoinPoint joinPoint) throws Throwable {

    // 측정 시작
    long startTime = System.currentTimeMillis();

    try {
        Object result = joinPoint.proceed();
        return result;
    } finally {
        // 측정 완료

        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        log.info("::: ExecutionTime : {} ms", executionTime);
    }

}

어노테이션 범위 기반 어드바이스 적용

어노테이션 생성

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

 

포인트컷 생성

/**
 * 포인트컷 : 어노테이션 범위 기반
 */
@Pointcut("@annotation(com.standard.sparta.annotation.TrackTime)")
public void trackTimeAnnotation() {}

 

어드바이스

/**
 * 어드바이스 : 어노테이션 범위 기반
 */
@Around("trackTimeAnnotation()")
public Object adviceAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {

    // 측정 시작
    long startTime = System.currentTimeMillis();

    try {
        Object proceed = joinPoint.proceed();
        return proceed;
    } finally {
    // 측정 완료
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        log.info("::: ExecutionTime : {} ms", executionTime);
    }
}
// 패키지 단위로 전체 다 설정하려면 execution 범위 설정 방법이 좋고
// 하나하나 원하는 메서드를 선택하려면 어노테이션 방식이 좋다.

AOP 동작 원리

스프링에서 객체를 빈으로 등록할 때 빈 후처리기 를 거치게 됨. 이때 빈 후처리기 는 AOP 설정이 적용된 객체를 감지하고, 해당 객체를 프록시객체로 감싼 후 스프링 컨테이너에 등록함.

 

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/bean")
public class BeanController {

    private final ApplicationContext applicationContext;

    @GetMapping
    public void checkBeanApi() {
        Object courseService = applicationContext.getBean("courseService");
        Class<?> aClass = courseService.getClass();
        log.info(aClass.getName());
    }
}

 

BeanController 를 만들어서 한 번 테스트를 해보았음

com.standard.sparta.service.CourseService // APO 미적용: 실제 객체  사용
com.standard.sparta.service.CourseService$$SpringCGLIB$$0 // AOP 적용: Proxy 객체 사용

 

이처럼 AOP 를 적용하면 CourseService 에 접근하기 전에 Proxy 객체에 먼저 접근하는 걸로 보아 Proxy 객체가 Service 를 감싸고 있는 것을 확인할 수 있었다.