
@Transactional를 사용하며 AOP의 주의할 점을 살펴보자.
먼저 @Transactional이 어떻게 동작하는지 로그를 통해 간단히 살펴보자.
@Service
@RequiredArgsConstructor
@Slf4j
public class MemberDBService {
private final MemberDBRepo memberDBRepo;
private final MemberPointService memberPointService;
@Transactional(rollbackFor = IOException.class)
public Long signUp(String email, String password) throws IOException {
Member member = Member.builder()
.email(email)
.password(password)
.build();
//저장
Member savedMember = memberDBRepo.save(member);
changeAllUserData(); // commit되기 전에 저장한 데이터를 조회하는거 확인하기
return savedMember.getId();
}
@Transactional
public void changeAllUserData() {
List<Member> members = memberDBRepo.findAll();
}

위 코드를 다음의 로그와 함께 살펴보면 @Transactional로 대상을 가로챈 순간 트랜잭션을 생성한다. 그 후 데이터를 저장할 때와 @Transactional이 붙은 changeAllUserDate()를 처리할 때 Participation in existing transaction 이라는 로그를 확인할 수 있다. 이는 기본적으로 @Transactional 은 REQUIRED 이라는 속성을 가지고 있어 자신에게 @Transactional이 붙어 있더라도 기존의 트랜잭션이 있으면 합류하는 속성을 가지고 있어 Participation in existing transaction 로그로 이를 확인할 수 있다.
save() 에도 @Transactional이 붙어 있다고 한다.

트랜잭션을 추가로 만들고 싶다면 @Transactional(propagation = Propagation.REQUIRES_NEW) 와 같이 옵션을 추가해 주면 된다. 그럼 이전 예시에서와 같이 같은 클래스에서 @Transactional(propagation = Propagation.REQUIRES_NEW)를 붙인다면 트랜잭션이 생성될까? 결론부터 말하자면 새로운 트랜잭션이 생성되지 않고 이전과 똑같이 동작한다.
@Service
@RequiredArgsConstructor
@Slf4j
public class MemberDBService {
private final MemberDBRepo memberDBRepo;
private final MemberPointService memberPointService;
@Transactional(rollbackFor = IOException.class)
public Long signUp(String email, String password) throws IOException {
Member member = Member.builder()
.email(email)
.password(password)
.build();
//저장
Member savedMember = memberDBRepo.save(member);
changeAllUserData(); // commit되기 전에 저장한 데이터를 조회하는거 확인하기
return savedMember.getId();
}
// 새로운 트랜잭션 생성 옵션 설정 --> 하지만 실제로는 생성되지 않음
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void changeAllUserData() {
List<Member> members = memberDBRepo.findAll();
}
그 이유는 이 블로그를 좀 참고하자면 이미 프록시라는 대문을 통과해 실제 객체 내부로 들어온 상태에서 자기 자신의 메서드를 호출하면(this.method()) 호출 흐름이 프록시를 거치지 않는다. 프록시가 가로챌 기회조차 없으니 @Transactional 설정이 아예 읽히지도 않고 새 트랜잭션도 만들어지지 않는다.
┌─────────────────────────────────────────────┐
│ MemberDBService 프록시 객체 │
│ │
│ (1) 외부(Controller)에서 outer() 호출 │
│ 👉 프록시가 가로챔 (Advisor 목록 순회) │
│ 👉 트랜잭션 A 시작! │
│ │
│ ┌─ 실제 객체 (Target) ───────────────────┐ │
│ │ @Transactional │ │
│ │ public void signUp() { │ │
│ │ // (2) 내부 호출: inner() 실행 │ │
│ │ (this.)changeAllUserData(); │ │
│ │ } │ │
│ │ │ │
│ │ @Transactional(propagation=NEW) │ │
│ │ public void inner() { ... } │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
그래서 새로운 트랜잭션을 만들기 위해서는 다른 클래스로 분리해야 한다. 그래서 다음과 같은 클래스를 만들어 기존의 MemberDBService와 연결 후 로그를 살펴보자.
@Service
@RequiredArgsConstructor
@Slf4j
public class MemberPointService {
private final MemberDBRepo memberDBRepo;
// 새로운 트랜잭션을 만들어서 처리하겠다!!
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void changeAllUserData() {
List<Member> members = memberDBRepo.findAll();
}
}
이 동작을 로그를 통해 확인해보면 Suspending current EntityManager로 기존의 트랜잭션을 중지 한것을 확인할 수 있다.

그 후에 내부 동작을 마무리 한 후 커밋 메세지와 그 중간에 Resuming suspended transaction after completion of inner transaction를 통해 내부의 트랜잭션을 종료하고 기존의 트랜잭션에 합류 한다는 것을 확인할 수 있다.

Controller ──▶ [MemberDBService 프록시] ──▶ [MemberPointService 실제 객체]
│
┌──────────────────────────────────────────┘
│ (txService.createNewTransaction() 호출)
▼
[TransactionService 프록시] ◀── 여기서 "가로채기" 발생!
│
├─ Advisor 목록 순회
│ └─ @Transactional(REQUIRES_NEW) 매칭? → ✅ YES
│ ▶ 기존 트랜잭션 A를 보류(Suspend) 시킴
│ ▶ 새로운 물리 트랜잭션 B 시작!
│
▼
[TransactionService 실제 객체] 실행 (새 트랜잭션 안에서 작동)
│
└─ 완료 후 프록시가 트랜잭션 B 커밋 ──▶ 다시 UserService로 복귀
Spring AOP를 활용하는 모든 기능, 즉 @Async(비동기 처리), @Cacheable같은 내장 어노테이션과 개발자가 직접 만든 사용자 정의 AOP도 동일한 제약을 가진다.
같은 클래스 내부에서 호출되는 순간 프록시라는 문지기를 만날 기회를 잃게 된다. 결국 'Spring AOP는 프록시 기반의 외부 호출 가로채기 방식이다'라는 걸 이해하는 것이 중요하다.