Back-End/Spring Advance & Boot

스프링 고급편 - 스프링 AOP 구현

Meluu_ 2024. 10. 31. 11:59

✔️ 구현


@Slf4j
@Aspect
public class AspectV1 {

    @Around("execution(* hello.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
}

AspectV1을 만든다. 

@Around : 포인트 컷 부분에 해당

* 모든 타입, hello.aop.order 패키지와 그 하위 패키지(..)를 지정 // 자세한건 다음 포스트에서 설명

@Around가 달린 메서드 doLog는 어드바이스가 된다.

 

 

 

 

✔️ 스프링 빈 등록


@Aspect가 달린 어드바이저는 스프링 빈으로 등록해줘야한다.

 

방법

1. @Bean을 사용해 직접 등록

2. @Component 컴포넌트 스캔을 사용해 자동 등록

3. Import 주로 설정 파일을 추가할 때 사용 (@Configuration) 

 

@Import가 빈 등록이 되는 이유

스프링 부트가 기본적으로 AOP 관련 설정을 해주기에 @EnableAspectJAutoProxy를 명시하지 않아도 AOP 프록시가 활성화 된다. 이는 스프링 부트의 자동 설정(Auto-Configuration)기능 덕분이다.

 

 

 

참고

스프링은 프록시 방식의 AOP를 사용, @Aspect 는 AspectJ가 제공하는 애노테이션

 

 

 

✔️ 포인트 컷 분리


@Slf4j
@Aspect
public class AspectV2 {

    // hello.aop.order 패키지와 하위 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    private void allOrder(){} // pointcut signature


    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
}

 

@Pointcut 에 포인트컷 표현식을 사용

메서드 이름 + 파라미터 = 포인트컷 시그니처

메서드의 반환 타입은 void 여야 한다.

코드 내용은 비움

 

내부에서만 사용시 private, 다른 애스펙트에 참고시 public 사용

 

포인트컷 시그니처로 포인트 컷을 지정할 수 있다.

 

 

 

✔️ 타입 이름 패턴과 포인트컷 조합


@Slf4j
@Aspect
public class AspectV3 {

    //allOrder() 포인트컷 시그니처..


    //클래스 이름 패턴이 *Service
    @Pointcut("execution (* *..*Service.*(..))") 
    private void allService() {}


    //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
    @Around("allOrder() && allService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[resource release] {}", joinPoint.getSignature());

        }
    }


}

 

패턴적용도 가능하다.

*xx 면 xx로끝나는 대상을 지정한다. 이를 타입 이름 패턴이라 한다. (클래스, 인터페이스에 모두 적용 가능)

 

또한 포인트컷 조합도 가능하다. &&(AND), ||(OR), !(Not) 3가지 조합이 가능

 

 

포인트 컷 모아두기

// 포인트 컷 모아놓고 사용
public class Pointcuts {
    // hello.aop.order 패키지와 하위 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    public void allOrder(){} // pointcut signature


    //클래스 이름 패턴이 *Service
    @Pointcut("execution (* *..*Service.*(..))")
    public void allService() {}

    @Pointcut("allOrder() && allService()")
    public void orderAndService() {}

}

 

이런식으로 포인트컷만 모아두고 다른 곳에서 불러서 사용 가능하다.

포인트 컷도 조합으로 만들 수 있다.

 

@Slf4j
@Aspect
public class AspectV4Pointcut {

    @Around("hello.aop.order.aop.Pointcuts.allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
	//...
}

패키지 명을 다 적어줘야한다.

 

 

 

 

✔️ 어드바이스 순서


어드바이스는 기본적으로 순서 보장 X

순서 지정을 하고 싶다면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애노테이션을 적용해야한다.

그런데 이 애노테이션은 클래스 단위로 적용 가능하다. 따라서 애스펙트를 별도의 클래스로 분리 해야한다.

 

트랜잭션 먼저 - 그다음 로그

@Slf4j
@Aspect
public class AspectV5Order {

    @Aspect
    @Order(2) // 숫자가 낮을 수록 우선순위
    public static class LogAspect {
        @Around("hello.aop.order.aop.Pointcuts.allOrder()")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[log] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

    @Aspect
    @Order(1)
    public static class TxAspect {
        //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
        @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
        public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
            try {
                log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
                Object result = joinPoint.proceed();
                log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
                return result;
            } catch (Exception e) {
                log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
                throw e;
            } finally {
                log.info("[resource release] {}", joinPoint.getSignature());

            }
        }

    }
}

 

 

 

✔️ 어드바이스 종류


 

@Around : 메서드 호출 전후 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택,

                   반환 값 변환, 예외 변환, try-catch-finall이 모두 들어가는 구문 처리  가능, proceed()를 여러번실행 가능(재시도) 

@Before : 조인 포인트 실행 이전에 실행

@AfterReturning : 조인 포인트가 정상 완료후 실행

@AfterThrowing : 메서드가 예외를 던지는 경우 실행

@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

 

 

 

@Slf4j
@Aspect
public class AspectV6Advice {

    //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
    @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            // @Before
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            // @AfterReturning
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            // @AfterThrowing
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            // @After
            log.info("[resource release] {}", joinPoint.getSignature());
        }
    }
    
    
    // @Before
    // 조인 포인트 전에 간단하게만 작성, 그 이후는 알아서 처리해줌(자동 다음 타겟 호출)
    // 작업 흐름변경 불가, 예외 발생시 다음코드 호출 X  
    @Before("hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("[before] {}", joinPoint.getSignature());
    }

    // @AfterReturning
    // returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야함
    // returning 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행 (부모 타입을 지정시 모든 자식 타입 인정)
    // 리턴 값을 조작할 순 있지만 바꿀 순 없다. (다른 객체 반환 불가)
    @AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
    public void doReturn(JoinPoint joinPoint, Object result) {
        log.info("[return] {} return={}", joinPoint.getSignature(), result);
    }

    // @AfterThrowing
    // throwing 속성에 사용된 이름은 어드바이스 매서드의 매개변수 이름과 일치해야함
    // throwing 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행 (부모 타입을 지정시 모든 자식 타입 인정)
    @AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
    public void doThrowing(JoinPoint joinPoint, Exception ex) {
        log.info("[ex] {} message={}", ex, ex.getMessage());
    }

    // @Around
    // 메서드 종료시 실행
    // 정상 및 예외 반환 조건을 모두 처리
    // 일반적으로 리소스 해제시 사용
    @After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("[after] {}", joinPoint.getSignature());
    }
}

 

 

 

✔️ 참고 정보 획득


모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용 가능 (생략 가능)

단 @Around 는 ProceedingJoinPoint(JoinPoint 하위 타입) 를 사용해야한다.

 

 

JoinPoint 인터페이스의 주요 기능

  • getArgs() : 메서드 인수를 반환
  • getThis() : 프록시 객체를 반환
  • getTarget() : 대상 객체를 반환
  • getSignature() : 조언되는 메서드에 대한 설명을 반환
  • toString() : 조언되는 방법에 대한 유용한 설명을 인쇄

 

 

ProceedingJoinPoint 인터페이스의 주요 기능

  • procced() : 다음 어드바이스나 타겟 호출

 

 

 

✔️@Around 외에 다른 어드바이스가 존재하는 이유


내가 이해한 바로는 각각의 역할이 있다고 본다. @Around를 사용시 모든 처리가 가능하지만 간단하게 실행 시 로그만 찍는 것이 필요한 경우는 굳이 @Around를 쓸 필요없지 않는가? 간단하게 @Before를 사용하는 것이 좋다.

괜히 @Around 사용하려고 만들다가 로직을 잘못짜서 에러가 나는 것보다 @Before을 사용해서 로그만 찍고 그 뒷부분은 자동으로 처리하게 하는게 더 좋다.

 

좋은 설계는 제약이 있는 것이다. (교재) 

 

 

 

 

🔖 학습내용 출처


스프링 핵심 원리 - 고급편 / 김영한