1. 서비스 레이어에서 @Transactional 사용
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
@Transactional
public void placeOrder(OrderRequest request) {
// 1. 주문 데이터 저장
Order order = orderRepository.save(request.toEntity());
// 2. 결제 시도 (실패 시 RuntimeException 발생)
paymentGateway.pay(order.getId(), request.getAmount());
}
}
핵심 포인트
- 기본 전파:
Propagation.REQUIRED - 기본 롤백:
RuntimeException/Error발생 시 롤백
2. 체크 예외(Checked Exception) 롤백 설정
@Transactional(rollbackFor = { IOException.class })
public void uploadFile(MultipartFile file) throws IOException {
storageService.store(file); // IOException 발생 가능
}
체크리스트
- Checked Exception 에 대해서는
rollbackFor로 명시 - 비즈니스 예외용 커스텀 예외는 RuntimeException 을 상속해서 사용하는 게 일반적
3. 읽기 전용 트랜잭션
@Transactional(readOnly = true)
public List<Order> getRecentOrders(Long userId) {
return orderRepository.findTop10ByUserIdOrderByCreatedAtDesc(userId);
}
효과
- JPA: dirty checking 비활성화 → 약간의 성능 이점
- 일부 DB 드라이버: 최적화 힌트로 사용
4. self-invocation 주의 (내부 메서드 호출)
@Service
public class UserService {
@Transactional
public void createUser(UserRequest request) {
userRepository.save(request.toEntity());
sendWelcomeMail(request.getEmail()); // ❌ 트랜잭션 적용 안 됨
}
@Transactional
public void sendWelcomeMail(String email) {
// 메일 발송 로직
}
}
왜 안 될까?
- 같은 빈 내부에서
this.sendWelcomeMail()호출 → 프록시를 거치지 않음 - 해결: 별도 서비스로 분리하거나, AOP 프록시를 명시적으로 사용
5. 간단한 실습 아이디어
- 의도적으로 예외를 던져 롤백 로그 확인하기
@Transactional유무에 따라 DB에 commit 되는지 비교readOnly = true와 일반 트랜잭션의 성능 차이 측정
💬 댓글