고딩왕 코범석

스프링 AOP 본문

Language & Framework/Spring

스프링 AOP

고딩왕 코범석 2021. 12. 18. 22:18
반응형

안녕하세요! 이번 포스팅에서는 AOP에 대해 포스팅해보려고 합니다. 김영한 님의 스프링 핵심 원리 - 고급편을 참조하여 정리했습니다!

🙆🏻‍♂️ Aspect(관점)와 AOP

Aspect는 부가 기능과 부가 기능을 어디에 적용할지 정의한 것입니다. 어드바이저(포인트컷 + 어드바이스)도 개념상 하나의 Aspect라 볼 수 있습니다.


관점이라는 말 대로 애플리케이션을 바라보는 관점을 하나의 기능에서 횡단 관심사 관점으로 달리 보는 것이라고 할 수 있습니다. 메서드에 공통으로 들어가는 로직(트랜잭션 호출, 로깅 등등..)이라고 이해하면 될 것 같습니다.


이렇게 Aspect를 사용한 프로그래밍 방식을 AOP(Aspect-Oriented-Programming)이라고 합니다.

🖥 AOP 적용 방식

1. 컴파일 시점

.java 소스 코드를 컴파일러로 .class를 만드는 시점에 부가 기능 로직을 추가할 수 있습니다. 이 때 AspectJ가 제공하는 특별한 컴파일러를 사용해야하는데, 쉽게 얘기하자면 컴파일된 코드 주변에 부가 기능을 붙여버린다고 생각하면 됩니다. 이렇게 원본 로직에 부가 기능 로직이 추가되는 것을 위빙(Weaving) 이라고 합니다.


AspectJ가 제공하는 컴파일러를 이용해야 하기 때문에 번거롭다는 단점이 있어 잘 사용하지 않는 방법입니다.

2. 클래스 로딩 시점

자바를 실행하면 .class파일을 JVM 내부의 클래스 로더에 보관합니다. 이 때 중간에서 .class를 조작한 다음 JVM에 올릴 수 있는데, 이 방법을 로드 타임 위빙(Load Time Weaving)이라고 합니다.


자바를 실행할 때, 옵션을 통해 로더 조작기를 지정해야 하는데 이 방법도 번거로워서 사용하지 않습니다.

3. 런타임 시점(프록시)

컴파일도 끝나고, 클래스 로더에 클래스도 다 올라가서 이미 자바가 실행되고 난 다음을 의미합니다. 스프링 컨테이너의 도움을 받고 프록시와 DI, BeanPostProcessor와 같은 개념들을 총 동원해야 최종적으로 프록시를 통해 스프링 빈에 부가 기능을 적용할 수 있습니다.

✈️ AOP 적용 위치

AOP를 적용할 수 있는 지점을 Joinpoint라고 하는데 스프링에서는 프록시 방식으로 동작하기 때문에 생성자, 필드 값 접근, static 메서드 접근시에는 프록시 개념이 적용될 수 없습니다. 그래서 스프링 AOP의 Joinpoint는 메서드에만 사용이 가능합니다. 생각해보면 프록시 자체가 원본 코드의 메서드를 호출하고 그 호출의 앞, 뒤에 부가 기능이 추가되는 것이기 때문입니다. 그리고 스프링 컨테이너가 관리할 수 있는 스프링 빈 에서만 AOP를 적용할 수 있습니다.

🧼 AOP 용어 정리

Joinpoint

Advice가 적용될 수 있는 위치를 의미합니다. 스프링 AOP는 앞에서 프록시 방식을 사용하기 때문에 Joinpoint는 항상 메소드 실행 지점으로 제한됩니다.

Pointcut

지정한 Joinpoint들 중에서 Advice가 적용될 위치를 선별하는 기능입니다. 주로 AspectJ 표현식을 사용하고, 프록시를 사용하는 스프링 AOP는 메소드 실행 지점으로만 포인트컷으로 선별 가능합니다.

Target

부가 기능이란, 원본의 기능의 호출 전이나 후에 실행되는 것을 의미하는데, 이 원본을 Target이라고 이해하면 될 것 같습니다.

Advice

부가 기능 그 자체를 의미하며, Around, Before, After와 같은 다양한 종류의 Advice가 존재합니다.

Aspect

Advice, Pointcut을 모듈화 한 것을 의미하며 여러 Advice와 Pointcut이 함께 존재합니다.

Advisor

스프링 AOP 에서만 사용되는 용어로, 하나의 Pointcut과 하나의 Advice를 생각하면 됩니다.

  • 1 Advice + 1 Pointcut = Advisor
  • Many Advice + Many Pointcut 모듈화 = Aspect

Weaving

Pointcut으로 결정한 Target의 Joinpoint에 Advice를 적용합니다.


부가 기능을 적용할 원본 기능에 대해 부가 기능을 실제로 적용한다 라고 이해하면 됩니다.

👀 Advice의 종류와 실행 순서

Advice의 종류로는 가장 넓은 범위인 @Around, @Before, @AfterReturning, @AfterThrowing, @After 총 다섯 가지의 종류가 있습니다. 하나씩 살펴보면 다음과 같습니다.

@Around

메서드 호출 전 후에 수행되며, 타겟 메서드에 대해 가장 세밀하게 조작할 수 있는 Advice 입니다. 조인 포인트 실행 여부 선택 / 반환 값 변환 / 예외 변환 등이 가능합니다. @Around를 사용할 때는 파라미터를 꼭 ProceedingJoinPoint를 사용해야 합니다.

@Before

Joinpoint 실행 이전에 실행합니다. 즉, AOP를 적용할 메서드(타겟 메서드) 이전에 수행됩니다.

@AfterReturning

Joinpoint가 정상 완료된 후 실행됩니다.

@AfterThrowing

Joinpoint 실행 시 예외가 발생할 경우 실행됩니다.

@After

Joinpoint가 정상 완료든 예외가 발생하든 최종적으로 실행됩니다.


동일한 Aspect 안에 여러 Joinpoint 들이 있을 경우 순서는 다음과 같이 적용됩니다.


  1. @Around
  2. @Before
  3. @After
  4. @AfterReturning
  5. @AfterThrowing

🚨 주의 사항

@AfterReturning, @AfterThrowing의 경우는 각각의 어노테이션에 returning, throwing 속성이 있습니다. 파라미터의 리턴 타입, 예외 타입이 맞지 않을 경우는 실행이 되지 않으므로 꼭 타입을 맞춰야 합니다! (다형성은 물론 활용 가능합니다!)

👉🏻 @Aspect를 적용했을 때 자동 프록시 생성 과정

  1. 스프링 빈 대상이 되는 객체를 생성합니다. (@Bean, Component Scan 대상 모두 포함)
  2. 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에게 전달합니다.
  3. 스프링 컨테이너에서 미리 생성된 Advisor 빈(이 때, @Aspect로 생성된 Advisor들도 포함)을 모두 조회합니다.
  4. 조회한 Advisor들 중에서 pointcut 표현식을 조회하여 해당 객체가 프록시를 적용할지 아닌지를 판단한다. 포인트컷 조건이 여러가지여도 하나만 만족하면 프록시 객체 적용 대상이 된다.
  5. 프록시 적용 대상의 경우 빈 후처리기에서 프록시를 생성하고 반환한다. 이 반환한 프록시를 스프링 빈으로 등록한다.

⚠️ 실무 주의 사항

스프링은 프록시 방식의 AOP를 사용하고, AOP를 적용하게 되면 대상 객체 대신에 프록시를 스프링 빈으로 등록합니다. 이러한 특징때문에 발생하는 문제는 다음과 같습니다.

  1. 프록시 객체 타겟의 내부 호출
  2. 타입 캐스팅으로 인한 의존관계 주입
  3. CGLIB의 생성자 2번 호출, 타겟 객체의 기본 생성자 반드시 필요, final키워드가 붙은 클래스와 메서드

1. 프록시 객체 타겟의 내부 호출

@Slf4j
@Service
public class Example {

  public void external() {
    log.info("external method");
    internal();
  }

  public void internal() {
    log.info("internal method");
  }
}

이렇게 두 메서드가 있다고 가정하고, 저 두 메서드에 AOP가 걸려있다고 가정해보겠습니다. external()을 프록시에서 호출하면 AOP 코드 실행 > external() 로그 > internal 로그 이렇게 출력됩니다. 저는 internal()을 호출하기 전에도 AOP가 적용될 줄 알았는데, 이 원인은 밑의 구조 때문에 발생한 것입니다.


image


external() 안에 있는 internal()은 자기 자신 객체 내부의 internal()을 호출한 것입니다. 그렇기 때문에 적용이 안된것이죠. 이 문제를 해결하는 방법에는 setter를 통한 자기 자신 참조 혹은 지연 조회를 통해 해결하는 방법이 있지만 가장 좋은 것은 별도의 클래스로 분리하여 internal()을 만드는 것입니다.


image

2. 타입 캐스팅으로 인한 의존관계 주입

우선 스프링 AOP는 프록시 기반입니다. 프록시를 만들때, JDK 동적 프록시, CGLIB 을 사용하는데 JDK 동적 프록시는 인터페이스를 구현하여 만듭니다. 이렇게 되면 구현체를 의존 받을때 에러가 발생합니다.


@Service
@RequiredArgsConstructor
public class ExampleService {
  private final AnotherService anotherService;    // 인터페이스라서 가능
  private final AnotherServiceImpl anotherServiceImpl; // 구현체이기 때문에 JDK 동적 프록시로는 불가능
}

하지만, CGLIB을 사용할 경우는 위와 같은 상황에서 에러가 발생하지 않습니다. 그 이유는 CGLIB의 경우, 해당 인터페이스를 구현한 구현체를 기반으로 프록시를 만들기 때문입니다.

3. CGLIB

CGLIB도 단점이 존재합니다.

  1. 대상 클래스에 기본 생성자가 필수로 작성되어야 함
  2. 생성자를 2번 호출
  3. final 키워드 클래스와 메서드 사용 불가

우선, final 키워드는 애플리케이션을 만드는 상황에서는 자주 발생하는 상황이 아니기에 넘어가도 상관없지만, CGLIB의 경우 위에서 말했듯 구체 클래스를 상속받습니다. 자바에서는 자식 클래스의 생성자를 호출 시 부모 클래스의 생성자도 호출해야 하기 때문에 생성자가 필요하고, 실제 프록시에 들어갈 타겟 객체 생성과 CGLIB 프록시 객체를 생성시에 상속하는 타겟 객체의 생성자 호출로 두 번의 호출이 발생합니다.


1, 2번의 문제도 스프링 4.0 부터 objenesis 라이브러리를 통해 기본 생성자가 없이 객체를 생성하고, 생성자를 2번 호출하지 않아도 됩니다. 그리고 스프링부트 2.0 부터는 디폴트로 CGLIB을 사용하게 되어있습니다.


이번 스프링 AOP 포스팅은 여기까지입니다..! 언제나 댓글은 환영합니다!

반응형