Spring/Spring Boot

[Spring] AOP란 - (AOP, Spring AOP, AOP 어노테이션)

민돌v 2022. 1. 18. 17:29

목차

  1. AOP란
  2. AOP의 주요 개념
  3. Spring AOP
  4. AOP 적용 시퀸스 다이어그램
  5. Spring AOP 어노테이션
  6. Spring AOP 코드 예시

AOP란

Aspect Oriented Programming - 관점 지향 프로그래밍이라고 불리는 AOP

관점 지향이란, 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 각각 모듈화하는 프로그래밍 기법을 의미합니다.

스파르타 코딩클럽

따라서 AOP는 핵심기능과 부가기능을 나누어서 설계, 구현하는 것을 말합니다. 


AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미입니다.

이떄, 소스코드상에서 다른 부분에 계속 반복해서 사용되는 코드들이 존재할 수 있는데, 이것을 흩어진 관심사(Crossing Concerns)라고 부릅니다.

https://engkimbs.tistory.com/746

위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 기능에서 분리하여 재사용하겠다는 것이 AOP의 취지입니다.

 

AOP의 주요 개념

  • Aspect : 위에서 설명한 흩어진 관심사를 모듈화 한 것입니다. (주로 부가기능을 모듈화)
  • Target : Aspect를 적용하는 곳(클래스, 메서드 등등)
  • Advice : 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
  • JoinPoint : Advice가 적용될 위치, 끼어들 수 있는 지점, 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때의 시점을 말합니다. 앱을 실행할 때 특정 작업이 시작되는 시점입니다.
  • PointCut : JoinPoint가 적용되는 대상, Adivice가 실행될 지점을 설정

 


Spring AOP

Spring에서 다음과같은 AOP를 제공합니다.

  1. 어드바이스: 부가기능
  2. 포인트컷: 부가기능 적용위치

스파르타 코딩클럽


AOP 적용후 시퀸스 다이어그램

  • AOP 적용 전

AOP 적용전

  • AOP 적용 후

AOP 적용 후

  • DispatcherServlet과 Controller 입장에서는 변화가 없음
    • 호출되는 함수의 input, output이 완전히 동일
    • "joinPoint.procced()"에 의해서 원래 호출하려고 했던 함수, 인수가 전달 됨
      • -> createProduct(requestDto)

스프링 AOP 어노테이션

1. @Aspect

  • 스프링 빈(Bean) 클래스에만 적용 가능

2. Advice(어드바이스) 종류

  • @Aroud : "핵심기능" 수행 전과 후(@Beafore + @After)
  • @Before : '핵심기능' 호출 전 (ex. Client의 입력값 Validation 수행)
  • @After : '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (Try, Catch의 finally()처럼 동작)
  • @AfterReturning : '핵심기능' 호출 성공시에만 (함수의 return값 사용 가능)
  • @AfterThrowing: '핵심기능' 호출 실패 시, 즉 예외(Exception) 발생한 경우만 동작 (ex. 예외 발생했을 때 개발자에게 email)

3. 포인트 컷

  • 포인트 컷 Expression Langauage
    • 포인트컷 Expression 형태 (? 는 생략 가능)  
execution(modifiers-pattern? 
		return-type-pattern 
            declaring-type-pattern? 
            method-name-pattern(param-pattern) 
            throws-pattern?)

 

  • 포인트컷 Expression 예제
    @Around("execution(public * com.sparta.springcore.controller..*(..))")
    		public Object execute(ProceedingJoinPoint joinPoint) 
            throws Throwable { ... }​
    • modifiers-pattern
      • public, private, *
    • return-type-pattern
      • void, String, List<String>, *****
    • declaring-type-pattern
      • 클래스명 (패키지명 필요)
      • com.sparta.springcore.controller.* - controller 패키지의 모든 클래스에 적용
      • com.sparta.springcore.controller.. - controller 패키지 및 하위 패키지의 모든 클
    • method-name-pattern(param-pattern)
      • 함수명
        • addFolders : addFolders() 함수에만 적용
        • add* : add 로 시작하는 모든 함수에 적용
      • 파라미터 패턴 (param-pattern)
        • (com.sparta.springcore.dto.FolderRequestDto) - FolderRequestDto 인수 (arguments) 만 적용
        • () - 인수 없음
        • (*) - 인수 1개 (타입 상관없음)
        • (..) - 인수 0~N개 (타입 상관없음)
  • @Pointcut
    • 포인트컷 재사용 가능
    • 포인트컷 결합 (combine) 가능
@Component
@Aspect
public class Aspect {
	@Pointcut("execution(* com.sparta.springcore.controller.*.*(..))")
	private void forAllController() {}

	@Pointcut("execution(String com.sparta.springcore.controller.*.*())")
	private void forAllViewController() {}

	@Around("forAllContorller() && !forAllViewController")
	public void saveRestApiLog() {
		...
	}

	@Around("forAllContorller()")
	public void saveAllApiLog() {
		...
	}	
}

 


AOP 예시

각 모듈의 실행시간을 측정하여, 사용자의 서비스 사용시간 저장하는 AOP 코드

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class UseTimeAop {
    private final ApiUseTimeRepository apiUseTimeRepository;

    public UseTimeAop(ApiUseTimeRepository apiUseTimeRepository) {
        this.apiUseTimeRepository = apiUseTimeRepository;
    }

    @Around("execution(public * com.sparta.springcore.controller..*(..))")
    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();

                // API 사용시간 및 DB 에 기록
                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);
            }
        }
    }
}

 

 

 

 

*참고

https://engkimbs.tistory.com/746