본문 바로가기
프로그래밍/kotlin

[Spring] 스프링 AOP 정리

by 뜨끔쓰 2022. 10. 9.
728x90
728x90
인프런의 김영한님의 강의 스프링 핵심 원리 - 고급편을 학습하며 정리한 글입니다.

 

스프링 핵심 원리 - 고급편 (인프런)

 

스프링 핵심 원리 - 고급편 - 인프런 | 강의

스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

스프링 AOP란?

AOP(Aspect-Oriented Programming)은 Aspect는 우리말로 해석하면 관점이라는 뜻인데, 이름 그대로 애플리케이션을 바라보는 관점을 하나하나의 기능에서 횡단 관심사(cross-cutting concerns)관점으로 달리 보는것이다.

 

그림으로 보면 다음과 같다.

출처: 김영한님의 스프링핵심원리 고급편

이미 작성된 코드들의 하나하나 부가기능을 추가하긴 여간 쉬운일이 아니기 때문에 한꺼번에 횡단하는 로직을 추가하는 것 쉽게 생각하면 이걸 AOP라고 생각하면 될 것 같다.

 

AsjectJ 프레임워크는 스스로를 다음과 같이 설명한다고 한다.
  • 자바 프로그래밍 언어에 대한 완벽한 관점 지향 확장
  • 횡단 관심사의 깔끔한 모듈화
    1. 오류 검사 및 처리
    2. 동기화
    3. 성능 최적화(캐싱)
    4. 모니터링 및 로깅

 

AOP의 적용 방식은 크게 3가지가 있다.
  • 컴파일 시점
  • 클래스 로딩 시점
  • 런타임 시점(프록시)

하지만 우리는 스프링 AOP를 사용하기 때문에 프록시를 이용한 런타임 시점의 방식을 사용한다고 생각하면된다.

 

왜냐하면 컴파일 시점이나 클래스 로딩 시점에 AOP를 적용하기 위해서는 AspectJ를 직접 사용해야 하기 때문에 번거롭기 때문이다.

 

 

프록시 기반의 스프링 AOP의 적용 위치

스프링 컨테이너가 관리 할 수 있는 스프링 빈에만 AOP가 적용 가능하다.

Why?

=> 프록시는 메서드 오버라이딩 개념으로 동작하기 때문에 생성자나 static 메서드, 필드 값 접근에는 적용이 불가능하다.

 

그렇기 때문에 같은 클래스 내부 메서드를 호출하는 경우에도 AOP를 적용이 불가능하다.

 

내부 메서드의 경우 프록시 클래스에서 호출하는것이 아닌 타겟 클래스에서 내부 메서드를 호출하는 것이기 때문이다.

 

 

 

AOP의 주요 용어 정리
  • 조인 포인트(Join point)
    포인트컷을 통해 선별된 어드바이스가 적용될 수 위치 (ex. 메소드 실행, 생성자 호출, 필드 값 접근같은 프로그램 실행 중 지점)

  • 포인트 컷(Pointcut)
    조인 포인트 중에서 어드바이스가 적용 될 위치를 선별하는 기능
    스프링 AOP는 메서드 실행 지점만 선별 가능

  • 타겟(Target)
    어드바이스를 받는 객체, 포인트컷으로 결정 (ex. 프록시 객체가 아닌 실제 객체)

  • 어드바이스(Advice)
    특정 조인 포인트에서 Aspect에 의해 취해지는 조치 또는 부가기능

  • 애스펙트(Aspect)
    어드바이스 + 포인트 컷을 모듈화 한 것이다. (ex. @Aspect 어노테이션)

  • 어드바이저(Advisor)
    하나의 어드바이스 + 포인트 컷으로 구성된 스프링 AOP에서만 사용하는 특별한 용어이다.

  • 위빙(Weaving)
    포인트컷으로 결정한 타겟의 조인 포인트에 어드바이스를 적용하는 것
    핵심 기능 코드에 영향을 주지 않고 부가기능을 추가 할 수 있다.

  • AOP 프록시
    AOP 기능 구현을 위해 만든 프록시 객체 스프링에서는 JDK 동적 프록시 또는 CGLIB 프록시이다.

 

스프링 AOP에서 상용되는 주요 어노테이션들
  • @Aspect
    Aspect클래스를 작성할때 사용한다. => 스프링 컨테이너에 AOP 담당 객체임을 알림

  • @Around => (Advice 종류)
    메서드의 실행의 주변에서 실행, 메서드 실행 전후에 작업을 수행함. 가장 강력한 어드바이스
    조인 포인트 실행 여부 선택 ( joinPoint.proceed() 호출 여부 선택 )
    전달 값 변환: joinPoint.porceed(args[])
    반환 값 변환
    예외 변환
    트랜잭션 처럼 try ~ catch ~ finally 모두 들어가능 구문 처리가능
    어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint를 사용해야함
    proceed()를 통해 타겟을 실행, 여러번 실행도 가능함(재시도)

  • @Before => (Advice 종류)
    조인포인트 실행전 @Around와는 다르게 작업 흐름을 변경할 수는 없다.

  • @After => (Advice 종류)
    메서드 실행이 종료되면 실행된다. (ex. finally와 유사)
    정상 및 예외 반환 조건을 모두 처리, 일반적으로 리소스 해제하는 데 사용

  • @AfterReturning => (Advice 종류)
    메서드 실행이 정상적으로 반환될 땔 실행

  • @AfterThrowing => (Advice 종류)
    메서드 실행이 예외를 던져서 종료될 때 실행

 

포인트컷 표현식

작성 방법은 여러가지가 있지만 와일드 카드를 이용하여 작성하며 다음과 같은 기호를 사용한다.

  • * : 모든 것
  • .. : 0개 이상

 

포인트컷 지시자의 종류
  • execution
    메소드 실행 조인 포인트를 매칭, 가장 많이 사용됨 기능도 복잡
    EX) execution([접근제어자] 반환타입 패키지.패키지.패키지.패키지.클래스.메소드(인자))

  • within
    특정 타입 내의 조인 포인트를 매칭, 부모타입을 지정하면 적용되지 않음
    EX) within(패키지.패키지.패키지.패키지.클래스)

  • args
    인자가 주어진 타입의 인스턴스인 조인 포인트

  • this
    스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트

  • target
    스프링 AOP가 가르키는 실제 대상으로 하는 조인 포인트

  • @target
    실행 객체의 클래스에 주어진 타입의 어노태이션이 있는 조인 포인트
    부모타입 메서드도 적용

  • @within
    주어진 어노테이션이 있는 타입 내 조인 포인트
    부모타입 메서드는 적용되지 않음

  • @annotation
    메서드가 주어진 어노테이션을 가지고 있는 조인 포인트를 매칭

  • @args
    전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트

  • bean
    스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정

 

포인트컷 지시자 적용 주의사항

args, @args, @target의 경우 단독으로 사용하면 오류가 발생합니다.

execution등과 함께 사용하여 적용 대상을 줄여주어야 합니다.

 

3개의 포인트컷 지시자의 경우 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인 할 수 있기 때문에 프록시를 미리 생성해주어야 합니다.

 

이때 모든 스프링 빈에 AOP를 적용하려 할텐데 final로 지정된 빈들이 등록될 때 프록시를 만들지 못하는 오류가 발생 할 수 있습니다. 

 

 

 

이제 실제 코드로 확인 해보도록하자.


@AOP

먼저 스프링 AOP를 사용하기 위해선 의존성 추가가 필요하다.

Gradle을 이용할 것 이기 때문에 다음과 같이 추가하였습니다.

implementation("org.springframework.boot:spring-boot-starter-aop")

간단한 예제 코드를 작성해보도록 합시다.

OrderService에서 OrderRepository를 호출하는 형태의 구조로 작성하였습니다.

 

OrderService
@Service
class OrderService(
    private val orderRepository: OrderRepository,
) {
    private val log = LoggerFactory.getLogger(OrderService::class.java)

    fun orderItem(itemId: String){
        log.info("[orderService] 실행")
        orderRepository.save(itemId)
    }
}

OrderService Class입니다. orderItem 함수는 orderRepository의 save함수를 호출해주고 있습니다.

실행전 orderService의 실행 로그를 찍는 간단한 함수입니다.

 

OrderRepository
@Repository
class OrderRepository {
    private val log = LoggerFactory.getLogger(OrderRepository::class.java)
    fun save(itemId: String): String {
        log.info("[orderRepository] 실행")
        return "ok"
    }
}

OrderRepository Class입니다. 마찬가지로 실행전 로그를 찍고 결과 값으로 ok를 리턴해주는 간단한 함수입니다.

 

이제 스프링 AOP를 적용하기 위해 Aspect를 나타날 클래스를 작성해보도록 합시다.

 

AspectAdvice
@Aspect
class AspectAdvice {
    private val log = LoggerFactory.getLogger(AspectAdvice::class.java)
    
    @Around("execution(* hello.aop.order.OrderRepository.*(..))")
    fun doLogging(joinPoint: ProceedingJoinPoint){
        log.info("실행전 로깅!!!")
        joinPoint.proceed()
        log.info("실행후 로깅!!!")
    }

 

클래스위에 @Aspect를 추가하여 스프링 컨테이너에 AOP 담당 객체임을 알린다.

 

참고로 Component 스캔에 걸리지 않기 때문에 @Component를 함께 사용하여아 한다. 

 

하지만 지금은 테스트 코드 상에서 @Import를 이용할 것이기 때문에 추가하지 않는다.

 

다음으로 @Around어노테이션의 execution(* hello.aop.order.OrderRepository.*(..)) 구문이 보일 것이다.

 

위의 포인트컷 지시자 부분을 참고하여 해석해보도록하자.

 

AopTest
@SpringBootTest
@Import(value = [AspectAdvice::class])
class AopTest(
    @Autowired
    val orderService: OrderService,
    @Autowired
    val orderRepository: OrderRepository,
) {
    private val log = LoggerFactory.getLogger(AopTest::class.java)
    

    @Test
    fun success() {
        orderService.orderItem("itemA")
    }
}

테스트 코드를 보면 OrderServce, OrderRepository를 의존성 주입 받아 orderService의 orderItem 함수를 호출 해준다.

 

테스트 코드 실행

코드를 실행하면 우리가 기대한 바와 같이 orderRepository의 save 메소드 호출 전후로 로깅이 정상적으로 찍히는 것을 확인 할 수있다.

 

이처럼 스프링 AOP를 이용하면 원본 코드를 수정하지 않고 원하는 기능들을 추가 할 수 있다.

 

물론 이글에서는 매우 기본적인 부분들만 설명 및 정리 한 글이기때문에 더욱 깊이있는 사용 방법등은 구글링을 통해 필요할때마다 검색하여 사용하면 될 것 같다.

728x90
반응형

댓글