ð ê²ì í¤ìë: Spring Boot UnexpectedRollbackException, í¸ëìì
롤백 ìë¬, Spring @Transactional ìë¬, rollback-only ìë¬, Spring í¸ëìì
ì í
ì´ ìë¬, ì ë¨ë ê±´ê°
ì´ì ì¤ì ê°ì기 ì´ë° ìë¬ë¥¼ ë§ì£¼íë ê²½ì°ê° ìë¤.
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());
}
}
}
@Service
public class PaymentService {
@Transactional
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) {
}
ë°©ë² 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 {
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);
}
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(propagation = Propagation.REQUIRES_NEW)
public void inner() { }
}
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 í¨í´ì¼ë¡ ì¤ê³ ë¨ê³ìì í¸ëìì
ê²½ê³ë¥¼ ëª
íí íë ê² ì¥ê¸°ì ì¼ë¡ ë«ë¤.