
우리는 서비스 로직을 작성할 때 여러 작업이 함께 묶여 처리되어야 하는 경우가 있다.
예를 들어, 회원이 탈퇴하면
회원 상태를 비활성화하고 관련 데이터도 모두 삭제해야 한다.
이런 상황에서는 하나라도 실패하면 전체 작업을 되돌려야 한다.
이때 사용하는 것이 바로 트랜잭션(Transaction) 이다.
그리고 스프링에서는 이 트랜잭션을 AOP 기반으로 적용할 수 있다.
트랜잭션의 4가지 속성 (ACID)
속성 설명 Atomicity (원자성) 모든 작업은 전부 성공하거나 전부 실패해야 한다 Consistency (일관성) 트랜잭션 실행 전과 후의 데이터 일관성이 유지되어야 한다 Isolation (격리성) 동시에 여러 트랜잭션이 실행될 때 서로 간섭하면 안 된다 Durability (지속성) 성공한 트랜잭션의 결과는 영구적으로 반영되어야 한다
@Transactional
public void placeOrder(Long itemId) {
inventory.decrease(itemId);
paymentService.pay(itemId);
deliveryService.schedule(itemId);
}
스프링 AOP는 프록시 객체가 대상 객체의 메서드를 호출하는 방식으로 동작한다.
하지만 private 메서드는 프록시 객체가 호출할 수 없기 때문에,
트랜잭션이 적용되지 않는다.
@Transactional
private void saveMember() {
// 트랜잭션 적용되지 않는다.
}
트랜잭션은 외부에서 진입한 첫 메서드를 기준으로 프록시가 트랜잭션을 적용한다.
따라서 같은 클래스 내에서 this.메서드()로 호출하는 내부 호출에는 트랜잭션이 동작하지 않는다.
@Service
public class MemberService {
@Transactional
public void register(String name) {
// 회원 등록 전 로직
saveMember(name); // 내부 호출 → 트랜잭션 적용 안 됨
}
@Transactional
public void saveMember(String name) {
memberRepository.save(new Member(name));
}
}
register()는 외부에서 프록시 객체를 통해 호출되므로 트랜잭션이 적용된다.
하지만 saveMember()는 this.saveMember() 식의 내부 호출이라
프록시를 거치지 않기 때문에 트랜잭션이 적용되지 않는다.
@Service
@RequiredArgsConstructor
public class MemberFacade {
private final MemberService memberService;
public void register(String name) {
// 회원 등록 전 로직
memberService.saveMember(name); // 다른 Bean → 트랜잭션 정상 작동
}
}
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public void saveMember(String name) {
memberRepository.save(new Member(name));
}
}
@Service
@RequiredArgsConstructor
public class MemberService {
private final ApplicationContext applicationContext;
private final MemberRepository memberRepository;
public void register(String name) {
// 프록시 객체로 자기 자신을 꺼내서 호출
MemberService proxy = applicationContext.getBean(MemberService.class);
proxy.saveMember(name); // 프록시 경유 → 트랜잭션 정상 적용
}
@Transactional
public void saveMember(String name) {
memberRepository.save(new Member(name));
}
}
스프링에서 @Transactional은 다음과 같은 기본 동작 규칙을 가진다.
@Transactional
public void doSomething() throws IOException {
throw new IOException(); // // 롤백되지 않고 트랜잭션이 커밋됨
}
이 경우, 트랜잭션이 커밋되어버린다.
예외가 발생했는데도 DB에 반영되는 문제가 생길 수 있다.
rollbackFor 속성을 명시rollbackFor에 롤백하고 싶은 예외 클래스를 지정하면
그 예외가 발생할 때도 롤백된다.
@Transactional(rollbackFor = Exception.class)
public void doSomething() throws IOException {
throw new IOException(); // 이 경우에는 롤백이 된다.
}
noRollbackFor 속성 사용@Transactional(noRollbackFor = CustomCheckedException.class)
public void updateMember() throws CustomCheckedException {
// 예외가 발생해도 롤백되지 않고 커밋됨
throw new CustomCheckedException();
}