[Spring AOP] 스프링 AOP 구현

2025. 7. 6. 17:00·Spring/AOP

스프링 AOP 구현

프로젝트 생성

  • Spring Web 의존성 사용 X
  • implementation 'org.springframework.boot:spring-boot-starter-aop' 의존성 추가

예제 프로젝트 만들기

이전 챕터에서 사용했던 서비스 계층과 리포지토리 계층 코드랑 같음. 메서드 호출에 대한 로깅만 추가해주자.

간단한 테스트

@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService={}",
                AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository={}",
                AopUtils.isAopProxy(orderRepository));
    }

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

    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex"))
                .isInstanceOf(IllegalStateException.class);
    }
}

[/스프링 AOP 구현1 - 시작](#스프링 AOP 구현1 - 시작)
이전 챕터에서 배운 @Aspect를 사용하여 스프링 AOP를 구현해보자.
먼저 가장 간단한 애스펙트를 만들어보자.

@Slf4j
@Aspect
public class AspectV1 {

    //hello.aop.order 패키지와 하위 패키지
    @Around("execution(* hello.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
        return joinPoint.proceed();
    }
}
  • @Around와 AspectJ 표현식에 대해서는 다음 챕터에서 자세히 다룬다.
  • OrderService, OrderRepository의 모든 메서드는 AOP 적용의 대상이 된다.
  • 스프링에서는 AspectJ가 제공하는 애노테이션이나 관련 인터페이스만 사용하는 것이고, 실제 AspectJ가 제공하는 컴파일, 로드타임 위버 등을 사용하는 것은 아니다. 스프링은 앞서 배운 프록시 방식의 AOP를 사용한다.

여기서 끝내면 프록시 적용이 안 된다. 이 애스펙트를 빈으로 등록해야, 스프링이 어드바이스로 만들고 자동 프록시 생성이 동작한다.

@Slf4j
@Import(AspectV1.class) //추가
@SpringBootTest
public class AopTest {

테스트 코드를 실행하고 로그를 통해 프록시가 적용되었는지 확인해보자.

참고) 스프링 빈으로 등록하는 방법은 다음과 같다.

  • @Bean을 사용해서 직접 등록
  • @Component 컴포넌트 스캔을 사용해서 자동 등록
  • @Import 주로 설정 파일을 추가할 때 사용(@Configuration)
    • @Import는 주로 설정 파일을 추가할 때 사용하지만, 이 기능으로 스프링 빈도 등록할 수 있다

스프링 AOP 구현2 - 포인트컷 분리

@Around에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수 도 있다.

@Slf4j
@Aspect
public class AspectV2 {

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

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

@Pointcut

  • @Pointcut에 포인트컷 표현식을 사용한다.
  • 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라 한다.
  • 메서드의 반환 타입은 void여야 한다.
  • 코드 내용은 비워둔다.
  • 포인트컷 시그니처는 allOrder()이다. 이름 그대로 주문과 관련된 모든 기능을 대상으로 하는 포인트컷이다.
    • 의미를 부여할 수 있다.
  • @Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다. 여기서는 @Around("allOrder()")를 사용한다.
  • private, public 같은 접근 제어자는 내부에서만 사용하면 private을 사용해도 되지만, 다른 애스펙트에서 참고하려면 public을 사용해야 한다.

포인트 컷을 모아두고, 여러 어드바이스에서 가져다 쓰는 구조다. (조합해서 쓸 수 있다)
이렇게 분리하면 하나의 포인트컷 표현식을 여러 어드바이스에서 함께 사용할 수 있다.

스프링 AOP 구현3 - 어드바이스 추가

로그를 출력하는 기능에 추가로 트랜잭션을 적용하는 코드도 추가해보자. (DB 트랜잭션의 로직(시작, 커밋, 롤백)을 로그로 출력한다)

@Slf4j
@Aspect
public class AspectV3 {

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

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

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

    //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("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }
}
  • @Pointcut("execution(* *..*Service.*(..))") : 클래스 뿐만 아니라 인터페이스에도 적용된다.

포인트컷이 적용된 AOP 결과

  • orderService: doLog(), doTransaction() 어드바이스 적용
  • orderRepository: doLog() 어드바이스 적용

스프링 AOP 구현4 - 포인트컷 참조

포인트컷을 공용으로 사용하기 위해 별도의 외부 클래스에 모아두어도 된다. 외부에서 포인트컷을 호출할 때는 포인트컷의 접근 제어자를 public으로 열어두어야 한다.

public class Pointcuts {

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

    //타입 패턴이 *Service
    @Pointcut("execution(* *..*Service.*(..))")
    public void allService(){}

    //allOrder && 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();
    }

    @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("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }
}

외부 포인트컷을 사용하는 방법: 패키지명을 포함한 클래스 이름과 포인트컷 시그니처를 모두 지정하면 된다.
포인트컷을 여러 어드바이스에서 사용할 때 유용하다.

스프링 AOP 구현5 - 어드바이스 순서

만약에, 로그 트랜잭션 대신, 트랜잭션 로그 순서로 어드바이스를 적용하려면 어떻게 해야 할까?

어드바이스는 기본적으로 순서를 보장하지 않는다. 순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애노테이션을 적용해야 한다.
문제는 이것을 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있다. 하나의 애스펙트에 여러 어드바이스가 있으면 순서 보장이 안된다. 별도의 클래스로 애스팩트를 분리해야 한다.

@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 {
            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());
            }
        }
    }
}
  • @Order의 Value(숫자)가 작을 수록 먼저 실행된다.

스프링 AOP 구현6 - 어드바이스 종류

어드바이스 종류

  • @Around: 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
    • 사실 아래 4개를 @Around로 다 처리 가능하다. @Around를 가장 많이 사용한다.
  • @Before: 조인 포인트 실행 이전에 실행
  • @AfterReturning: 조인 포인트가 정상 완료후 실행
  • @AfterThrowing: 메서드가 예외를 던지는 경우 실행
  • @After: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Slf4j
@Aspect
public class AspectV6Advice {

    @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            //@Before
            log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            //@AfterReturning
            log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            //@AfterThrowing
            log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            //@After
            log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

    @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("[return] {} 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.getMessage());
    }

    @After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("[after] {}", joinPoint.getSignature());
    }
}

여기서 잘 보면, 모든 어드바이스는 org.aspectj.lang.JoinPoint를 첫번째 파라미터에 사용할 수 있다. (생략해도 된다.) @Around는 JoinPoint를 상속받은 ProceedingJoinPoint를 사용해야 한다.

JoinPoint 인터페이스의 주요 기능

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

ProceedingJoinPoint 인터페이스의 주요 기능

  • proceed(): 다음 어드바이스나 타겟을 호출한다.

어드바이스 종류

  • @Before: 조인 포인트 실행 전
  • @AfterReturning: 메서드 실행이 정상적으로 반환될 때 실행
    • returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
    • 위 예제에서 Object 대신 String을 사용하면 이 AOP기능은 적용되지 않는다. service.orderItem()의 반환 타입이 void이기 때문이다.
    • 만약에 반환 타입이 String인 메서드에 이 AOP를 걸고, 파라미터를 String으로 받으면 이 기능이 작동한다.
    • @Around와 다르게 반환되는 객체를 변경할 수는 없다. 다만, 참조 객체의 메서드를 이용해서 조작하는 건 가능하다.
  • @AfterThrowing: 메서드 실행이 예외를 던져서 종료될 때 실행
    • throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
    • throwing 절에 지정된 타입과 맞는 예외를 대상으로 실행한다 (부모 타입 지정 시 자식 타입도 인정됨)
  • @After: 메서드 실행이 종료되면 실행된다. (finally를 생각하면 된다.)
  • @Around: 메서드의 실행의 주변에서 실행된다. 메서드 실행 전후에 작업을 수행한다.
    • 조인 포인트 실행 여부 선택: joinPoint.proceed() 호출 여부 선택
    • 전달 값 변환: joinPoint.proceed(args[]) (다른 인자 전달)
    • 반환 값 변환 (다른 반환 값으로 반환)
    • 예외 변환
    • 트랜잭션 처럼 try ~ catch~ finally 모두 들어가는 구문 처리 가능
    • 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint를 사용해야 한다.
    • proceed()를 여러번 실행할 수도 있음(재시도)

순서

  • 스프링은 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.
  • 실행 순서: @Around, @Before, @After, @AfterReturning, @AfterThrowing
  • 어드바이스가 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대라는 점을 알아두자.
  • 물론 @Aspect 안에 동일한 종류의 어드바이스가 2개 있으면 순서가 보장되지 않는다. 이 경우 앞서 배운 것 처럼 @Aspect를 분리하고 @Order를 적용하자.

@Around 외에 다른 어드바이스가 존재하는 이유
만약에 @Around에서 실수로 joinPoint.proceed()를 누락한다면? 큰일이 나버린다.
단순히 호출 직전에 로그를 찍는다면, @Around 대신 @Before를 사용하면 joinPoint.proceed()를 고민하지 않아도 된다.
또한, @Before라는 애노테이션을 보는 순간 이 코드는 타켓 실행 전에 한정해서 어떤 일을 하는 코드구나 라는 의도가 드러난다.

좋은 설계는 제약이 있는 것이다! 라는 영한님의 말에 어느정도는 동의하는 편이다. 이런 설계는 프로젝트 규모가 커질수록 진가가 드러난다고 생각한다.

Ref) 스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

'Spring > AOP' 카테고리의 다른 글

[Spring AOP] 스프링 AOP 적용 예시, 실무 주의 사항  (0) 2025.07.06
[Spring AOP] 스프링 AOP - 포인트컷  (0) 2025.07.06
[Spring AOP] 스프링 AOP 개념  (0) 2025.07.06
[Spring AOP] @Aspect AOP  (0) 2025.07.06
[Spring AOP] 빈 후처리기  (0) 2025.07.06
'Spring/AOP' 카테고리의 다른 글
  • [Spring AOP] 스프링 AOP 적용 예시, 실무 주의 사항
  • [Spring AOP] 스프링 AOP - 포인트컷
  • [Spring AOP] 스프링 AOP 개념
  • [Spring AOP] @Aspect AOP
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (457)
      • Software Development (27)
        • Performance (0)
        • TroubleShooting (1)
        • Refactoring (0)
        • Test (8)
        • Code Style, Convetion (0)
        • DDD (0)
        • Software Engineering (18)
      • Java (71)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (12)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (1)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (130)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (6)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
        • Clean Code (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Spring AOP] 스프링 AOP 구현
상단으로

티스토리툴바