Spring ํธ๋์ญ์ ์ ์ธ์ ์ด๋ป๊ฒ ๋กค๋ฐฑ ๋ ๊น? -1ํธ
๊ฐ์
์คํ๋ง์์ @Transactional ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ฉด ํธ๋์ญ์ ์ด ์๋์ผ๋ก ๊ด๋ฆฌ๋๋ค. ๋ก์ง์ด ์ฑ๊ณต์ ์ผ๋ก ์ฒ๋ฆฌ๋๋ฉด commit์ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ณ๊ฒฝ ์ฌํญ์ด ๋ฐ์๋๊ณ , ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด rollback์ ํตํด ๋ชจ๋ ๋ณ๊ฒฝ ์ฌํญ์ด ์ทจ์๋๋ค. ๋ค๋ง ์ด๋ ๊ธฐ๋ณธ์ ์ธ ๊ฐ๋ ์ผ๋ก ์ด๋ฒ ๊ธ์์๋ ๋ ๋์๊ฐ ์ธ๋ถ์ ์ผ๋ก ๋กค๋ฐฑ์ด ์ ํํ ์ธ์ ๋ฐ์ํ๊ณ (1ํธ), ํน์ ์ํฉ์ ์์๋ก ํด๋น ๋ก์ง์ด ๋กค๋ฐฑ์ด ๋ ์ง ์ ๋ ์ง์ ๋ํด ๋ง์ถฐ๋ณด๋ ์(2ํธ)์ผ๋ก ์์๋ณด๊ณ ์ ํ๋ค.
Check Exception, Unchecked Exception
๋จผ์ ์๋ฐ์์๋ ์์ธ๋ฅผ ํฌ๊ฒ ๋ค์๊ณผ ๊ฐ์ด Exception๊ณผ Error ๋ ๊ฐ์ง๋ก ๋๋๊ณ ์๋ค.
Exception์ ์ ๋ ฅ ๊ฐ์ ๋ํ ์ฒ๋ฆฌ๊ฐ ๋ถ๊ฐ๋ฅํ๊ฑฐ๋ ํ๋ก๊ทธ๋จ ์คํ ์ค์ ์ฐธ์กฐ๋ ๊ฐ์ด ์๋ชป๋ ๊ฒฝ์ฐ์ธ ์์ธ๋ค์ ์๋ฏธํ๋ฉฐ, Error๋ OutOfMemmoryError๋ StackOverFlowError์ ๊ฐ์ด ํ๋ก๊ทธ๋จ์ด ์ค๋จ๋ ์๋ ์๋ ์ฌ๊ฐํ ์๋ฌ๋ฅผ ์๋ฏธํ๋ค.
๊ทธ๋ฆฌ๊ณ Exception ๋ด๋ถ์์๋ ํฌ๊ฒ ๋ค์๊ณผ ๊ฐ์ ๊ทธ๋ฃน์ผ๋ก ๊ตฌ๋ถ ์ง๋๋ค.
- Checked Exception: RuntimeException ํด๋์ค์ ์ ํ ๊ด๊ณ์๋ ์์ธ๋ค(IoException, FileNotFoundException..)
- Unchecked Exception: RuntimeException ํด๋์ค ๋ฐ ๊ทธ ํ์ ํด๋์ค๋ฅผ ์์๋ฐ๋ ์์ธ๋ค(NullPointerException, ClassCastException..)
Checked Exception์ ์ปดํ์ผ ์์ ์์ ๋ฐ๋์ ์ฒ๋ฆฌํด์ผ ํ๋ ์์ธ๋ก, ๋ง์ฝ try-catch๋ก ์ฒ๋ฆฌํ์ง ์๊ฑฐ๋ ๋ฉ์๋ ์ ์ธ๋ถ์ throws๋ก ์์ธ๋ฅผ ๋์ง์ง ์์ผ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ปดํ์ผ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
unreported exception IOException; must be caught or declared to be thrown
๋ฐ๋ฉด Unchecked Exception์ ์ปดํ์ผ ์์ ์ด ์๋ ์คํ ์ค์ ๋ฐ์ํ ์ ์๋ ์์ธ๋ก, ์ปดํ์ผ ์์ ์์ ์์ธ ์ฒ๋ฆฌ๋ฅผ ๊ฐ์ ํ์ง ์๋๋ค. ๋ฐ๋ผ์ ๋ณ๋๋ก ์ฒ๋ฆฌ๋ฅผ ํ์ง ์๋๋ผ๋ ์ปดํ์ผ ์์ ์์๋ ์ ์์ ์ผ๋ก ์งํ๋๋ค.
JAVA 11์ ๊ธฐ์ค์ผ๋ก Chekced Exception, Unchecked Exception, ๊ทธ๋ฆฌ๊ณ Error์ ํด๋นํ๋ ํด๋์ค ๋ชฉ๋ก์ ๋ค์ ๋ฌธ์์์ ํ์ธํ ์ ์๋ค.
Spring @Transactionl๊ณผ Exception
์ด์ ์์์ ์ดํด๋ณธ CheckedException๊ณผ UncheckedException์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ๋์ํ๋ @Transactional ์ ๋ํ ์ด์ ์ ๋ํด ์์๋ณด๊ฒ ๋ค.
์คํ๋ง์์๋ @Transactional ์ ๋ํ ์ด์ ์ ์ฌ์ฉํ์ฌ ํธ๋์ญ์ ์ ์ฒ๋ฆฌํ๋๋ฐ, ์ด๋ฅผ ํตํด ๋ด๋ถ์ ์ผ๋ก ํ๋ก์ ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ , ๋ฉ์๋ ์คํ ์ ํ๋ก ํธ๋์ญ์ ๊ด๋ จ ๋ก์ง์ ์ฝ์ ํ์ฌ ํธ๋์ญ์ ์ ๊ด๋ฆฌํ๋ค. ๊ฐ๋จํ๊ฒ ๋งํ๋ฉด ๋ฉ์๋ ์คํ ์ค Exception์ด ๋ฐ์ํ๋ฉด rollback์ ์ํํ๊ณ , ์ ์์ ์ผ๋ก ์คํ๋๋ฉด commit์ด ์ด๋ฃจ์ด์ง๋ ๋ฐฉ์์ด๋ค.
๋ค๋ง ์ฌ๊ธฐ์ ์ ํํ๊ฒ ์ง๊ณ ๋์ด๊ฐ๋ฉด ๋ชจ๋ Exception์ ๋ํด์ ์ ๋ถ ๋กค๋ฐฑ์ ์ํค๋ ๊ฒ์ ์๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก Error์ UncheckedException(์ฆ, RuntimeException)์ ๋ํด์๋ง ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ฉฐ, CheckedException์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋กค๋ฐฑ๋์ง ์๋๋ค.(https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html)
Rollback ๊ณผ์
์ด์ ๋กค๋ฐฑ์ด ์ผ์ด๋๋ ๊ณผ์ ์ ๋ํด ์ดํด๋ณผ ํ ๋ฐ, ํต์ฌ์ด ๋๋ ๋ถ๋ถ๋ค์ ์์๋๋ก ํฌ๊ฒ ์๋์ ๋ค ๊ฐ์ง ํจ์๋ค์ด๋ค.
- TransactionAspectSupport.invokeWithinTransaction()
- TransactionAspectSupport.completeTransactionAfterThrowing()
- RuleBasedTransactionAttribute.rollbackOn()
- DefaultTransactionAttribute.rollbackOn()
๊ทธ๋ฆฌ๊ณ ์๋์ 5๊ฐ์ง ๋์์ ์ดํด๋ณผ ๊ฒ์ด๋ค.
- UncheckedException ๋ฐ์ ์ ํธ๋์ญ์ ๋์
- UncheckedException์ try-catch๋ก ์ก์ ๋ ํธ๋์ญ์ ๋์
- CheckedException ๋ฐ์ ์ ํธ๋์ญ์ ๋์
- CheckedException + rollbackFor์ ํธ๋์ญ์ ๋์
- UncheckedException + noRollbackFor์ ํธ๋์ญ์ ๋์
1. UncheckedException ๋ฐ์ ์ ํธ๋์ญ์ ๋์
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional
public void processOrders(List<Order> orders) {
for (Order order : orders) {
validateOrder(order);
orderRepository.save(order);
}
}
private void validateOrder(Order order) {
if (order.getAmount() < 0) {
throw new IllegalArgumentException("Invalid order amount"); // Unchecked Exception
}
}
}
์ ์ฝ๋์์ processOrders ๋ฉ์๋๋ ์ฃผ๋ฌธ ๋ชฉ๋ก์ ์ฒ๋ฆฌํ๋๋ฐ, order.getAmount()๊ฐ 0๋ณด๋ค ์์ ๊ฒฝ์ฐ IllegalArgumentException์ด ๋ฐ์ํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ด ์์ธ๋ UncheckedException์ด๋ฏ๋ก ํธ๋์ญ์ ์ด ์๋์ผ๋ก ๋กค๋ฐฑ๋์ด์ผ ํ ๊ฒ์ด๋ค.
๋ด๋ถ ๋์
์ผ๋จ @Transactional์ด ์ ์ฉ๋ ๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด ์คํ๋ง ๋ด๋ถ์ ์ผ๋ก TransactionAspectSupport.invokeWithinTransaction() ๋ฉ์๋๊ฐ ์คํ๋๋ค. ์ด ๋ฉ์๋๋ ํธ๋์ญ์ ์ ๊ด๋ฆฌํ๊ณ , ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ํธ๋์ญ์ ์ ๋กค๋ฐฑํ๋ ์ญํ ์ ํ๋ค.
TransactionAspectSupport.invokeWithinTransaction()
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, InvocationCallback invocation) throws Throwable {
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// ์ค์ ๋น์ฆ๋์ค ๋ฉ์๋ ํธ์ถ
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// ์์ธ ๋ฐ์ ์ ๋กค๋ฐฑ ์ฒ๋ฆฌ
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// ํธ๋์ญ์
์ ๋ฆฌ ์์
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo);
return retVal;
}
invokeWithinTransaction()๋ proceedWithInvocation()์ ํธ์ถํด ์ค์ ๋น์ฆ๋์ค ๋ฉ์๋๋ฅผ ์คํ์ํจ๋ค. ๊ทธ๋ฆฌ๊ณ try-catch๋ฅผ ํตํด ์ด ๊ณผ์ ์์ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด completeTransactionAfterThrowing()๊ฐ ํธ์ถ๋์ด ํธ๋์ญ์ ์ ๋กค๋ฐฑํ ์ง ๋๋ ์ปค๋ฐํ ์ง๋ฅผ ๊ฒฐ์ ํ๋ค.
TransactionAspectSupport.completeTransactionAfterThrowing()
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (txInfo.transactionAttribute.rollbackOn(ex)) { // ๋กค๋ฐฑ ์ฌ๋ถ ํ๋จ
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
} else {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}
}
completeTransactionAfterThrowing()์์ ์ค์ํ ๋ถ๋ถ์ txInfo.transactionAttribute.rollbackOn(ex)๊ฐ ์ฃผ์ด์ง ์์ธ์ ๋ฐ๋ผ ํธ๋์ญ์ ์ ๋กค๋ฐฑํ ์ง ์ปค๋ฐํ ์ง๋ฅผ ๊ฒฐ์ ํ๋ค๋ ์ ์ด๋ค. ์ด๋ TransactionAttribute์ ๊ตฌํ์ฒด๋ RuleBasedTransactionAttribute๊ฐ ๋ค์ด๊ฐ๊ฒ ๋๋ค.
RuleBasedTransactionAttribute.rollbackOn()
@Override
public boolean rollbackOn(Throwable ex) {
RollbackRuleAttribute winner = null; // ๊ฐ์ฅ ์ ํฉํ ๋กค๋ฐฑ ๊ท์น์ ๋ด์ ๋ณ์
int deepest = Integer.MAX_VALUE; // ๊ณ์ธต ๊น์ด๋ฅผ ์ถ์ ํ๊ธฐ ์ํ ๋ณ์ (์์์๋ก ์์ ํด๋์ค์ ๊ฐ๊น์)
// ๋กค๋ฐฑ ๊ท์น์ด ์กด์ฌํ๋ ๊ฒฝ์ฐ
if (this.rollbackRules != null) {
for (RollbackRuleAttribute rule : this.rollbackRules) {
// ํ์ฌ ์์ธ์ ์์ ๊ณ์ธต์์ ์ด ๊ท์น๊ณผ ์ผ์นํ๋์ง ํ์ธ
int depth = rule.getDepth(ex);
// ์ผ์นํ๋ ๊ท์น์ด ์์ผ๋ฉด, ํด๋น ๊ท์น์ ๊น์ด๋ฅผ ํ์ธ
if (depth >= 0 && depth < deepest) {
// ๋ ์์(๊ฐ๊น์ด) ๊น์ด์ ๊ท์น์ด ์์ผ๋ฉด ๊ทธ๊ฒ์ winner๋ก ์ค์
deepest = depth;
winner = rule;
}
}
}
// winner๊ฐ ์๋ค๋ฉด ๊ธฐ๋ณธ ๊ท์น(UncheckedException ๋๋ Error์ผ ๋ ๋กค๋ฐฑ)์ ์ ์ฉ
if (winner == null) {
return super.rollbackOn(ex); // ๊ธฐ๋ณธ ๋กค๋ฐฑ ๊ท์น: RuntimeException ๋๋ Error์ผ ๊ฒฝ์ฐ ๋กค๋ฐฑ
}
// winner๊ฐ NoRollbackRuleAttribute๊ฐ ์๋๋ฉด ๋กค๋ฐฑ, ๋ง๋ค๋ฉด ์ปค๋ฐ
return !(winner instanceof NoRollbackRuleAttribute);
}
๋กค๋ฐฑ์ ์ฌ๋ถ๋ rollbackOn()์์ ๊ฒฐ์ ๋๋๋ฐ, rollbackOn()์ ์คํ๋ง์ ์์ธ ํ์ ์ ๊ฒ์ฌํ์ฌ ํธ๋์ญ์ ์ ๋กค๋ฐฑํ ์ง ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๋ฉฐ ๊ธฐ๋ณธ์ ์ผ๋ก RuntimeException ๋๋ Error๊ฐ ๋ฐ์ํ์ ๋ ๋กค๋ฐฑ์ด ์ํ๋๋ค.
์ฌ๊ธฐ์ winner๋ ๋ค์๊ณผ ๊ฐ์ ์ญํ ์ ํ๋ค.
- ๋ฐ์ํ ์์ธ์ ๊ฐ์ฅ ์ ํฉํ ๋กค๋ฐฑ ๊ท์น์ ์ฐพ๋ ์ญํ ์ ๋ด๋นํ๋ค.
- ์์ธ์ ์์ ๊ณ์ธต์์ ๊ฐ์ฅ ๊ฐ๊น์ด ๋กค๋ฐฑ ๊ท์น(์ฆ, ์์ธ ๊ณ์ธต ๊ตฌ์กฐ์ ๊ฐ์ฅ ์์ ๊น์ด์ ์๋ ๊ท์น)์ ์ ํํ๋ค.
- ์๋ฅผ ๋ค์ด, ์์ธ๊ฐ IllegalArgumentException์ผ ๋, IllegalArgumentException์ ๋ํ ๊ท์น์ด ์์ผ๋ฉด ํด๋น ๊ท์น์ด winner๊ฐ ๋๋ค. ๊ท์น์ด ์๋ค๋ฉด ์์ ํด๋์ค์ธ RuntimeException์ ๋ํ ๊ท์น์ ์ฐพ๋๋ค.
๋ง์ฝ ์ฐ๋ฆฌ ์์์ฒ๋ผ winner๊ฐ ์์ผ๋ฉด(null์ด๋ฉด) ๊ธฐ๋ณธ์ ์ผ๋ก UncheckedException(RuntimeException)์ด๋ Error์ ๋ํด ๋กค๋ฐฑ์ ์ํํ๋ super.rollbackOn(ex) ๋ฉ์๋๊ฐ ํธ์ถ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ด ๋์์ DefaultTransactionAttribute.rollBackOn()์์ ์ฒ๋ฆฌ๋๋ค.
DefaultTransactionAttribute.rollBackOn()
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error); // UncheckedException ๋๋ Error ์ ๋กค๋ฐฑ
}
rollbackOn()์ true๋ก ๋ฐํ๋๊ณ ๋ฐ๋ผ์ @Transactional ์ฌ์ฉ ์ UncheckedException(RuntimeException)์ ๋ณ๋๋ก ์ฒ๋ฆฌํ์ง ์์ผ๋ฉด ์๋์ผ๋ก ๋กค๋ฐฑ์ด ์ผ์ด๋๊ฒ ๋๋ค.
2. UncheckedException์ try-catch๋ก ์ก์ ๋ ํธ๋์ญ์ ๋์
์ด๋ฒ์๋ @Transactional์ ์ฌ์ฉํ์ฌ UncheckedException(์ฆ, RuntimeException)์ด ๋ฐ์ํ์ง๋ง, ์ด๋ฅผ try-catch๋ก ์ฒ๋ฆฌํ์ ๋ ํธ๋์ญ์ ์ด ์ด๋ป๊ฒ ๋์ํ๋์ง ์ดํด๋ณด์
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional
public void processOrders(List<Order> orders) {
for (Order order : orders) {
validateOrderWithTryCatch(order);
orderRepository.save(order);
}
}
private void validateOrderWithTryCatch(Order order) {
if (order.getAmount() < 0) {
try {
throw new IllegalArgumentException("Invalid order amount"); // Unchecked Exception
} catch (IllegalArgumentException e) {
e.printStackTrace(); // ์์ธ๋ฅผ try-catch๋ก ์ฒ๋ฆฌํ์ฌ ๋ณต๊ตฌ๋ ๊ฒ์ผ๋ก ๊ฐ์ฃผ
}
}
}
}
๋ด๋ถ ๋์
1๋ฒ๊ณผ ๋์ผํ๊ฒ TransactionAspectSupport.invokeWithinTransaction()๊ฐ ์คํ๋๋ค.
TransactionAspectSupport.invokeWithinTransaction()
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, InvocationCallback invocation) throws Throwable {
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// ์ค์ ๋น์ฆ๋์ค ๋ฉ์๋ ํธ์ถ
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// ์์ธ ๋ฐ์ ์ ๋กค๋ฐฑ ์ฒ๋ฆฌ
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// ํธ๋์ญ์
์ ๋ฆฌ ์์
cleanupTransactionInfo(txInfo);
}
commitTransactionAfterReturning(txInfo); // ์์ธ๊ฐ ์์ผ๋ฏ๋ก ํธ๋์ญ์
์ด ์ปค๋ฐ๋จ
return retVal;
}
๋ค๋ง ์ด๋ฒ์๋ ๋น์ฆ๋์ค ๋ก์ง์์ ์์ธ๊ฐ try-catch๋ก ์ฒ๋ฆฌ๋์๊ธฐ ๋๋ฌธ์, invokeWithinTransaction() ๋ฉ์๋์ catch ๋ธ๋ก์์ ๊ฐ์ง๋์ง ์๋๋ค. ๋ฐ๋ผ์ completeTransactionAfterThrowing() ๋ฉ์๋๊ฐ ํธ์ถ๋์ง ์์ ํธ๋์ญ์ ์ ์ ์์ ์ผ๋ก ์ปค๋ฐ๋๋ค.
๊ฒฐ๋ก ์ ์ผ๋ก UncheckedException(์ฆ, RuntimeException)์ด ๋ฐ์ํ๋๋ผ๋ ์ด๋ฅผ try-catch๋ก ์ฒ๋ฆฌํ๋ฉด ํธ๋์ญ์ ์ ๋กค๋ฐฑ๋์ง ์๊ณ ์ปค๋ฐ๋๋ค.
3. CheckedException ๋ฐ์ ์ ํธ๋์ญ์ ๋์
CheckedException์ ์๋์์ ์ธ๊ธํ๋ค์ํผ ์ปดํ์ผ ์์ ์์ ๋ฐ๋์ ์ฒ๋ฆฌํด์ผ ํ๋ ์์ธ๋ก, try-catch๋ก ์ฒ๋ฆฌํ์ง ์๊ฑฐ๋ ๋ฉ์๋ ์ ์ธ๋ถ์ throws๋ก ์์ธ๋ฅผ ๋์ง์ง ์์ผ๋ฉด ์ปดํ์ผ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค. ๊ฒฐ๋ก ๋จผ์ ๋ณด๋ฉด ์ด๋ค ์ฒ๋ฆฌ๋ฅผ ํด์ฃผ๋ ๊ฐ์ ๊ธฐ๋ณธ ์ค์ ์ผ๋ก๋ ํธ๋์ญ์ ์์ CheckedException์ด ๋ฐ์ํ๋๋ผ๋ ํธ๋์ญ์ ์ ๋กค๋ฐฑ๋์ง ์๊ณ ์ปค๋ฐ๋๋ค. ๋ค๋ง ์ฒ๋ฆฌ ๋ฐฉ์์ ๋ฐ๋ผ ๋ด๋ถ์ ์ผ๋ก ๋์ํ๋ ๊ณผ์ ์ ๋ค๋ฅด๋ ํ๋ฒ ์ดํด๋ณด์.
ChckedException์ ๋ค์ ๋ ๊ฐ์ง๋ก ๋๋์ด ์ฒ๋ฆฌํ ์ ์๋ค.
- try-catch๋ก ์ฒ๋ฆฌ
- throws๋ก ์ฒ๋ฆฌ
try-catch๋ก ์ฒ๋ฆฌ
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional
public void processOrders(List<Order> orders) {
for (Order order : orders) {
try {
validateOrder(order);
orderRepository.save(order);
} catch (IOException e) {
e.printStackTrace(); // CheckedException์ try-catch๋ก ์ฒ๋ฆฌํ์ฌ ํธ๋์ญ์
์ ์ํฅ์ ์ฃผ์ง ์์
}
}
}
private void validateOrder(Order order) throws IOException {
// ์ฃผ๋ฌธ ์ค๋ช
์ด ์์ผ๋ฉด IOException์ ๋ฐ์์ํด (Checked Exception)
if (order.getDescription() == null || order.getDescription().isEmpty()) {
throw new IOException("Order description cannot be empty"); // Checked Exception
}
}
}
์ด ๊ฒฝ์ฐ ํธ๋์ญ์ ๋์ ๋ฐฉ์์ 2. UncheckedException์ try-catch๋ก ์ก์ ๋ ํธ๋์ญ์ ๋์๊ณผ ๋์ผํ๊ฒ ์ฒ๋ฆฌ๋๋ค.
TransactionAspectSupport.invokeWithinTransaction()๊ฐ ์คํ๋๋๋ผ๋ ๋น์ฆ๋์ค ๋ก์ง์์ ์์ธ๊ฐ try-catch๋ก ์ฒ๋ฆฌ๋์๊ธฐ ๋๋ฌธ์, invokeWithinTransaction() ๋ฉ์๋์ catch ๋ธ๋ก์์ ๊ฐ์ง๋์ง ์๋๋ค. ๋ฐ๋ผ์ completeTransactionAfterThrowing() ๋ฉ์๋๋ ํธ์ถ๋์ง ์์ผ๋ฉฐ, ํธ๋์ญ์ ์ ์ ์์ ์ผ๋ก ์ปค๋ฐ๋๋ค.
throws๋ก ์ฒ๋ฆฌ
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional
public void processOrders(List<Order> orders) throws IOException {
for (Order order : orders) {
validateOrder(order);
orderRepository.save(order);
}
}
private void validateOrder(Order order) throws IOException {
// ์ฃผ๋ฌธ ์ค๋ช
์ด ์์ผ๋ฉด IOException์ ๋ฐ์์ํด (Checked Exception)
if (order.getDescription() == null || order.getDescription().isEmpty()) {
throw new IOException("Order description cannot be empty"); // Checked Exception
}
}
}
์ด๋ฒ์๋ ๋ฉ์๋ ์ ์ธ๋ถ์ throws๋ฅผ ์ฌ์ฉํ์ฌ ์์ธ๋ฅผ ์์ ๊ณ์ธต์ผ๋ก ๊ณ์ํด์ ๋์ก๋ค. ๊ฐ๋จํ ์์๋ผ ์๋น์ค๋ง ์ธ๊ธํ์ง๋ง ์ปจํธ๋กค๋ฌ๊น์ง throws๋ฅผ ๋์ก๋ค๊ณ ๋ณด๋ฉด ๋๋ค.
์ด๋ฒ์๋ IOException์ด TransactionAspectSupport.invokeWithinTransaction() ๋ด๋ถ์ try-catch ๋ธ๋ก์์ ์กํ๊ธฐ ๋๋ฌธ์ completeTransactionAfterThrowing()๊ฐ ํธ์ถ๋๋ค.
๊ทธ๋ฆฌ๊ณ @Transactional์ rollbackFor ์ค์ ์ ํ์ง ์์๊ธฐ ๋๋ฌธ์, RuleBasedTransactionAttribute.rollbackRules์๋ ์๋ฌด ๋กค๋ฐฑ ๊ท์น์ด ์กด์ฌํ์ง ์๋๋ค. ๋ฐ๋ผ์ winner๊ฐ null์ด ๋์ด 1. UncheckedException ๋ฐ์ ์ ํธ๋์ญ์ ๋์๊ณผ ๋์ผํ๊ฒ DefaultTransactionAttribute.rollBackOn()๊ฐ ํธ์ถ๋๋ค.
๋ค๋ง ์ด๋ฒ์๋ ๋์ด์ค๋ ์์ธ๊ฐ RuntimeException์ด๋ Error ํ์ ์ด ์๋๊ธฐ ๋๋ฌธ์ rollbackOn() ๋ฉ์๋๋ false๋ฅผ ๋ฐํํ์ฌ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋์ง ์๊ณ ์ปค๋ฐ๋๋ค.
4. CheckedException ๋ฐ์ ์ ํธ๋์ญ์ ๋์(rollbakFor ์ฌ์ฉ)
์ง๊ธ๊น์ง ๋ด์ฉ์ ์ ์ดํดํ๋ค๋ฉด, ๋ณ๋๋ก try-catch๋ก ์ก๋ ์๊ฐ ์์ธ๊ฐ ๋ฐ์ํ๋๋ผ๋ TransactionAspectSupport.invokeWithinTransaction() ๋ฉ์๋์ catch ๋ธ๋ก์์ ์์ธ๊ฐ ๊ฐ์ง๋์ง ์๊ธฐ ๋๋ฌธ์ completeTransactionAfterThrowing() ๋ฉ์๋๋ ํธ์ถ๋์ง ์์ผ๋ฉฐ, ์ด๋ก ์ธํด ํธ๋์ญ์ ์ ์ ์์ ์ผ๋ก ์ปค๋ฐ๋๋ค๋ ์ฌ์ค์ ์ ์ ์์ ๊ฒ์ด๋ค.
๋ฐ๋ผ์ ์ด๋ฒ์๋ throws๋ก ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฒฝ์ฐ์ ๋ํด์๋ง ์ดํด๋ณผ ๊ฒ์ด๋ค.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional(rollbackFor = IOException.class) // rollbackFor ์ง์ !
public void processOrders(List<Order> orders) throws IOException {
for (Order order : orders) {
validateOrder(order);
orderRepository.save(order);
}
}
private void validateOrder(Order order) throws IOException {
// ์ฃผ๋ฌธ ์ค๋ช
์ด ์์ผ๋ฉด IOException์ ๋ฐ์์ํด (Checked Exception)
if (order.getDescription() == null || order.getDescription().isEmpty()) {
throw new IOException("Order description cannot be empty"); // Checked Exception
}
}
}
์ ์ฝ๋์์๋ @Transactional์์ rollbackFor ์ต์ ์ ์ฌ์ฉํด IOException์ด ๋ฐ์ํ์ ๋ ํธ๋์ญ์ ์ ๋กค๋ฐฑํ๋๋ก ์ค์ ํ๋ค. (@Transactional์์๋ rollbackFor์ด๋ผ๋ ํ๋๋ก Throwable์ ๋ชจ๋ ํ์ ์์ธ ํด๋์ค๋ค์ ๋กค๋ฐฑ์ ์ ๋ฐํ๋๋ก ์ ์ํ ์ ์๋ค. - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html)
TransactionAspectSupport.invokeWithinTransaction()
๋ง์ฐฌ๊ฐ์ง๋ก IOException์ด invokeWithinTransaction() ๋ฉ์๋ ๋ด๋ถ try-catch์ ์กํ๊ธฐ ๋๋ฌธ์ completeTransactionAfterThrowing()์ด ํธ์ถ๋๋ค.
RuleBasedTransactionAttribute.rollbackOn()
๋ค๋ง ์ด๋ฒ์๋ @Transactional์ rollbackFor ์ค์ ํ๊ธฐ ๋๋ฌธ์ RuleBasedTransactionAttribute.rollbackRules ์๋ IOException์ด ๋ค์ด๊ฐ๊ฒ ๋๋ค. ๋ฐ๋ผ์ winner๊ฐ null์ด ๋์ง ์์ rollbackOn์ด true๋ก ๋ฐํ๋์ด TransactionAspectSupport.completeTransactionAfterThrowing() ๋ฉ์๋ ๋ด๋ถ์์ ๋กค๋ฐฑ ๋ก์ง์ ์ํํ๊ฒ ๋๋ค.
๊ฒฐ๋ก ์ ์ผ๋ก @Transactional(rollbackFor = IOException.class)์ ์ฌ์ฉํ๋ฉด IOException์ throw ํ์ ๋ ๋กค๋ฐฑ์ด ์ผ์ด๋๋ค.
5. UncheckedException + noRollbackFor์ ํธ๋์ญ์ ๋์
์ด๋ฒ์๋ rollbackFor์ด ์๋ noRollbackFor์ ์ฌ์ฉ ์ ๋์ ๊ณผ์ ์ด๋ค.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional(noRollbackFor = IllegalArgumentException.class) // noRollbackFor ์ง์ !
public void processOrders(List<Order> orders) {
for (Order order : orders) {
validateOrder(order);
orderRepository.save(order);
}
}
private void validateOrder(Order order) {
// ์ฃผ๋ฌธ ๊ธ์ก์ด 0๋ณด๋ค ์์ผ๋ฉด IllegalArgumentException ๋ฐ์ (Unchecked Exception)
if (order.getAmount() < 0) {
throw new IllegalArgumentException("Order amount cannot be negative"); // Unchecked Exception
}
}
}
noRollbackFor์ด ์๋๋ผ๋ฉด ์ ๋ก์ง์ ๋กค๋ฐฑ๋์ด์ผ ํ ๊ฒ์ด๋ค. ํ์ง๋ง noRollbackFor ์ต์ ์ ์ฌ์ฉํ๋ฉด ํน์ UncheckedException์ด ๋ฐ์ํ๋๋ผ๋ ํธ๋์ญ์ ์ ๋กค๋ฐฑํ์ง ์๊ณ ์ปค๋ฐํ ์ ์๋๋ก ์ค์ ํ ์ ์๋ค. ๋์๊ณผ์ ์ ์ดํด๋ณด์.
TransactionAspectSupport.invokeWithinTransaction()
๋น์ฐํ๊ฒ๋ IllegalArgumentException์ด TransactionAspectSupport.invokeWithinTransaction() ๋ด๋ถ์ try-catch ๋ธ๋ก์์ ์กํ๊ธฐ ๋๋ฌธ์ completeTransactionAfterThrowing()์ด ํธ์ถ๋๋ค.
RuleBasedTransactionAttribute.rollbackOn()
@Override
public boolean rollbackOn(Throwable ex) {
RollbackRuleAttribute winner = null; // ๋กค๋ฐฑ ๊ท์น์ ๊ฒฐ์ ํ ๋ณ์
int deepest = Integer.MAX_VALUE; // ๊ณ์ธต ๊น์ด๋ฅผ ์ถ์ ํ๊ธฐ ์ํ ๋ณ์
if (this.rollbackRules != null) {
for (RollbackRuleAttribute rule : this.rollbackRules) {
// ์์ธ๊ฐ ๋กค๋ฐฑ ๊ท์น๊ณผ ์ผ์นํ๋์ง ํ์ธ
int depth = rule.getDepth(ex);
// ์์ธ์ ๊น์ด๊ฐ ๋ ์๋ค๋ฉด(๊ณ์ธต์ ์ผ๋ก ๋ ๊ฐ๊น์ด ๊ท์น์ด๋ผ๋ฉด) ํด๋น ๊ท์น์ winner๋ก ์ค์
if (depth >= 0 && depth < deepest) {
deepest = depth;
winner = rule;
}
}
}
// IllegalArgumentException์ ๋ํ ๋กค๋ฐฑ ๋ฐฉ์ง ๊ท์น์ด ์์ผ๋ฏ๋ก winner๊ฐ NoRollbackRuleAttribute
if (winner == null) {
return super.rollbackOn(ex); // ๊ธฐ๋ณธ์ ์ผ๋ก RuntimeException๊ณผ Error์ผ ๋ ๋กค๋ฐฑ
}
return !(winner instanceof NoRollbackRuleAttribute); // NoRollbackRuleAttribute๋ฉด false ๋ฐํ
}
๋ค๋ง ์๋๋ผ๋ฉด 1. UncheckedException ๋ฐ์ ์ ํธ๋์ญ์ ๋์์ฒ๋ผ winner๊ฐ null์ด ๋์ด rollbackOn()์ด true๋ก ๋ฐํ๋์ด ๋กค๋ฐฑ์ด ์ผ์ด๋๊ฒ ์ง๋ง ์ด๋ฒ์๋ rollbackRules์ IllegalArgumentException์ ๋ํ ๋กค๋ฐฑ ๋ฐฉ์ง ๊ท์น์ด ์ค์ ๋์ด ์๊ธฐ ๋๋ฌธ์ rollbackOn() ๋ฉ์๋๋ false๋ฅผ ๋ฐํํ์ฌ ๋กค๋ฐฑ๋์ง ์๊ณ ์ฑ๊ณต์ ์ผ๋ก ์ปค๋ฐ๋๋ค.
๊ฒฐ๋ก ์ ์ผ๋ก @Transactional(noRollbackFor = RuntimeException.class)์ ์ฌ์ฉํ๋ฉด RuntimeException์ด ๋ฐ์ํ๋๋ผ๋ ๋กค๋ฐฑ์ด ์ผ์ด๋์ง ์๊ณ , ์ฑ๊ณต์ ์ผ๋ก ์ปค๋ฐ๋๋ค.
์ ๋ฆฌ
Java์ ์์ธ ์ข ๋ฅ์ Spring์์ @Transactional์ด ์์ธ๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ์ฌ ๋กค๋ฐฑํ๊ฑฐ๋ ์ปค๋ฐํ๋์ง์ ๋ํด ์ดํด๋ณด์๋ค. ์ด ๊ธ์ ์ด 2๋ถ๋ก ๋๋๋ฉฐ, ์ด๋ฒ 1ํธ์์๋ ํธ๋์ญ์ ๊ณผ ์์ธ ์ฒ๋ฆฌ์ ๊ธฐ๋ณธ ๊ฐ๋ ๊ณผ ๋์ ๋ฐฉ์์ ์ดํด๋ณด์๋ค. ๋ค์ 2ํธ์์๋ ์๋ก ๋ค๋ฅธ ํด๋์ค์์ ํธ๋์ญ์ ์ ํธ์ถํ ๋, Propagation.REQUIRES_NEW๋ฅผ ์ฌ์ฉํ ๋ ๋ฑ์ ์์๋ก ๋ค์ด ๋ ๋ค์ํ ์ํฉ๋ค์ ์์๋ฅผ ๋ค์ด, ํด๋น ๋ก์ง์ด ๋กค๋ฐฑ๋ ์ง ์ปค๋ฐ๋ ์ง ๋ง์ถฐ๋ณด๋ ๋ฐฉ์์ผ๋ก ์ดํด๋ณผ ๊ฒ์ด๋ค.
์ ๋ด์ฉ์ ์ ์ฒด์ ์ธ ํ๋ฆ์ https://sup2is.github.io/2021/03/04/java-exceptions-and-spring-transactional.html๋ฅผ ๋ง์ด ์ฐธ๊ณ ํ๋ค.