이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Spring › AOP와 프록시
AOP: 어드바이스와 포인트컷
어드바이스는 어느 메서드에, 어떤 순서로 끼어드나
로그 남기기처럼 여러 메서드에 똑같이 필요한 일을 한 클래스(애스펙트)에 적어 두면, 스프링이 메서드 호출의 앞뒤에 그 코드를 끼워 넣습니다. 끼워 넣는 코드를 어드바이스, 어느 메서드에 끼울지 고르는 식을 포인트컷이라고 합니다.
실행으로 대조 · 스프링 프레임워크 7.0.9에서 72가지 조합을 실행해 대조
이 실습에 나오는 개념
- AOP(관점 지향 프로그래밍)
- 로깅, 트랜잭션, 권한 검사처럼 여러 곳에 반복해서 나타나는 부가 기능(횡단 관심사)을 핵심 비즈니스 코드에서 분리해 한곳에 모아 두는 프로그래밍 기법이다. 부가 기능을 한 번만 작성하고 "어디에 적용할지"를 따로 지정하므로, 핵심 코드가 깔끔해지고 부가 기능의 수정이 한곳에서 끝난다.
- 어드바이스(Advice)
- AOP에서 메서드 호출에 끼워 넣을 코드와, 그 코드를 언제 실행할지를 함께 정한 것이다. 스프링에서는 애스펙트 클래스의 메서드에 @Before(메서드 실행 전), @AfterReturning(정상적으로 값을 돌려준 뒤), @AfterThrowing(예외가 나간 뒤), @After(정상이든 예외든 끝난 뒤), @Around(메서드 실행을 앞뒤로 감쌈) 가운데 하나를 붙여 만든다. @Around는 ProceedingJoinPoint의 proceed()를 불러야 대상 메서드가 실행되며, 부르지 않으면 대상 메서드는 실행되지 않는다. @AfterThrowing은 지나가는 예외를 보기만 하고 없애지 않으므로, 예외는 그대로 호출한 쪽에 전달된다.
- 포인트컷(Pointcut)
- 어드바이스를 어느 메서드에 적용할지 고르는 조건식이다. execution(반환타입 패키지.클래스.메서드(매개변수)) 꼴이 가장 많이 쓰이며, *는 아무 이름 하나를, (..)는 매개변수가 몇 개든 상관없음을 뜻한다. 괄호 안을 비운 ()는 매개변수가 없는 메서드만 고른다. @annotation(애너테이션 타입)은 그 애너테이션이 붙은 메서드를 고른다. 식이 어느 메서드도 고르지 못해도 오류가 나지 않고 어드바이스가 실행되지 않을 뿐이므로, 식을 쓴 뒤에는 실제로 적용되는지 확인해야 한다.
- 애스펙트(Aspect)와 @Order
- 애스펙트는 포인트컷과 어드바이스를 한 클래스에 모아 둔 것으로, 스프링에서는 빈으로 등록한 클래스에 @Aspect를 붙여 만든다. 한 메서드에 애스펙트가 여럿 걸리면 @Order의 숫자가 작은 애스펙트가 바깥쪽에서 감싼다. 바깥쪽 애스펙트는 들어갈 때 먼저 실행되고 나올 때는 나중에 실행된다. @Order를 붙이지 않은 애스펙트끼리의 순서는 정해져 있지 않으므로, 순서가 결과에 영향을 주면 반드시 지정한다.
- 프록시(Proxy)
- 실제 객체를 대신해 호출을 받는 대리 객체다. 호출을 받으면 부가 작업을 수행한 뒤 실제 객체에 전달한다. 스프링은 AOP와 @Transactional 같은 기능을 적용할 빈을 프록시로 감싸 컨테이너에 등록하므로, 다른 빈이 주입받는 것은 실제 객체가 아니라 프록시다.
이렇게 바꿔 보세요
- 처음 설정으로 — 메서드가 예외를 던집니다. @Around의 뒷부분은 실행되지 않고 @After는 실행됩니다.
- 정상적으로 끝나면 — 어드바이스 다섯 가지 가운데 네 가지가 정해진 순서로 실행됩니다.
- 괄호 안을 비운 포인트컷 — cancel()이라고 썼는데 cancel(Long)에는 끼어들지 않습니다.
- 애너테이션으로 고르기 — @Audit이 붙은 메서드라면 다른 클래스의 메서드에도 끼어듭니다.
- 같은 클래스인데 끼어들지 않는 메서드 — @Audit이 없는 order에는 아무것도 실행되지 않습니다.
- 애스펙트가 둘일 때 — @Order의 숫자가 작은 LogAspect가 바깥쪽에서 감쌉니다.
- @Order를 서로 바꾸면 — TimeAspect가 바깥쪽이 됩니다. 예외가 나도 "Time 끝"은 찍힙니다.
처음 설정으로 실행하면
- 애플리케이션이 뜰 때: 스프링이 포인트컷을 빈의 메서드 하나하나와 맞춰 봅니다. execution(* shop.OrderService.*(..))은 shop.OrderService 클래스의 모든 메서드를 고릅니다. 맨 앞의 *는 반환 타입이 무엇이든, 메서드 이름 자리의 *는 이름이 무엇이든, (..)는 매개변수가 몇 개든 좋다는 뜻입니다.
- order(String) 메서드와 맞춰 본 결과: order는 OrderService의 메서드이므로 고른 메서드입니다. OrderService 빈 대신 프록시(대리 객체)가 등록됩니다(원래 객체와 프록시는 서로 다른 객체).
- orderService.order("키보드"): 다른 빈이 메서드를 부릅니다. 호출은 프록시에 먼저 도착합니다. 프록시는 이 메서드에 걸린 어드바이스를 바깥쪽부터 차례로 실행합니다.
- LogAspect의 @Around 앞부분: 한 애스펙트 안에서는 @Around가 가장 먼저 시작합니다. pjp.proceed()보다 위에 적은 줄이 실행됩니다.
- pjp.proceed() 호출 → LogAspect의 @Before: @Around가 안쪽으로 넘기면 같은 애스펙트의 @Before가 실행됩니다. 그다음이 메서드 본문입니다.
- order(String) 본문 실행: "order 본문"을 찍고 IllegalStateException을 던집니다. 예외는 안쪽에서 바깥쪽으로, 들어온 길을 거꾸로 지나가며 나갑니다.
- LogAspect의 @AfterThrowing: 안쪽에서 예외가 나왔을 때만 실행됩니다. 예외를 잡아서 없애는 것이 아니라 지나가는 예외를 보기만 하므로, 예외는 계속 밖으로 나갑니다. @AfterReturning은 실행되지 않습니다.
- LogAspect의 @After: 예외가 났어도 실행됩니다. 자바의 finally와 비슷합니다. 같은 애스펙트 안에서는 @AfterThrowing보다 나중입니다.
- LogAspect의 @Around 뒷부분: pjp.proceed()가 예외를 던졌으므로, 그 아래에 적은 "Log @Around 뒤" 줄은 실행되지 않고 예외가 그대로 나갑니다. 예외가 나도 실행하려면 try-finally로 감싸야 합니다.
- 호출한 쪽: IllegalStateException을 받습니다. 어느 어드바이스도 예외를 잡지 않았으므로 예외가 그대로 전달됩니다.
실행 결과
- 예외가 나면 @AfterThrowing과 @After가 실행되고, @AfterReturning과 @Around에서 pjp.proceed() 아래에 적은 줄은 실행되지 않습니다. 어느 경우에나 해야 하는 일은 @After에 두거나, @Around 안에서 try-finally로 감쌉니다.
코드
// 아래 클래스는 모두 shop 패키지에 있다
// @Audit은 이 예제에서 직접 만든 애너테이션이다 (메서드에 붙이는 표시)
// 다른 빈에서 부르는 코드: orderService.order("키보드");
@Aspect
@Component
class LogAspect {
@Pointcut("execution(* shop.OrderService.*(..))")
void pick() { } // 어느 메서드에 끼어들지 고른다
@Around("pick()")
Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Log @Around 앞");
Object result = pjp.proceed(); // 안쪽으로 넘긴다
System.out.println("Log @Around 뒤");
return result;
}
@Before("pick()")
void before() { System.out.println("Log @Before"); }
@AfterReturning("pick()")
void afterReturning() { System.out.println("Log @AfterReturning"); }
@AfterThrowing("pick()")
void afterThrowing() { System.out.println("Log @AfterThrowing"); }
@After("pick()")
void after() { System.out.println("Log @After"); }
}
@Service
class OrderService {
public String order(String item) {
System.out.println("order 본문");
throw new IllegalStateException("실패");
}
@Audit
public String cancel(Long id) {
System.out.println("cancel 본문");
return "취소 완료";
}
}
@Service
class MemberService {
@Audit
public String join(String name) {
System.out.println("join 본문");
return "가입 완료";
}
}
이 모형의 전제
72가지 조합 모두를 이 PC의 스프링 프레임워크 7.0.9(스프링 부트 4.1.1에 들어 있는 버전, AspectJ weaver 1.9.25.1, JDK 21)에서 화면과 같은 구조의 코드로 실행해, 포인트컷이 메서드를 고르는지, 콘솔에 찍힌 줄의 순서, 호출한 쪽이 받는 것, 빈이 프록시인지를 대조했습니다. 어드바이스 메서드를 클래스 안에 적는 순서를 거꾸로 해도 72가지 결과가 모두 같다는 것, @Around에서 pjp.proceed()를 부르지 않으면 메서드 본문이 실행되지 않는다는 것도 실행으로 확인했습니다. 실행으로 확인하지 못하고 스프링 문서를 따른 것은 두 가지입니다. 첫째, 한 애스펙트 안의 실행 순서는 스프링 프레임워크 5.2.7부터 지금과 같이 정해졌고 그보다 오래된 버전에서는 @After가 @AfterReturning보다 먼저 실행되기도 했다는 것입니다(이 PC에는 옛 버전이 없습니다). 둘째, @Order를 붙이지 않은 애스펙트끼리의 순서는 정해져 있지 않다는 것입니다. 화면의 코드는 @EnableAspectJAutoProxy가 켜져 있다고 가정합니다(실행할 때는 설정 클래스에 직접 붙였습니다. 스프링 부트에서는 AOP 스타터를 넣으면 자동으로 켜진다고 문서에 나와 있으나 이 부분은 실행으로 확인하지 않았습니다). 메서드는 다른 빈에서 부른다고 가정했습니다. 같은 클래스 안에서 부르면 프록시를 지나지 않아 어드바이스가 실행되지 않는데, 이 내용은 "프록시와 자기 호출" 실습에서 다룹니다. 포인트컷 메서드의 이름은 pick으로 지었습니다. 이 PC에서 이름을 target으로 지었을 때는 오류 없이 LogAspect의 어드바이스가 하나도 실행되지 않았습니다(target은 포인트컷 식에서 따로 쓰이는 낱말입니다).
앞 실습: 프록시와 자기 호출 · 다음 실습: 트랜잭션 기본