[Spring Boot] Spring AOP

2024. 10. 10. 16:39·Back-End/Spring Boot
728x90

 

들어가기 전에

 

InConcert 프로젝트를 진행하면서 스크래핑 시간을 측정할 때, 메소드 안에서 실행 시간을 측정하는 것이 과연 올바른지 생각하게 되었다. 비즈니스 로직과 공통 로직을 분리하고 싶었고, 이를 AOP를 적용해서 코드를 수정하기로 하였다. 이전의 코드는 이 게시물에서 확인할 수 있다.

 

 

AOP?

 

AOP(Aspect Oriented Programming)는 관점 지향 프로그래밍으로, 기존의 OOP(객체 지향 프로그래밍)을 보완하는 확장 형태로 사용하고 있다. '관심의 분리'를 통해 핵심 관점(비즈니스 로직)과 횡단 관점(트랜잭션, 로그 등)을 분리하고자 하는 목적을 가지고 있다. AOP의 용어는 다음과 같다.

📌 AOP 용어

JoinPoint: 어플리케이션을 실행할 때 특정 작업이 실행되는 시점 (메소드를 호출하는 시점)
Advice: Joinpoint에서 실행되어야 하는 코드 (횡단 관점)
Target: 실질적인 비즈니스 로직을 구현하고 있는 코드 (핵심 관점)
Pointcut: Target 클래스와 Advice가 Weaving(결합)될 때 둘 사이의 결합 규칙을 정의
Aspect (Advice + Pointcut): 일정한 패턴을 가지는 클래스에 Advice를 적용하도록 지원할 수 있는 것으로, 공통된 부가 기능을 관리하고 원하는 메서드나 클래스에 자동으로 적용하게 해주는 도구

 

Spring AOP

Spring은 Aspect의 적용 대상(target)이 되는 객체에 대한 Proxy를 만들어 제공한다. @Aspect 어노테이션을 통해 작동 순서를 명시할 수 있고, Advice의 유형을 지정할 수 있다. 스크래핑 시간을 측정할 때는 메소드가 호출되는 전후를 따져야 하기 때문에 @Around 어노테이션을 사용하였다.

 

시간을 측정하는 어노테이션 정의
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

 

Ascpet 정의
package com.inconcert.common.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
public class ExecutionTimeAspect {
    // @LogExecutionTime을 사용하는 메소드 실행 전후로 실행 시간 기록
    @Around("@annotation(com.inconcert.common.annotation.LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();                    // 시작 시간 기록
        Object result = joinPoint.proceed();                            // 메서드 실행
        long endTime = System.currentTimeMillis();                      // 종료 시간 기록
        double executionTimeInSeconds = (endTime - startTime) / 1000.0; // ms를 초 단위로 변환

        System.out.println("스크래핑 실행 시간: " + executionTimeInSeconds + "초");
        return result;
    }
}

 

@LogExecutionTime 어노테이션을 사용하여 메소드의 실행 시간을 기록하고자 하였다. 메소드의 앞뒤로 실행 시간을 측정해야 하기 때문에 @Around 어노테이션을 사용한 것을 알 수 있다. joinPoint.proceed()를 통해 메소드를 실행 한 후 종료 시간을 기록하고 있다.

 

@LogExecutionTime 어노테이션을 스크래핑하는 메소드에 사용하게 된다면, 비즈니스 로직과 로깅 로직이 혼합되기 때문에 AOP의 목적을 해치게 된다. 따라서 별도의 Service을 만들어 로깅 로직을 작성하였다. 또한, Spring AOP는 메서드 호출을 가로채기 위해 프록시 객체를 사용하는데, AOP의 효과를 보장하려면 비동기 메소드를 Spring에 의해 관리되는 빈(Bean)에 두어야 한다. 현재 스크래핑이 @Async에 의해 비동기로 처리되고 있기 때문에 AOP의 Advice가 제대로 적용되기 위해서라도 Service를 따로 작성해야 할 이유가 있었다.

 

ScrapingLoggingService
import com.inconcert.common.annotation.LogExecutionTime;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class ScrapingLoggingService {
    @Async
    @LogExecutionTime
    public void measureScrapingPerformance(Runnable task) {
        task.run();
    }
}

 

여기서 @Async 어노테이션을 사용한 이유는 각 카테고리 별로 스크래핑 작업을 별도의 스레드에서 수행하기 때문에 각 작업의 로깅 시간을 합산하기 위함이다.

 

PerformanceService
// PerformanceService
@Async
public void startCrawlingAsync() {
    // type 별로 별도의 스레드에서 스크래핑 (생략)
    ...

    CompletableFuture.runAsync(() -> {
        try {
            List<CompletableFuture<Void>> futures = new ArrayList<>();
            for (int type = 1; type <= 4; type++) {
                int finalType = type;
                futures.add(CompletableFuture.runAsync(() ->
                        // 로깅 서비스의 메소드 호출
                        scrapingLoggingService.measureScrapingPerformance(() ->
                                crawlPerformances(String.valueOf(finalType)))));
            }
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

        } 
        // finally (생략)
        ...
    });
}

 

 

실행 결과

AOP를 적용하면 유지보수성, 가독성, 재사용성, 테스트 용이성 등을 향상시킬 수 있다. 어플리케이션을 실행하면 결과는 다음과 같이 나타난다. 각 스레드마다 스크래핑이 끝나면 중간 합산한 실행 시간이 출력되고, 최종적으로 스크래핑이 완료되면 최종 실행 시간을 출력하는 것을 확인할 수 있다.

첫 번째 ~ 세 번째 스레드가 끝난 후
스레드가 모두 끝난 후

 

 

 

728x90
저작자표시 비영리 변경금지 (새창열림)

'Back-End > Spring Boot' 카테고리의 다른 글

[Spring Boot] 인기 공연 순위 알고리즘 구현  (5) 2024.10.19
[Spring Boot] 스크래핑 비동기 처리  (6) 2024.10.09
[Spring Boot] N+1 문제  (1) 2024.10.05
[Spring Boot] JPA 쿼리 최적화  (1) 2024.10.04
[Spring Boot] Setter vs Builder  (0) 2024.03.28
'Back-End/Spring Boot' 카테고리의 다른 글
  • [Spring Boot] 인기 공연 순위 알고리즘 구현
  • [Spring Boot] 스크래핑 비동기 처리
  • [Spring Boot] N+1 문제
  • [Spring Boot] JPA 쿼리 최적화
hxxzz
hxxzz
개발새발 안 되게 개발 노력 중
  • hxxzz
    개발새발
    hxxzz
  • 전체
    오늘
    어제
    • 분류 전체보기 (104)
      • Java (3)
      • Back-End (9)
        • Spring Boot (7)
        • DevOps (1)
        • Redis (1)
      • Computer Scrience (4)
        • Data Structrue (4)
        • Algorithm (0)
      • SQLD (3)
      • 코딩테스트 연습 (85)
        • Programmers (30)
        • 백준 (15)
        • etc. (0)
        • 99클럽 (40)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    자료구조
    BFS
    코딩테스트 준비
    SQLD
    99클럽
    스택
    SpringBoot
    Stack
    Spring Boot
    redission
    프로그래머스
    N+1 문제
    개발자 취업
    til
    java
    SQL
    LeetCode
    dfs
    백준
    jpa
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
hxxzz
[Spring Boot] Spring AOP
상단으로

티스토리툴바