ì´ ìë¬, ì ë¨ë ê±´ê°
ì´ì ì¤ì ê°ì기 ì´ë° ìë¬ë¥¼ ë§ì£¼íë ê²½ì°ê° ìë¤.
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
ê²ì¼ë¡ ë³´ë©´ "ì¡°ì©í 롤백ëë¤"ë ê±´ë°, ì 롤백ëëì§ ì´ì ê° ì ë³´ì¸ë¤. ë´ ì½ëì ìì¸ì²ë¦¬ë íê³ , try-catchë íëë° ì?
ìì¸: Spring í¸ëìì ì í(Propagation) 구조 ì´í´
íµì¬ ê°ë
Springì 기본 í¸ëìì
ì í ë°©ìì REQUIREDë¤. ì¦, ì´ë¯¸ í¸ëìì
ì´ ìì¼ë©´ ê·¸ í¸ëìì
ì ì°¸ì¬(join)íë¤.
문ì ë ì¬ê¸°ì ë°ìíë¤.
[ì¸ë¶ í¸ëìì
ìì]
âââ ë´ë¶ ìë¹ì¤ í¸ì¶ (REQUIRED â ì¸ë¶ í¸ëìì
ì ì°¸ì¬)
âââ ë´ë¶ìì ìì¸ ë°ì â í¸ëìì
ì rollback-only ë§í¹
ë´ë¶ ìì¸ë¥¼ try-catchë¡ ì¡ì
ì¸ë¶ í¸ëìì
ì»¤ë° ìë
â BOOM: UnexpectedRollbackExceptionë´ë¶ ë©ìëìì ìì¸ê° ë°ìí´ì rollback-onlyë¡ ë§í¹ëëë°, ì¸ë¶ìì ìì¸ë¥¼ ì¡ìë²ë¦¬ë©´ Springì "ì ê´ì°®ì 거구ë"íê³ ì»¤ë°ì ìëíë¤. íì§ë§ ì´ë¯¸ 롤백 ë§í¹ì´ ë기 ë문ì UnexpectedRollbackExceptionì´ í°ì§ë¤.
ë ë²¨ë³ í´ê²° ë°©ë²
Level 1 â ê¸°ì´ (ìì¸ íì ë¶í°)
ìí© ì¬í ì½ë:
@Service
@RequiredArgsConstructor
public class OrderService {
private final PaymentService paymentService;
@Transactional
public void placeOrder(OrderRequest request) {
// 주문 ì ì¥ ë¡ì§...
try {
paymentService.processPayment(request.getPaymentInfo()); // ë´ë¶ í¸ëìì
ì°¸ì¬
} catch (Exception e) {
log.error("ê²°ì ì¤í¨: {}", e.getMessage()); // ìì¸ ì¡ì
// â ì´ ìì ì í¸ëìì
ì ì´ë¯¸ rollback-only
}
// ì»¤ë° ìë â UnexpectedRollbackException ë°ì!
}
}
@Service
public class PaymentService {
@Transactional // REQUIRED (기본ê°) â ì¸ë¶ í¸ëìì
ì ì°¸ì¬
public void processPayment(PaymentInfo info) {
// ë´ë¶ ìì¸ ë°ì
throw new PaymentException("ì¹´ë íë ì´ê³¼");
}
}í¸ëìì ìí ëë²ê¹ :
@Transactional
public void placeOrder(OrderRequest request) {
log.info("í¸ëìì
íì±: {}", TransactionSynchronizationManager.isActualTransactionActive());
log.info("롤백 ë§í¹: {}", TransactionSynchronizationManager.isCurrentTransactionReadOnly());
}Level 2 â ì¤ë¬´ í´ê²°ì± (ì í ë°©ì ë³ê²½)
ë°©ë² 1: REQUIRES_NEWë¡ ë³ë í¸ëìì ë¶ë¦¬
@Service
public class PaymentService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
// â 기존 í¸ëìì
ê³¼ ìì í ë¶ë¦¬ë ì í¸ëìì
ìì
public void processPayment(PaymentInfo info) {
// ì´ í¸ëìì
ì´ ë¡¤ë°±ë¼ë ì¸ë¶ í¸ëìì
ì ìí¥ ìì
paymentRepository.save(/* ... */);
}
}REQUIRES_NEWë ë³ë DB 커ë¥ì
ì ì¬ì©íë¤. 커ë¥ì
í ê³ ê° ìíì´ ìì¼ë ë¨ë°íë©´ ì ëë¤.
ë°©ë² 2: noRollbackFor ì¤ì
@Transactional(noRollbackFor = PaymentException.class)
public void processPayment(PaymentInfo info) {
// PaymentExceptionì´ ë°ìí´ë 롤백íì§ ìì
}ë°©ë² 3: ìì¸ë¥¼ ì¡ì§ ë§ê³ ëì§ê¸°
@Transactional
public void placeOrder(OrderRequest request) {
try {
paymentService.processPayment(request.getPaymentInfo());
} catch (PaymentException e) {
// ì¡ì§ ë§ê³ ê·¸ë¥ ëì§ë¤
throw e; // í¹ì ì ìì¸ë¡ ëí
// â ì¸ë¶ í¸ì¶ìê° í¸ëìì
롤백 ì²ë¦¬
}
}Level 3 â ê³ ê¸ (ìí¤í ì² ê´ì ìì ì¤ê³)
ì´ë²¤í¸ ê¸°ë° ë¶ë¦¬ (í¸ëìì ìë£ í ì²ë¦¬)
@Service
@RequiredArgsConstructor
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void placeOrder(OrderRequest request) {
Order order = orderRepository.save(Order.of(request));
// í¸ëìì
ì»¤ë° í ì´ë²¤í¸ ë°í
eventPublisher.publishEvent(new OrderPlacedEvent(order.getId()));
}
}
@Component
public class PaymentEventHandler {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
// â ì¸ë¶ í¸ëìì
ì»¤ë° íì ì¤í â í¸ëìì
ì¤ì¼ ìì
public void handleOrderPlaced(OrderPlacedEvent event) {
paymentService.processPayment(event.getOrderId());
}
}Facade í¨í´ì¼ë¡ í¸ëìì ê²½ê³ ëª íí
@Service
public class OrderFacade {
// @Transactional ìì â í¸ëìì
ê²½ê³ ìì
public void placeOrder(OrderRequest request) {
orderService.saveOrder(request); // ê°ê° ë
립 í¸ëìì
paymentService.processPayment(request); // ê°ê° ë
립 í¸ëìì
}
}
@Service
public class OrderService {
@Transactional // ì´ ë©ìë ë²ìë§ í¸ëìì
public void saveOrder(OrderRequest request) { /* ... */ }
}ìí©ë³ í´ê²° ì²´í¬ë¦¬ì¤í¸
| ìí© | ê¶ì¥ ë°©ë² | 주ìì¬í |
|---|---|---|
| ë´ë¶ ìì¸ê° ì¸ë¶ì ë 립ì ì¼ë¡ ì²ë¦¬ë¼ì¼ í ë | REQUIRES_NEW | 커ë¥ì í ì¬ì©ë ì¦ê° |
| í¹ì ìì¸ë 롤백 ì í´ë ë ë | noRollbackFor | ë°ì´í° ì í©ì± ê²í íì |
| ê²°ì ê°ì ì¸ë¶ I/Oê° ìì ë | @TransactionalEventListener | AFTER_COMMIT íì´ë° 주ì |
| ìë¹ì¤ ë ì´ì´ ì¤ê³ë¥¼ ë°ê¿ ì ìì ë | Facade í¨í´ | í¸ëìì ê²½ê³ ì¬ì¤ê³ íì |
| ë¹ ë¥´ê² ìì ìì ì´ íìí ë | ìì¸ ì¬ëì§ê¸° | í¸ì¶ë¶ìì ì²ë¦¬ íì |
ì주 íë ì¤ì
ì¤ì 1: try-catchìì ìì¸ë¥¼ 먹ì´ë²ë¦¬ê¸°
// â ì못ë ì½ë try { innerService.doSomething(); } catch (Exception e) { log.error("ìë¬ ë°ì", e); // ìì¸ë¥¼ ì¼ì¼ë²ë¦¼ â UnexpectedRollbackException íì } // â ì¬ë°ë¥¸ ì½ë try { innerService.doSomething(); } catch (Exception e) { log.error("ìë¬ ë°ì", e); throw new BusinessException("ì²ë¦¬ ì¤í¨", e); // ë°ëì ë¤ì ëì§ê¸° }
ì¤ì 2: Self-invocation (ê°ì í´ëì¤ ë´ ë©ìë í¸ì¶)
@Service
public class MyService {
@Transactional
public void outer() {
inner(); // íë¡ì를 ê±°ì¹ì§ ìì â @Transactional 무ìë¨
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() { /* ... */ }
// â REQUIRES_NEWê° ì ì©ëì§ ììì ì¬ì í ê°ì í¸ëìì
ì¬ì©
}Self-invocation í´ê²°:
@Service
@RequiredArgsConstructor
public class MyService {
private final ApplicationContext context;
public void outer() {
MyService proxy = context.getBean(MyService.class); // íë¡ì ì§ì ê°ì ¸ì¤ê¸°
proxy.inner();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() { /* ... */ }
}ì 리
UnexpectedRollbackExceptionì í¸ëìì
ì í를 모르면 ë°ëì í ë²ì ë§ëë ìë¬ë¤. íµì¬ì íëë¤.
í´ê²°ì â«ê¸ ë ë°©í¥ì´ë¤.
- í¸ëìì
ì ë¶ë¦¬íë¤ (
REQUIRES_NEW, Facade í¨í´, ì´ë²¤í¸ 기ë°) - ìì¸ë¥¼ ì ëë¡ ë¤ë£¬ë¤ (ë¨¹ì§ ë§ê³ ëì§ê¸°,
noRollbackFor)
ì¤ë¬´ììë REQUIRES_NEW를 무ë¶ë³íê² ì°ê¸°ë³´ë¤ @TransactionalEventListenerë Facade í¨í´ì¼ë¡ ì¤ê³ ë¨ê³ìì í¸ëìì
ê²½ê³ë¥¼ ëª
íí íë ê² ì¥ê¸°ì ì¼ë¡ ë«ë¤.