본문으로 바로가기
TaeyoungKim.dev

Spring AOP @Around 로그가 안 찍힐 때: 자기 호출과 프록시 경계

자바/스프링작성 약 4분 읽기TaeyoungKim
LinkedInX

서비스의 reserve()를 직접 호출하면 @Around 로그가 찍힌다. 그런데 같은 서비스의 checkout()에서 reserve()를 부르면 로그가 없다. 메서드가 실행되지 않은 게 아니라 프록시를 거치지 않은 호출이라서 부가 로직이 끼어들 자리가 없어진 것이다.

Spring AOP는 어느 호출에 끼어들까?

도식의 바깥 화살표는 프록시를 거쳐 advice가 적용되지만, 대상 객체 안에서 this로 이어지는 화살표는 프록시 밖으로 나가지 않는다. 애너테이션이 붙어 있어도 호출 경로가 다르면 부가 기능의 적용 결과가 달라지는 이유다.

교육 노트의 시간 측정 AOP는 핵심 서비스 코드 밖으로 로그·시간 측정을 분리한다. Spring AOP는 대상 빈 앞에 프록시를 두어 프록시로 들어온 호출 전후에 advice를 실행한다. @Around는 그 advice 중 대상 호출을 감싸는 방식이다.

아래는 흐름만 보이도록 줄인 예시다. reserve()에 걸리는 @Around가 등록돼 있다고 가정한다.

java
@Service
public class CouponService {
    public void checkout() {
        reserve(); // 같은 객체의 this.reserve() 호출
    }

    public void reserve() {
        // 예약 처리
    }
}

컨트롤러가 주입받은 CouponService 빈의 reserve()를 직접 호출하면 프록시 → advice → reserve() 경로다. 반면 checkout()이 시작된 뒤 같은 객체 안에서 reserve()를 부르면 this.reserve()다. 두 번째 이동은 프록시로 다시 들어가지 않으므로 reserve()에만 걸린 advice는 실행되지 않는다. checkout() 자체에 적용되는 advice가 있다면 그것은 별개의 호출 경계다.

로그가 빠진 원인을 어떤 순서로 좁힐까?

먼저 메서드 본문에 도달했는지와 advice에 도달했는지를 다른 로그 또는 테스트 관찰로 나눈다. 메서드는 동작하는데 advice만 빠진다면 다음 경로를 비교한다.

  1. 스프링이 주입한 빈을 통해 reserve()를 호출했는가?
  2. checkout() 안의 reserve()가 사실은 같은 객체의 호출인가?
  3. pointcut이 대상 메서드 이름·범위를 실제로 가리키는가?

작은 통합 테스트에서는 주입받은 빈에 대한 외부 reserve() 호출과 외부 checkout() 호출을 나란히 수행하고, reserve()에만 걸린 advice의 관찰 횟수를 비교할 수 있다. 예상은 첫 경로에서 한 번, 자기 호출 경로에서 0번이다. 이 원고는 실제 Spring 프로젝트를 실행해 얻은 로그나 테스트 결과를 주장하지 않는다. 프록시 기반 AOP의 호출 규칙에서 나온 기대값이다.

new CouponService()도 별도 문제다

직접 new로 만든 객체는 스프링이 주입한 프록시 빈이 아니다. 이 객체의 메서드에서 advice가 보이지 않는다면 자기 호출을 고치기 전에 어떤 객체를 호출했는지부터 확인해야 한다. 프록시가 없는 객체와 프록시를 건너뛴 자기 호출은 원인은 다르지만 결과가 비슷해 보인다.

다른 빈으로 책임을 나누면 무엇이 달라질까?

reserve()가 독립된 책임이라면 ReservationService 빈으로 옮기고 CouponService에 주입할 수 있다. 그때 checkout()은 자기 자신의 메서드가 아니라 다른 빈의 프록시를 호출한다.

java
@Service
public class CouponService {
    private final ReservationService reservations;

    public CouponService(ReservationService reservations) {
        this.reservations = reservations;
    }

    public void checkout() {
        reservations.reserve();
    }
}

이 변경은 로그 한 줄을 얻기 위한 요령이 아니라 책임을 실제로 나눌 때 적합하다. 작은 내부 헬퍼를 AOP 때문에 억지로 새 빈으로 만들 필요는 없다. 반대로 advice가 보안·트랜잭션 같은 중요한 경계라면 자기 호출로 빠지는 구간을 묵인해서도 안 된다. AopContext로 현재 프록시를 다시 부르는 방법도 있지만 업무 코드가 AOP 구현에 묶이므로 먼저 설계를 바꿀 수 있는지 검토한다. 프록시 자기 호출의 정확한 규칙은 Spring 공식 문서에서 확인할 수 있다.

핵심 요약: 메서드가 아니라 호출 경로를 확인한다

Spring AOP의 advice는 프록시를 통과하는 호출에 적용된다. 같은 객체의 this.reserve()는 메서드가 실행돼도 프록시를 다시 지나지 않는다. 외부 빈 호출과 내부 호출을 나눠 테스트하고, 필요한 부가 경계라면 다른 빈으로 책임을 분리할지 판단하자.

작성자

TaeyoungKim

기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

#Spring AOP#Around#프록시#자기 호출#서비스 테스트

함께 읽으면 좋은 글