내일배움캠프

[내일배움캠프] Spring AOP

munsik22 2026. 4. 10. 15:40

🧩 TOP 5 회원 찾기 구현

  • 스크래치 파일 생성
    • 스크래치: 프로젝트와 상관 없이 어떤 특정 메서드나 코드를 수행시켜보고 싶을 때 사용하는 파일
    • 프로젝트 폴더가 아니라 스크래치 및 콘솔 탭 - 스크래치 폴더에 생성 된다.

스크래치 파일 저장 위치

class Scratch {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        long output = sumFromOneTo(1_000_000_000);
        long endTime = System.currentTimeMillis();
        long runTime = endTime - startTime;
        System.out.println("소요시간: " + runTime);
    }

    private static long sumFromOneTo(long input) {
        long output = 0;
        for (int i = 1; i < input; ++i) {
            output = output + i;
        }
        return output;
    }
}
  • ApiUseTime 엔티티 생성
@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "api_use_time")
public class ApiUseTime {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @Column(nullable = false)
    private Long totalTime;

    public ApiUseTime(User user, Long totalTime) {
        this.user = user;
        this.totalTime = totalTime;
    }

    public void addUseTime(long useTime) {
        this.totalTime += useTime;
    }
}
  • ApiUseTimeRepository 생성
public interface ApiUseTimeRepository extends JpaRepository<ApiUseTime, Long> {
    Optional<ApiUseTime> findByUser(User user);
}
  • ProductController: createProduct 수정
private final ApiUseTimeRepository apiUseTimeRepository;

PostMapping("/products")
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
    long startTime = System.currentTimeMillis();

    try {
        return productService.createProduct(requestDto, userDetails.getUser());
    } finally {
        long endTime = System.currentTimeMillis();
        long runTime = endTime - startTime;

        User loginUser = userDetails.getUser();

        ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
                .orElse(null);
        if (apiUseTime == null) {
            apiUseTime = new ApiUseTime(loginUser, runTime);
        } else {
            apiUseTime.addUseTime(runTime);
        }

        System.out.println("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
        apiUseTimeRepository.save(apiUseTime);
    }
}
  • 이 방식의 문제점
    • 핵심기능이 100개라면 100개의 핵심기능 모두에 동일한 내용의 부가기능 코드를 추가해야 한다
    • 부가기능 코드를 100번 겨우 복붙했는데 수정할 부분이 생기면 100번 반복해서 코드를 수정해야 한다🤯
  • 해결책: AOP(Aspect Oriented Programming)를 통해 부가기능을 모듈화

🧩 Spring AOP란

Spring의 AOP 어노테이션

  • @Aspect: Spring Bean 클래스에만 적용 가능함
  • 어드바이스 종류
    • @Around: 핵심기능 수행 전과 후
    • @Before: 핵심기능 호출 전
    • @After: 핵심기능 수행 성공/실패 여부와 상관없이 언제나 동작
    • @AfterReturning: 핵심기능 호출 성공 시
    • @AfterThrowing: 핵심기능 호출 실패 시 (예외가 발생한 경우)
  • 포인트컷
    • 포인트컷 Expression 형태: execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
    • @Pointcut: 포인트컷 재사용 및 결합 가능

Spring AOP 적용

  • ProductController: createProduct 롤백
  • UseTimeAop 생성: AOP를 사용해 FolderController, ProductController, NaverApiController에 부가기능을 일괄 추가
    ▼ 코드 보기
    @Slf4j(topic = "UseTimeAop")
    @Aspect
    @Component
    public class UseTimeAop {
    
        private final ApiUseTimeRepository apiUseTimeRepository;
    
        public UseTimeAop(ApiUseTimeRepository apiUseTimeRepository) {
            this.apiUseTimeRepository = apiUseTimeRepository;
        }
    
        @Pointcut("execution(* com.sparta.myselectshop.controller.ProductController.*(..))")
        private void product() {}
        @Pointcut("execution(* com.sparta.myselectshop.controller.FolderController.*(..))")
        private void folder() {}
        @Pointcut("execution(* com.sparta.myselectshop.naver.controller.NaverApiController.*(..))")
        private void naver() {}
    
        @Around("product() || folder() || naver()")
        public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
            long startTime = System.currentTimeMillis();
    
            try {
                Object output = joinPoint.proceed();
                return output;
            } finally {
                long endTime = System.currentTimeMillis();
                long runTime = endTime - startTime;
    
                Authentication auth = SecurityContextHolder.getContext().getAuthentication();
                if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
                    UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
                    User loginUser = userDetails.getUser();
    
                    ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser).orElse(null);
                    if (apiUseTime == null) {
                        apiUseTime = new ApiUseTime(loginUser, runTime);
                    } else {
                        apiUseTime.addUseTime(runTime);
                    }
    
                    log.info("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
                    apiUseTimeRepository.save(apiUseTime);
                }
            }
        }
    }
  • AOP를 적용하면 DispacherServlet과 ProductController 사이에 AOP Proxy가 삽입된다.
    • DispatcherServlet과 ProductController 입장에서는 변화가 전혀 없다.
    • joinPoint.proceed()에 의해 원래 호출하려고 했던 함수와 인수가 전달된다.