Spring/스프링 핵심 원리 - 고급편

[스프링 핵심 원리 - 고급편] 스프링 AOP 구현

2023. 3. 8. 22:48
목차
  1. 1. AOP 구현1
  2. 2. AOP 구현2
본문은 인프런의 [스프링 핵심 원리 - 고급편]를 수강하고 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
스프링 핵심 원리 - 고급편
1. 쓰레드 로컬
2. 템플릿 메서드 패턴과 콜백 패턴
3. 프록시 패턴과 데코레이터 패턴
4. 동적 프록시 기술
5. 스프링이 지원하는 프록시
6. 빈 후처리기
7. @Aspect AOP
8. 스프링 AOP 개념
9. 스프링 AOP 구현
10. 포인트컷
11. 실무 주의사항

 

1. AOP 구현1

1) @Aspect를 사용한 AOP 구현

@Slf4j
@Aspect
public class AspectV1 {
    
    @Around("execution(* hello.aop.order..*(..))") //hello.aop.order 패키지와 하위 패키지
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
        return joinPoint.proceed();
    }
}
  • @Aspect
    • 스프링 AOP를 적용할 때 주로 사용되는 애너테이션, 이를 통해 프록시 방식의 AOP가 적용된다
    • @Aspect를 포함한 org.aspectj 패키지 관련 기능은 aspectjweaver.jar 라이브러리가 제공한다
    • @Aspect는 애스펙트라는 표식일 뿐 객체를 컴포넌트 스캔의 대상으로 만들어주진 않으므로 AOP 적용을 위해선 별도 빈으로 등록하는 과정이 필요하다
      • @Bean을 이용한 수동 등록
      • @Component를 이용한 자동 등록
      • @Import를 사용한 클래스 참조 (주로 설정 파일을 추가할 때 사용)
  • @Around
    • 애노테이션의 값(execution(* hello.aop.order..*(..)))은 포인트컷이 된다
    • 애노테이션의 메서드(doLog)는 어드바이스가 된다

 

2) 포인트컷 분리

@Slf4j
@Aspect
public class AspectV2 {

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

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
        return joinPoint.proceed();
    }

}
  • @Pointcut
    • 애노테이션 값에 포인트컷 표현식을 사용한다
    • 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라고 한다
      • 메서드의 반환 타입은 void여야 한다
      • 코드 내용은 비워둔다
    • 여기서 포인트컷 시그니처는 allOrder()이다
      • 포인트컷에 의미를 부여할 수 있다는 장점이 있다
    • 접근제어자(private, public, ...)를 사용할 수 있다
  • @Around 어드바이스에서 포인트컷을 직접 지정하는 것도 가능하지만, 위 예제처럼 포인트컷 시그니처를 사용해도 된다
    • ex) @Around(allOrder())

 

3) 어드바이스 추가

@Slf4j
@Aspect
public class AspectV3 {

    //나머지는 AspectV2와 동일..

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

    @Around("allOrder() && allService()") //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
    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("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

}
  • 트랙잭션을 적용하는 코드가 추가되었다 (실제로는 적용 X, 로그만 출력)
  • allService()
    • 타입 이름 패턴 매칭 → 클래스 & 인터페이스 모두 적용
  • 어드바이스가 적용되어 호출 순서가 바뀜
    • AS-IS) 클라이언트 → orderService.orderItem() → orderRepository.save()
    • TO-BE) 클라이언트 → 프록시 로직[doLog() → doTransaction()] →  orderService.orderItem() → orderRepository.save() → 프록시 로직[doLog()] → orderRepository.save()

 

4) 포인트컷 참조

앞서 포인트 컷에 접근 제어자를 적용할 수 있다는 사실을 언급한 바 있다. 이를 적절히 사용하면 잘 모듈화된 코드를 작성할 수 있다. 위에서 서 작성한 포인트컷을 분리해보자.

 

Pointcuts - 분리된 포인트컷 모듈

public class Pointcuts {

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

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

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

}

 

포인트컷 모듈을 참조하는 예제

@Slf4j
@Aspect
public class AspectV4Pointcut {

    @Around("hello.aop.order.aop.Pointcuts.allOrder()") //참조 대상 지정
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        //...
    }

    @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        //...
    }

}
  • @Around의 값에 참조 대상을 지정한다
    • 이때 패키지명, 클래스명, 메소드명이 모두 포함되어야 한다

 

5) 어드바이스 순서 변경

어드바이스는 기본적으로 순서를 보장하지 않는다. 순서를 보장하기 위해선 @Order 애너테이션을 사용해야 하는데, @Order는 클래스 단위로 적용된다. 따라서 애스펙트를 클래스별로 나눠줘야 한다.

@Slf4j
public class AspectV5Order {

    @Aspect
    @Order(2) //클래스 단위로 적용된다.
    public static class LogAspect {
        //...
    }

    @Aspect
    @Order(1) //클래스 단위로 적용된다.
    public static class TxAspect {
        //...
    }
}
  • 위 코드에선 내부 클래스로 어드바이저가 선언되었지만, 밖으로 빼도 무관하다

 

 

2. AOP 구현2

1) 참고 정보 획득

@Aspect는 @Around 외에도 다른 어드바이스들을 지원한다. 모든 어드바이스는 JoinPoint를 첫번째 파라미터에 사용할 수 있으면 생략도 가능하다. 단 @Around는 ProceedingJointPoint를 사용해야 한다. ProceedingJoinPoint는 JoinPont의 하위 타입이다.

package org.aspectj.lang;


public interface JoinPoint {

    String toString(); //조언되는 방법에 대한 유용한 설명을 출력

    String toShortString();

    String toLongString();

    Object getThis(); //프록시 객체 반환

    Object getTarget(); //대상 객체 반환

    Object[] getArgs(); //메서드 인수 반환

    Signature getSignature(); //조언되는 메서드에 대한 설명을 반환
    
    //...
}
package org.aspectj.lang;


public interface ProceedingJoinPoint extends JoinPoint {

    //...

    Object proceed() throws Throwable; //다음 어드바이스나 타겟 호출

    Object proceed(Object[] args) throws Throwable; //위와 동일, 인수를 전달받음

}

 

2) 어드바이스 종류

1) @Around

@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {

    try {
        log.info("[트랜잭션 시작] {}", joinPoint.getSignature()); //@Before
        Object result = joinPoint.proceed();
        log.info("[트랜잭션 커밋] {}", joinPoint.getSignature()); //@AfterReturning
        return result;
    } catch (Exception e) {
        log.info("[트랜잭션 롤백] {}", joinPoint.getSignature()); //@AfterThrowing
        throw e;
    } finally {
        log.info("[리소스 릴리즈] {}", joinPoint.getSignature()); //@After
    }
}
  • 메서드 호출 전후에 실행
  • 가장 강력한 어드바이스 → 나머지 어드바이스 기능을 모두 수행할 수 있는 완전체
    • 조인 포인트 실행 여부 선택
      • joinPoint.proceed()의 호출 여부를 선택할 수 있다
    • 전달 값 변환
      • joinPoint.proceed(args[])를 호출할 때 임의의 인수를 넘길 수 있다
    • 반환 값 변환
    • 예외 변환
    • 트랜잭션처럼 try/catch/finally가 모두 들어가는 구문 처리 가능
  • 제약
    • 어드바이스의 첫 번째 파라미터로 반드시 ProccedingJoinPoint를 사용해야 한다
  • proceed()를 통해 대상을 실행하며, 여러 번 실행도 가능하다
    • proceed()를 호출하지 않으면 타겟이 호출되지 않으므로 주의해야 한다
    • 하나의 타겟만 호출되지 않는 것이 아니라 그 뒤에 연쇄적인 타켓 호출이 모두 일어나지 않는다
  • @Around를 제외한 나머지 어드바이스들은 @Around 기능의 일부만을 지원하므로, @Around 어드바이스만 사용해도 모든 기능을 구현할 수 있다

 

2) @Before

@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
    log.info("[before] {}", joinPoint.getSignature());
}
  • 조인 포인트 실행 이전에 수행되는 작업을 정의한다
  • @Around와 다르게 작업 흐름을 변경할 수 없다
    • ProceedingJoinPoint가 아닌 JoinPoint 객체를 넘겨 받기 때문에 proceed() 자체를 호출할 수 없다
    • 예외가 발생하지 않는 한 메서드 종료 시 자동으로 다음 타겟이 호출된다

 

3) @AfterReturning

@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
    log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
  • 메서드 실행이 정상적으로 완료된 후 실행되는 작업을 정의한다
  • returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 동일해야 한다
  • returning절에 지정된 변수의 타입을 반환하는 메서드만 대상으로 실행한다
    • 만약 타겟의 메서드가 String 타입을 반환하는데, Int 타입의 result를 받고자 하면 무시된다
    • 부모 타입을 지정하면 모든 자식 타입은 인정된다
  • result의 메서드를 호출해서 상태를 변경할 수는 있지만, 반환되는 객체 자체를 바꿀 수는 없다
    • 이 점이 @Around와 주요한 차이점이다

 

4) @AfterThrowing

@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
    log.info("[ex] {} message={}", ex);
}
  • 메서드가 예외를 던지는 경우 실행되는 작업을 정의한다
  • throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 동일해야 한다
  • throwing절에 지정된 변수의 타입과 일치하는 예외를 대상으로 실행한다
    • 부모 타입을 지정하면 모든 자식 타입은 인정된다

 

5) @After

@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
    log.info("[after] {}", joinPoint.getSignature());
}
  • 조인 포인트가 정상 완료되었거나 메서드가 예외를 던지거나 상관없이 실행되는 작업을 정의한다 (=finally)
  • 정상 및 예외 반환 조건을 모두 처리한다
  • 일반적으로 리소스를 해제하는 데 사용한다

 

3) 실행 흐름

  • 스프링 5.2.7 버전부터 하나의 @Aspect 안에서 동일한 조인포인트의 우선순위가 정해졌다
    • @Around, @Before, @After, @AfterReturning, @AfterThrowing 순이다
    • 실행순서도 이와 동일하다
    • 호출 순서와 리턴 순서는 반대이다
  • @Aspect 안에 동일한 종류의 어드바이스가 2개 이상 있으면 순서가 보장되지 않으므로 이 경우엔 @Aspect를 분리하고 @Order를 적용함으로써 순서를 지정할 수 있다 (1-5 참고)

 

4) @Around 외에 다른 어드바이스가 존재하는 이유?

@Around만 있어도 모든 기능을 수행할 수 있지만, 그럼에도 불구하고 다른 어드바이저들이 존재하는 이유가 있다.

 

1) @Around를 잘못 사용하면 위험하다

  • @Around는 그 기능이 막강한 만큼 위험이 큰 어드바이저다
  • 만약 개발자의 실수로 joinPoint.proceed()를 호출하지 않으면 큰 장애가 일어날 수 있다
    • 타겟이 호출되지 않고, 그 뒤의 연쇄적인 로직이 모두 실행되지 않을 수 있다

2) 역할이 명확하다

  • @Around는 그 기능이 다양하기 때문에 해당 어드바이저가 무슨 일을 하는지 파악하기 어렵다
  • 다른 어드바이저들은 기능이 한정된 만큼 그 역할이 명확하다
    • 예컨대 @Before은 조인 포인트 실행 전에, @After는 조인 포인트 실행 후에 수행된다는 것이 이름에서부터 명확히 드러난다
저작자표시 비영리 (새창열림)

'Spring > 스프링 핵심 원리 - 고급편' 카테고리의 다른 글

[스프링 핵심 원리 - 고급편] 실무 주의사항  (0) 2023.03.22
[스프링 핵심 원리 - 고급편] 포인트컷  (0) 2023.03.16
[스프링 핵심 원리 - 고급편] 스프링 AOP 개념  (0) 2023.03.08
[스프링 핵심 원리 - 고급편] @Aspect AOP  (0) 2023.03.07
[스프링 핵심 원리 - 고급편] 빈 후처리기  (0) 2023.03.06
  1. 1. AOP 구현1
  2. 2. AOP 구현2
'Spring/스프링 핵심 원리 - 고급편' 카테고리의 다른 글
  • [스프링 핵심 원리 - 고급편] 실무 주의사항
  • [스프링 핵심 원리 - 고급편] 포인트컷
  • [스프링 핵심 원리 - 고급편] 스프링 AOP 개념
  • [스프링 핵심 원리 - 고급편] @Aspect AOP
코택
코택
TaxFree코택 님의 블로그입니다.
코택
TaxFree
코택
전체
오늘
어제
  • 분류 전체보기 (369)
    • Spring (29)
      • Spring (18)
      • 스프링 핵심 원리 - 고급편 (11)
    • Spring Batch (4)
    • JPA (4)
    • CS (89)
      • 자료구조 (2)
      • 네트워크 (5)
      • 운영체제 (1)
      • 데이터베이스 (4)
      • SQL (7)
      • 알고리즘 이론 (4)
      • 알고리즘 문제 풀이 (66)
    • 웹 (28)
      • React.js (4)
      • Next.js (1)
      • Node.js (14)
      • FastAPI (4)
      • Django (5)
    • 프로그래밍 언어 (45)
      • Python (5)
      • Java + Kotlin (29)
      • JavaScript + TypeScript (11)
    • 테스트코드 (26)
      • ATDD, 클린 코드 with Spring (4)
      • 이규원의 현실 세상의 TDD: 안정감을 주는 코드.. (20)
    • 인프라 (6)
      • AWS (2)
      • Kubernetes (4)
    • 트러블슈팅 (25)
    • 책 (89)
      • Effective Java (54)
      • Effective Kotlin (14)
      • 도메인 주도 개발 시작하기: DDD 핵심 개념 정.. (11)
      • 웹 프로그래머를 위한 데이터베이스를 지탱하는 기술 (6)
      • 도메인 주도 설계 첫걸음 (4)
    • Git (10)
    • 회고 (5)
    • etc (8)

블로그 메뉴

  • 홈
  • 방명록
  • 관리
  • GitHub
  • LinkedIn

공지사항

  • 스킨 관련

인기 글

태그

  • 브루트포스
  • http
  • 그래프
  • 백준
  • dp
  • 파이썬
  • 깊이 우선 탐색
  • Git
  • atdd
  • mysql
  • 장고
  • Shortest Path
  • fastapi
  • BOJ
  • 그래프 탐색

최근 댓글

최근 글

hELLO · Designed By 정상우.
코택
[스프링 핵심 원리 - 고급편] 스프링 AOP 구현
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.