스프링 부트 - 스프링 AOP

SeungTaek·2021년 11월 7일
0
post-thumbnail

본 게시물은 스스로의 공부를 위한 글입니다.
잘못된 내용이 있으면 댓글로 알려주세요!

📒 핵심 기능과 부가 기능

  • 핵심 기능은 해당 객체가 제공하는 고유의 기능
    • EX) 주문로직, 회원가입, 로그인
  • 부가 기능은 핵심 기능을 보조하기 위헤 제공되는 기능
    • EX) 로그 로직, 트랜잭션 기능 등
    • 부가 기능은 단독으로 사용되지 않고, 핵심 기능과 함께 사용된다.



📌 부가 기능

  • 보통 부가 기능은 여러 클래스에 걸쳐서 함께 사용된다.
    • 이런 부가 기능을 횡단 관심사(cross-cuting concerns)라 한다.
  • 부가 기능을 적용해야 하는 클래스가 여러개면 모두 동일한 코드를 추가해야 한다.
    • 단순 호출 뿐 아니라 다른 로직까지 들어가게되면 더 복잡해진다.
    • 수정을 해야한다면? 사용된 클래스 모두를 다 찾아가면서 수정해야 한다.
  • 따라서 다음과 같은 적용 문제를 갖게된다.
    • 아주 많은 중복 코드가 생성된다.
    • 부가 기능을 변경할 때 많은 수정이 필요하다.
    • 적용 대상을 변경할 때도 많은 수정이 필요하다.


📌 부가 기능 문제 해결사 AOP

  • 부가 기능을 핵심 기능에서 분리하고 한 곳에서 관리하기 위해 하나의 모듈로 만들었는데, 바로 애스펙트(aspect)이다.
  • 애스펙트는 부가 기능 로직 + 해당 부가 기능을 어디에 적용할지 정의한 것이다.
    • EX) 로그 출력 기능을 모든 컨트롤러에 적용해라!
  • 스프링이 제공하는 어드바이저(어드바이스 + 포인트컷)도 개념상 하나의 애스펙트이다.
  • 이렇게 애스펙트를 사용한 프로그래밍 방식을 관점 지향 프로그램 AOP(Aspect-Oriented Programming)이라 한다.


📌 AspectJ 프레임워크

  • AOP의 대표적인 구현으로 AspectJ 프레임워크가 있다.
  • 정말 많은 기능이 있지만, 공부 내용도 많고 설정도 복잡하다.
  • 스프링은 AspectJ의 문법을 차용하고 프록시 방식의 AOP를 적용한다. AspectJ를 직접 사용하는게 아니다.
    • 실무에서는 스프링이 제공하는 AOP 기능만 사용해도 대부분의 문제를 해결할 수 있다.
    • 본 게시물에서도 스프링이 제공하는 AOP만 다룬다.



📒 AOP 용어 정리

  • 조인 포인트(Join point)
    • 어드바이스가 적용될 수 있는 위치: 메소드, 생성자, 필드 값 접근 등
    • 스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메소드 실행 지점이다.
  • 포인트컷(Pointcut)
    • 조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능
    • 주로 AspectJ 표현식을 사용해서 지정
  • 타겟(Target)
    • 어드바이스를 받는 객체, 포인트컷으로 결정
  • 어드바이스(Advice)
    • 부가 기능 로직
  • 어드바이저(Advisor)
    • 하나의 어드바이스 + 하나의 포인트컷



📒 예제

  • 프로젝트 생성
    • 디펜던시: Lombok, aop
    • aop는 아래 코드 추가
implementation 'org.springframework.boot:spring-boot-starter-aop'

  1. 🎈 포인트컷+어드바이스
@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();
    }
}
  • @Around로 포인트컷 설정
    • 포인트컷 문법은 여기 참고
    • hello.aop.order 패키지와 하위 패키지의 모든 메서드 범위에 적용한다는 뜻이다.
  • joinPoint.proceed()로 타겟 호출



  1. 🎈 포인트컷을 메서드로 분리
@Slf4j
@Aspect
public class AspectV3 {
    @Pointcut("execution(* hello.aop.order..*(..))")
    private void allOrder(){}

    @Pointcut("execution(* *..*Service.*(..))")
    private void allService(){}

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

    @Around("allOrder() && allService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log2] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
}
  • 포인트컷을 다른 메서드로 분리 후 @Around( {메소드명} )으로 참조할 수 있다.

  • 포인트컷 조합은 &&, ||, ! 이 가능하다.



  1. 🎈 포인트컷을 다른 클래스로 완전 분리할 수 있다.
public class Pointcuts {
    @Pointcut("execution(* hello.aop.order..*(..))")
    public void allOrder(){}
    
    @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("[log1] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }

    @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log2] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
}
  • @Around에 포인트컷 패지지명+메서드명을 써주면 된다.
  • 단, 포인트컷의 메소드가 public임을 주의



  1. 🎈 어드바이스 순서
@Slf4j
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 {
        @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
        public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        	log.info("[log2] {}", joinPoint.getSignature());
       		return joinPoint.proceed();
        }
    }
}
  • @Order()로 어드바이스 실행 순서를 지정할 수 있다.
  • 단, 클래스 단위로만 가능하므로 내부 클래스 생성 or 외부 클래스 분리



  1. 🎈 다양한 어드바이스 종류
@Slf4j
@Aspect
public class AspectV6Advice {

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

    @Before("hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("[before] {}", joinPoint.getSignature());
    }

    @AfterReturning(value="hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
    public void doReturn(JoinPoint joinPoint, Object result){
        log.info("[AfterReturning] {} return={}", joinPoint.getSignature(), result);
    }

    @AfterThrowing(value="hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
    public void doThrowing(JoinPoint joinPoint, Exception ex){
        log.info("[ex] {} message={}", joinPoint.getSignature(), ex);
    }

    @After(value="hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doAfter(JoinPoint joinPoint){
        log.info("[after] {}", joinPoint.getSignature());
    }
}
  • 어드바이스 종류
    • @Around: 메서드 호출 전후에 수행, 조인 포인트 실행 여부, 반환 값 변환, 예외 변환 등 가능
      • 반드시 .proceed()를 해야 다음 타겟이 실행된다. 여러번 호출도 가능
    • @Before: 조인 포인트 실행 이전에 실행
      • 메서드 종료시 자동으로 다음 타켓 호출
    • @AfterReturning : 조인 포인트가 정상 완료후 실행
      • returning속성에 사용된이름은 매개변수 이름과 일치해야 한다.
      • 이때 반환 타입에 주의해야 한다. (해당 타입이나 상위 타입으로 리턴값을 받아야 함)
    • @AfterThrowing : 메서드가 예외를 던지는 경우 실행
      • throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
      • 이때 예외 타입에 주의해야 한다. (해당 타입이나 부모 타임으로 받아야 함)
      • 메소드 종료 후 자동으로 throw e가 실행된다.
    • @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
      • 정상 및 예외 반환 조건을 모두 처리한다.
      • 일반적으로 리소스를 해제하는데 사용한다.

  • 사실상 @Around만 있으면 모든 어드바이스 처리가 가능하다.
  • 근데 왜 다양한 다른 어드바이스가 존재할까?
    1. @Around는 항상 joinPoint.proceed()를 호출해야 하므로 개발자가 실수할 여지가 있다.
      • 다른 어드바이스는 메서드 종료 후 타겟을 자동 호출하거나, 타겟 호출 후 나중에 메서드를 실행하므로 필요없다.
    2. 코드를 작성한 의도가 명확하게 들어난다.
      • @Before라는 애노테이션을 보는 순간 개발자의 의도를 쉽게 파악할 수 있지만, @Around는 의도를 해석해야 한다.

인프런의 '스프링 핵심 원리 고급편(김영한)'을 스스로 정리한 글입니다.
자세한 내용은 해당 강의를 참고해주세요.

profile
I Think So!

0개의 댓글