240227 TIL #332 AOP / 부가기능 / AspectJ

김춘복·2024년 2월 27일
0

TIL : Today I Learned

목록 보기
332/494

Today I Learned

@Transactional 을 어제 공부하다가 AOP로 작동한다는 것을 좀 더 파보기 위해 AOP를 좀 더 공부해보았다.


AOP

(예시) 서버 사용시간 측정

  • 가장 많이 이용한 사용자 5명에게 선물을 주는 Event
    이용의 기준을 그냥 켜놓고 놔둔 시간이 아니라 서버 사용 시간을 기준으로 했을 때,
    'Controller에 요청이 들어온시간 - Controller에서 응답이 나간 시간' 으로 계산.

  • 구현
    시간 측정하는 Entity 만들어서 user_id를 FK로 불러오고 시간 측정 컬럼 만들어둠.
    repo도 하나 만들고 측정하고자 하는 Controller의 API에 다음과 같이 코딩

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();
        // API 사용시간 및 DB 에 기록
        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);
    }
}
  • 그런데 이벤트를 정확하게 적용하려면 모든 API에다가 위의 코드를 적용시켜야 한다.
    코드의 길이도 길어지고 가독성도 나빠지고 비효율적이다.
    그래서 이를 편하게 적용 시켜주는 것이 바로 AOP이다.

AOP

  • 핵심기능 : 각 API별 수행해야 할 비즈니스 로직
    ex) 상품 검색, 회원가입, 로그인, 관심 폴더에 저장 등

  • 부가기능 : 핵심기능을 보조하는 기능
    ex) 회원 패턴 분석을 위한 로그 기록, API 수행시간 저장

위의 예시와 같이 모든 핵심기능에 부가기능을 추가해야 할 경우 가독성이 떨어지고 코드가 길어지며 번거로워질 가능성이 생긴다.
이를 해결하기 위해 AOP를 통해 부가기능을 모듈화해서 사용할 수 있다.

AOP

Aspect Oriented Programming. 관점 지향 프로그래밍.

어떤 로직을 기준으로 핵심적인 관점 / 부가적인 관점으로 나누어서 보고
그 관점을 기준으로 각각을 모듈화(공통된 로직이나 기능을 하나의 단위로 묶음) 한다는 것.
코드 상에서 다른부분에 계속 반복해서 쓰는 코드를 흩어진 관심사(Crosscutting Concerns), (주로 부가기능)라 한다. 이런 부가기능들을 Aspect로 모듈화 하고 핵심기능(비즈니스 로직)에서 분리해 재사용 하는것이 AOP의 취지이다.

  • Aspect : =Advice+Pointcut 흩어진 관심사(주로 부가기능)들을 모듈화 한 것.
    Target: Aspect를 적용하는 곳. AOP가 적용 될 대상 객체 ex) 클래스, 메서드 등
    Advice : 실질적인 부가기능을 담은 구현체. JoinPoint에서 실행되는 코드.
    JoinPoint : Advice가 적용될 위치. 실행지점. 끼어들 수 있는 지점.
    PointCut : JoinPoint의 상세한 스펙을 정의한 것. 구체적으로 Advice가 실행될 지점을 정함

AOP를 사용해 부가기능 구현

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class UseTimeAop {

    private final ApiUseTimeRepository apiUseTimeRepository;

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

                log.info("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
                apiUseTimeRepository.save(apiUseTime);

            }
        }
    }
}
  • @Aspect : 해당 클래스가 Aspect(부가기능이 모듈화된 것)임을 명시.
    @Component : 부가기능도 Bean으로 등록해야 한다. 스프링에선 Bean에만 AOP를 적용할 수 있다.
    @Around("execution(public * 적용할패키지경로..*(..))") : 해당 경로에 아래의 메서드를 동작 시키겠다. (이 포스트의 하단에 자세하게 설명)

  • Object output = joinPoint.proceed();
    return output;
    : 핵심 기능(비즈니스 로직)을 실행한다는 의미. 이 코드가 있는 위치에서 핵심기능이 수행된다.


AOP in Spring

스프링에서는 위의 경로를 통해 AOP가 적용된다.
AOP가 없을때는 DispatcherServelet에서 Controller로 바로 넘어가지만,
AOP를 사용하면 앞에 Proxy객체가 먼저오고 Advice가 실행된 다음 그 안의
joinPoint.proceed() 코드 자리에서 Controller의 핵심기능이 실행된다.
joinPoint.proceed()에 의해 원래 호출하려 했던 함수와 인수가 전달되니
부가기능이 추가된 것 말고는 DispatcherServlet과 Controller입장에서 달라질 것은 전혀 없다.

  • Build.gradle의 dependancies에 아래 코드 넣으면 사용가능
    (Spring JPA나 WEB을 쓰면 이미 적용되어있을 가능성 높다)
    // Spring AOP
		implementation 'org.springframework.boot:spring-boot-starter-aop'
  • Spring AOP는 Proxy 기반의 AOP 프레임워크. 대상 객체에 Aspect를 적용하기 위해 대상 객체의 Proxy를 생성해 사용.

스프링의 AOP @애너테이션

이부분은 외우려하지말고 필요할때 검색으로 찾아서 사용하면 된다.

@Aspect : AOP 부가기능임을 선언. Bean에만 사용 가능하니 @Component도 꼭 같이 써야

  • Advice 종류(AOP 코드가 실행되는 시점을 지정. AOP안의 메서드 위에다 단다)
    @Around : 핵심기능 수행 전과 후(@Before + @After)
    @Before : 핵심기능 호출 전. ex) client의 입력값 validation 수행
    @After : 핵심기능 수행 실패/성공과 상관없이 언제나 동작. try-catch의 finally()처럼 동작
    @AfterReturning : 핵심기능 호출 성공시. 함수의 return값 사용 가능
    @AfterThrowing : 핵심기능 호출 실패시. 즉 예외 발생시 동작. ex)예외발생시 개발자에게 email

PointCut 문법

구체적으로 advice가 실행되는 장소를 지정. ?는 생략가능

execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
// 예시
@Around("execution(public * com.sparta.springcore.controller..*(..))")
  • modifiers-pattern(제어자) : public, private, *
  • return-type-pattern(반환타입) : void, String, int, List<User>, * 등
  • declaring-type-pattern : 클래스명(패키지명도 필요). 아래는 예시
    com.sparta.controller.* : *로 끝나면 controller 패키지의 모든 클래스에 적용
    com.sparta.controller.. : ..으로 끝나면 controller 패키지 및 하위 패키지의 모든 클래스에 적용
  • method-name-pattern(param-pattern) : 함수명(파라미터 패턴). 아래는 예시
    addFolders : addFolders() 함수에만 적용
    add* : add 로 시작하는 모든 함수에 적용
    (com.sparta.dto.FolderRequestDto) : FolderRequestDto의 파라미터에만 적용
    () : 인수없음
    (*) : 인수 1개(타입상관x)
    (..) : 인수 0~N개(타입상관x)

*은 모든 경우가 다 가능하다는 의미

  • @Pointcut : 이걸로 등록한 Pointcut은 등록후 재사용이나 결합이 가능.
@Component
@Aspect
public class Aspect {
	@Pointcut("excution pointcut경로1")
	private void forAllController() {}

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

AspectJ와 Spring AOP의 차이점

출처 : 1HOON님 블로그

  • Spring AOP는 Spring IoC를 통한 간단한 AOP의 구현이 목적이다.
    완전한 AOP 구현을 의도하지 않고 Spring Container에 의해 관리되는 Bean에만 적용이 가능.
    런타임 weaving. Proxy기반 AOP. 순수 java만으로도 구현이 된다.

  • 반면 AspectJ는 완전한 AOP를 제공하는 것이 목적인 근원적인 AOP 기술이다.
    Spring AOP보다 강력하고 복잡하다. Bean이 아닌 모든 객체에 적용이 가능하다.
    컴파일이나 로드타임 weaving. 클래스들이 Aspect와 같이 컴파일되기때문에 런타임시에는 아무것도 하지 않는다. 구현시 추가 도구가 필요.

Weaving : AOP가 적용되는 시점. 컴파일타임/로드타임/런타임.

  • 성능은 AspectJ가 월등하지만 복잡하다.
profile
꾸준히 성장하기 위해 매일 log를 남깁니다!

0개의 댓글