๊ฐ์
์ด์ ๊ธ์์๋ Spring์ @Transactional ์ด๋ ธํ ์ด์ ์ด ๊ธฐ๋ณธ์ ์ผ๋ก ์ด๋ป๊ฒ ๋์ํ๋์ง, ๊ทธ๋ฆฌ๊ณ ์์ธ ์ฒ๋ฆฌ์ ๋ฐ๋ผ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ ๋ฉ์ปค๋์ฆ์ ๋ํด ๋ค๋ฃจ์๋ค. ์ด๋ฒ ๊ธ์์๋ ์ค์ ์ฝ๋์ ํ ์คํธ๋ฅผ ํตํด ๋ค์ํ ์ํฉ์์ ํธ๋์ญ์ ์ด ์ด๋ป๊ฒ ๋์ํ๋์ง ์ดํด๋ณผ ์์ ์ด๋ค. ๊ธ์ ์ฝ๋ ๋ถ๋ค๋ ๊ฐ ์ํฉ์์ ๊ฒฐ๊ณผ๋ฅผ ์์ธกํด ๋ณด๋ฉฐ ๋ฐ๋ผ๊ฐ๋ฉด ์ดํด์ ๋์์ด ๋ ๊ฒ ๊ฐ๋ค.
๋์ผํ ํด๋์ค ๋ด์์์ ํธ๋์ญ์ ๋์
- ๋ณธ์ธ ๋ฉ์๋์์ ์์ธ ๋ฐ์ ์ ๋กค๋ฐฑ
- ๋ค๋ฅธ ๋ฉ์๋ ํธ์ถ ํ ์์ธ ๋ฐ์ ์ ๋กค๋ฐฑ
- ๋ณธ์ธ ๋ฉ์๋์์ try-catch๋ก ์์ธ ์ฒ๋ฆฌ ํ ์ปค๋ฐ
- ๋ค๋ฅธ ๋ฉ์๋ ํธ์ถ ํ ๋ณธ์ธ ๋ฉ์๋์์ try-catch๋ก ์์ธ ์ฒ๋ฆฌ ํ ์ปค๋ฐ
- ๋ณธ์ธ ๋ฉ์๋์์ noRollbackFor ์ค์ ํ ์์ธ ๋ฐ์ ์ ์ปค๋ฐ
- noRollbackFor ์ค์ ๋ ๋ค๋ฅธ ๋ฉ์๋ ํธ์ถ ์์๋ ์์ ๋ฉ์๋์ ํธ๋์ญ์ ์ค์ ์ ๋ฐ๋ผ ๋กค๋ฐฑ๋จ
- ๋์ผ ํด๋์ค ๋ด์์ REQUIRES_NEW ์ค์ ๋ ๋ฉ์๋ ํธ์ถ ํ ์์ธ ๋ฐ์ ์ ๋กค๋ฐฑ๋จ
- noRollbackFor ์ค์ ํ try-catch๋ก ์์ธ ์ฒ๋ฆฌ ํ ์ปค๋ฐ
๋ค๋ฅธ ํด๋์ค ๊ฐ์ ํธ๋์ญ์ ๋์
- ServiceA(์๋ฌ) -> ServiceB์ REQUIRES_NEW ๋ฉ์๋ ํธ์ถ ์ ๋ถ๋ชจ ํธ๋์ญ์ ๋กค๋ฐฑ, ์์์ ์ปค๋ฐ
- ServiceA try-catch ์์ธ์ฒ๋ฆฌ -> ServiceB(์๋ฌ) ๋ฉ์๋ ํธ์ถ ์ ๋กค๋ฐฑ๋จ
- ServiceA -> ServiceB(์๋ฌ)์ noRollbackFor ์ค์ ๋ฉ์๋ ํธ์ถ ์ ๋กค๋ฐฑ
- ServiceA(noRollbackFor) -> ServiceB(์๋ฌ) ๋ฉ์๋ ํธ์ถ
- ServiceA try-catch ์์ธ์ฒ๋ฆฌ -> ServiceB(์๋ฌ)์ noRollbackFor ์ค์ ๋ฉ์๋ ํธ์ถ ์ ์ปค๋ฐ
๋ฐ์์ ๋ถ๋ค์ ์์ ์์ ๋ฃ์ ์ธ ๊ฐ์ง ์์๋ง์ด๋ผ๋ ๊ผญ ์ดํด๋ณด์์ผ๋ฉด ์ข๊ฒ ๋ค.
๋์ผํ ํด๋์ค ๋ด์์์ ํธ๋์ญ์ ๋์
1. ๋ณธ์ธ ๋ฉ์๋์์ ์์ธ ๋ฐ์
@Service
public class ServiceA {
@Transactional
public void callOwnMethodWithException(Long memberId) {
memberRepository.updateNameById(memberId, "Updated Name A1");
throw new RuntimeException("Intentional Exception");
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA: ๋ณธ์ธ ๋ฉ์๋์์ ์์ธ ๋ฐ์ ์ ๋กค๋ฐฑ")
void testOwnMethodWithExceptionRollback() {
assertThrows(RuntimeException.class, () -> serviceA.callOwnMethodWithException(1L));
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Initial Name", updatedMember.getName());
}
}
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ํธ๋์ญ์ ๋กค๋ฐฑ ์ฌ๋ก๋ค. @Transactionl์ด ์ ์ฉ๋ ๋ฉ์๋ ๋ด์์ RuntimeException์ด ๋ฐ์ํ๋ฉด ํด๋น ํธ๋์ญ์ ์ ๋กค๋ฐฑ๋๋ค. 1ํธ์์ ์ดํด๋ณธ ๊ฒ์ฒ๋ผ @Transactional์ด ์ ์ฉ๋ ๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด ์คํ๋ง์ ๋ด๋ถ์ ์ผ๋ก 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;
}
RuntimeException์ด ๋ฐ์ํ๋ฉด catch ๋ธ๋ก์์ completeTransactionAfterThrowing()์ด ํธ์ถ๋๊ณ , ์ด ๋ฉ์๋๋ RuleBasedTransactionAttribute.rollbackOn()์ ํตํด ๋กค๋ฐฑ ์ฌ๋ถ๋ฅผ ๊ฒฐ์ ํ๊ฒ ๋๋ค. ๊ฒฐ๊ตญ RuntimeException๊ณผ ๊ฐ์ UncheckedException์ด ๋ฐ์ํ๋ฏ๋ก ํธ๋์ญ์ ์ ๋กค๋ฐฑ๋์ด ๋ณ๊ฒฝ์ฌํญ์ด ๋ฐ์๋์ง ์๋๋ค.
2. ๋ค๋ฅธ ๋ฉ์๋ ํธ์ถ ํ ์์ธ ๋ฐ์
@Service
public class ServiceA {
@Transactional
public void callAnotherMethodWithException(Long memberId) {
callOwnMethodWithException(memberId);
}
@Transactional
public void callOwnMethodWithException(Long memberId) {
memberRepository.updateNameById(memberId, "Updated Name A1");
throw new RuntimeException("Intentional Exception");
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA: ๋ค๋ฅธ ๋ฉ์๋ ํธ์ถ ํ ์์ธ ๋ฐ์ ์ ๋กค๋ฐฑ")
void testAnotherMethodWithExceptionRollback() {
assertThrows(RuntimeException.class, () -> serviceA.callAnotherMethodWithException(1L));
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Initial Name", updatedMember.getName());
}
}
์ด ๊ฒฝ์ฐ๋ ๋์ผํ๊ฒ @Transactional์ด ์ ์ฉ๋ ๋ฉ์๋ ๋ด์์ RuntimeException์ด ๋ฐ์ํ๋ฏ๋ก ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ค.
3. ๋ณธ์ธ ๋ฉ์๋์์ try-catch๋ก ์์ธ ์ฒ๋ฆฌ
@Service
public class ServiceA {
@Transactional
public void callOwnMethodWithTryCatch(Long memberId) {
try {
memberRepository.updateNameById(memberId, "Updated Name A2");
throw new RuntimeException("Intentional Exception");
} catch (RuntimeException e) {
// ์์ธ๋ฅผ ์ฒ๋ฆฌํ์ฌ ํธ๋์ญ์
๋กค๋ฐฑ์ ๋ฐฉ์ง
}
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA: ๋ณธ์ธ ๋ฉ์๋์์ try-catch๋ก ์์ธ ์ฒ๋ฆฌ ํ ์ปค๋ฐ")
void testOwnMethodWithTryCatchCommit() {
serviceA.callOwnMethodWithTryCatch(1L);
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Updated Name A2", updatedMember.getName());
}
}
@Transactional์ด ์ ์ฉ๋ ๋ฉ์๋์์ ์์ธ๊ฐ ๋ฐ์ํด๋ try-catch๋ก ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ฉด ํธ๋์ญ์ ์ ๋กค๋ฐฑ๋์ง ์๊ณ ์ปค๋ฐ๋๋ค. 1ํธ์์ ๋ค๋ค๋ฏ์ด Spring์ ํธ๋์ญ์ ๊ด๋ฆฌ ๋ฉ์ปค๋์ฆ ๋๋ฌธ์ธ๋ฐ, ๋ด๋ถ์ ์ผ๋ก 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;
}
// ๋น์ฆ๋์ค ๋ก์ง์ด ์ ์์ ์ผ๋ก ์๋ฃ๋๋ฉด ์ปค๋ฐ ์ํ
commitTransactionAfterReturning(txInfo);
return retVal;
}
๋ฐ๋ผ์ RuntimeException์ด ๋ฐ์ํ๋ฉด try ๋ธ๋ก ๋ด์์ ์์ธ๊ฐ ๋ฐ์ํ๋๋ผ๋ catch ๋ธ๋ก์์ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ฉด invokeWithinTransaction() ๋ฉ์๋์ catch ๋ธ๋ก์ ๋๋ฌํ์ง ์๊ธฐ ๋๋ฌธ์ Spring์ ๋น์ฆ๋์ค ๋ก์ง์ด ์ ์์ ์ผ๋ก ์๋ฃ๋ ๊ฒ์ผ๋ก ํ๋จํ๊ณ ํธ๋์ญ์ ์ ์ปค๋ฐํ๋ค.
4. ๋ค๋ฅธ ๋ฉ์๋ ํธ์ถ ํ try-catch๋ก ์์ธ ์ฒ๋ฆฌ
@Service
public class ServiceA {
@Transactional
public void callAnotherMethodWithTryCatch(Long memberId) {
try {
callOwnMethodWithException(memberId);
} catch (RuntimeException e) {
// ์์ธ ์ฒ๋ฆฌ๋ก ํธ๋์ญ์
๋กค๋ฐฑ ๋ฐฉ์ง
}
}
@Transactional
public void callOwnMethodWithException(Long memberId) {
memberRepository.updateNameById(memberId, "Updated Name A1");
throw new RuntimeException("Intentional Exception");
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA: ๋ค๋ฅธ ๋ฉ์๋ ํธ์ถ ํ try-catch๋ก ์์ธ ์ฒ๋ฆฌ ํ ์ปค๋ฐ")
void testAnotherMethodWithTryCatchCommit() {
serviceA.callAnotherMethodWithTryCatch(1L);
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Updated Name A1", updatedMember.getName());
}
}
์ด ๊ฒฝ์ฐ๋ ๋ง์ฐฌ๊ฐ์ง๋ก ์์ธ๊ฐ try-catch๋ก ์ฒ๋ฆฌ๋์ด ํธ๋์ญ์ ์ด ์ปค๋ฐ๋๋ค. callOwnMethodWithException์์ ๋ฐ์ํ RuntimeException์ด callAnotherMethodWithTryCatch์ try-catch ๋ธ๋ก์์ ์ฒ๋ฆฌ๋๋ฏ๋ก, ํธ๋์ญ์ ์ด ์ ์์ ์ผ๋ก ์ปค๋ฐ๋์ด "Updated Name A1"๋ก์ ๋ณ๊ฒฝ์ด ์ ์ง๋๋ค.
5. ๋ณธ์ธ ๋ฉ์๋์์ noRollbackFor ์ค์ ํ ์์ธ ๋ฐ์
@Service
public class ServiceA {
@Transactional(noRollbackFor = RuntimeException.class)
public void callOwnMethodWithNoRollback(Long memberId) {
memberRepository.updateNameById(memberId, "Updated Name A3");
throw new RuntimeException("Intentional Exception");
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA: ๋ณธ์ธ ๋ฉ์๋์์ noRollbackFor ์ค์ ํ ์์ธ ๋ฐ์ ์ ์ปค๋ฐ")
void testOwnMethodWithNoRollbackCommit() {
assertThrows(RuntimeException.class, () -> serviceA.callOwnMethodWithNoRollback(1L));
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Updated Name A3", updatedMember.getName());
}
}
1ํธ์์ ์ดํด๋ณธ ๊ฒ์ฒ๋ผ, noRollbackFor ์ค์ ์ ์ฌ์ฉํ๋ฉด 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) {
deepest = depth;
winner = rule;
}
}
}
// noRollbackFor ์ค์ ์ผ๋ก ์ธํด winner๊ฐ NoRollbackRuleAttribute ์ธ์คํด์ค๊ฐ ๋จ
if (winner == null) {
return super.rollbackOn(ex); // ๋ถ๋ชจ ํด๋์ค์ rollbackOn ํธ์ถ
}
// NoRollbackRuleAttribute ์ธ์คํด์ค๋ผ๋ฉด ๋กค๋ฐฑํ์ง ์์
return !(winner instanceof NoRollbackRuleAttribute);
}
๋ฐ๋ผ์ RuntimeException์ด ๋ฐ์ํ๋๋ผ๋ noRollbackFor ์ค์ ์ ์ํด ๋กค๋ฐฑ๋์ง ์๊ณ ๋ณ๊ฒฝ์ฌํญ์ด ์ปค๋ฐ๋๋ค.
6. noRollbackFor ์ค์ ๋ ๋ค๋ฅธ ๋ฉ์๋ ํธ์ถ ํ ์์ธ ๋ฐ์
@Service
public class ServiceA {
@Transactional
public void callAnotherMethodWithNoRollback(Long memberId) {
callOwnMethodWithNoRollback(memberId);
}
@Transactional(noRollbackFor = RuntimeException.class)
public void callOwnMethodWithNoRollback(Long memberId) {
memberRepository.updateNameById(memberId, "Updated Name A3");
throw new RuntimeException("Intentional Exception");
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA: noRollbackFor ์ค์ ๋ ๋ค๋ฅธ ๋ฉ์๋ ํธ์ถ ์์๋ ์์ ๋ฉ์๋์ ํธ๋์ญ์
์ค์ ์ ๋ฐ๋ผ ๋กค๋ฐฑ๋จ")
void testAnotherMethodWithNoRollbackCommit() {
assertThrows(RuntimeException.class, () -> serviceA.callAnotherMethodWithNoRollback(1L));
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Initial Name", updatedMember.getName());
}
}
ํธ์ถํ๋ ๋ฉ์๋(callAnotherMethodWithNoRollback)์๋ ์ผ๋ฐ์ ์ธ @Transactional์ด ์ ์ฉ๋์ด ์๊ณ ํธ์ถ๋๋ ๋ฉ์๋(callOwnMethodWithNoRollback)์๋ noRollbackFor๊ฐ ์ค์ ๋์ด ์๋ค. ์ฆ RuntimeException์ด ๋ฐ์ํด๋ ํธ๋์ญ์ ๋กค๋ฐฑ์ด ๋์ง ์๋๋ก ์ค์ ๋ ์ํ์ด๋ค.
๊ทธ๋ฌ๋ ์ฌ๊ธฐ์ ์ฃผ์ํ ์ ์ Spring์ ํธ๋์ญ์ ๊ด๋ฆฌ ๋ฐฉ์์ด ํ๋ก์ ๊ธฐ๋ฐ์ด๋ผ๋ ์ ์ด๋ค. ์คํ๋ง์ ํธ๋์ญ์ ์ฒ๋ฆฌ๋ฅผ ์ํด AOP๋ฐฉ์์ ์ฌ์ฉํ๋ฉฐ, ํธ๋์ญ์ ์ด ์ ์ฉ๋ ๋ฉ์๋๋ฅผ ํธ์ถํ ๋ ํ๋ก์ ๊ฐ์ฒด๊ฐ ํธ๋์ญ์ ๊ฒฝ๊ณ๋ฅผ ๊ด๋ฆฌํ๋ค. ํ์ง๋ง ๋์ผ ํด๋์ค ๋ด์์ ํธ์ถ๋ ๋ฉ์๋๋ ํ๋ก์๋ฅผ ๊ฑฐ์น์ง ์๊ณ ์ค์ ๊ฐ์ฒด์ ๋ฉ์๋๋ฅผ ์ง์ ํธ์ถํ๊ฒ ๋๋ค. ์ด๋ก ์ธํด callOwnMethodWithNoRollback์์ ์ค์ ๋ @Transactional ๋ฐ noRollbackFor๋ ๋ฌด์๋๋ค. ๊ทธ๋ ๊ธฐ์ ์์ธ๊ฐ ๋ฐ์ํด๋ ์ธ๋ถ ๋ฉ์๋(callAnotherMethodWithNoRollback)์ @Transactional ์ค์ ๋ง ์ ์ฉ๋์ด ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ค.
7. ๋์ผ ํด๋์ค ๋ด์์ REQUIRES_NEW ์ค์ ๋ ๋ฉ์๋ ํธ์ถ ํ ์์ธ ๋ฐ์
@Service
public class ServiceA {
@Transactional
public void callMethodWithTransaction(Long memberId) {
callRequiresNewInSameClassMethod(memberId);
throw new RuntimeException("Intentional Exception with REQUIRES_NEW");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void callRequiresNewInSameClassMethod(Long memberId) {
memberRepository.updateNameById(memberId, "Updated Name A4");
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA: ๋์ผ ํด๋์ค ๋ด์์ REQUIRES_NEW ์ค์ ๋ ๋ฉ์๋ ํธ์ถ ํ ์์ธ ๋ฐ์ ์ ๋กค๋ฐฑ๋จ")
void testRequiresNewInSameClassMethod() {
assertThrows(RuntimeException.class, () -> serviceA.callMethodWithTransaction(1L));
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Initial Name", updatedMember.getName());
}
}
REQUIRES_NEW ์ ํ ์์ฑ์ ์ฌ์ฉํ๋ฉด ๊ธฐ์กด์ ํธ๋์ญ์ ๊ณผ ๊ด๊ณ์์ด ์๋ก์ด ํธ๋์ญ์ ์ ์์ฑํ์ฌ ์คํ๋๋๋ก ํ๋ค. ํ์ง๋ง ์ด์ ์ ๋งํ ๊ฒ๊ณผ ๊ฐ์ด ์คํ๋ง์ AOP๋ฅผ ์ฌ์ฉํ์ฌ ํธ๋์ญ์ ๊ฒฝ๊ณ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์, ๊ฐ์ ํด๋์ค ๋ด์์ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด ํ๋ก์๊ฐ ์ ์ฉ๋์ง ์์ ์ ํ ์์ฑ์ด ์ ๋๋ก ์ ์ฉ๋์ง ์๋๋ค.
๋ฐ๋ผ์ callRequiresNewInSameClassMethod ๋ฉ์๋์์ REQUIRES_NEW๋ฅผ ์ค์ ํ์์์๋ ๋ถ๊ตฌํ๊ณ , ๋ด๋ถ์์ callRequiresNewInSameClassMethod๋ฅผ ํธ์ถํ ๋ ํ๋ก์๊ฐ ์ ์ฉ๋์ง ์์ ์๋ก์ด ํธ๋์ญ์ ์ด ์์ฑ๋์ง ์๊ณ ๊ธฐ์กด ํธ๋์ญ์ ์ด ๊ทธ๋๋ก ์ ์ง๋๋ฉฐ RuntimeException์ด ๋ฐ์ํ๋ฉด ๊ธฐ์กด ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ค.
8. noRollbackFor์ค์ ํ try-catch๋ก ์์ธ ์ฒ๋ฆฌ
@Service
public class ServiceA {
@Transactional(noRollbackFor = RuntimeException.class)
public void callMethodWithNoRollbackAndTryCatch(Long memberId) {
try {
memberRepository.updateNameById(memberId, "Updated Name A5");
throw new RuntimeException("Intentional Exception");
} catch (RuntimeException e) {
// Exception handled
}
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA: noRollbackFor ์ค์ ํ try-catch๋ก ์์ธ ์ฒ๋ฆฌ ํ ์ปค๋ฐ")
void testNoRollbackForWithTryCatch() {
serviceA.callMethodWithNoRollbackAndTryCatch(1L);
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Updated Name A5", updatedMember.getName());
}
}
์ด ๊ฒฝ์ฐ๋ ๋ ๊ฐ์ง ๋ฉ์ปค๋์ฆ์ด ๋์์ ์ ์ฉ๋๋ค.
- noRollbackFor ์ค์ ์ผ๋ก RuntimeException์ ๋ํ ๋กค๋ฐฑ ๋ฐฉ์ง
- try-catch๋ก ์ธํ ์์ธ์ ๋ฉ์๋ ๋ฐ ์ ํ ๋ฐฉ์ง
๋ ์ค ์ด๋ ํ๋๋ง ์ ์ฉ๋์ด๋ ํธ๋์ญ์ ์ด ์ปค๋ฐ๋๋๋ฐ, ๋ ๊ฐ์ง๊ฐ ๋ชจ๋ ์ ์ฉ๋์ด ์์ผ๋ฏ๋ก ๋น์ฐํ ๋ณ๊ฒฝ์ฌํญ์ด ์ ์์ ์ผ๋ก ์ปค๋ฐ๋๋ค.
๋ค๋ฅธ ํด๋์ค ๊ฐ์ ํธ๋์ญ์ ๋์
1. ServiceA(์๋ฌ) -> ServiceB์ REQUIRES_NEW ๋ฉ์๋ ํธ์ถ
@Service
public class ServiceA {
@Transactional
public void callRequiresNewInOtherMethod(Long memberId) {
serviceB.updateNameInNewTransaction(memberId, "Updated Name B1");
memberRepository.updateNameById(memberId, "Updated Name A1");
throw new RuntimeException("Intentional Exception");
}
}
@Service
public class ServiceB {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateNameInNewTransaction(Long memberId, String newName) {
memberRepository.updateNameById(memberId, newName);
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA(์๋ฌ) -> ServiceB์ REQUIRES_NEW ๋ฉ์๋ ํธ์ถ ์ ๋ถ๋ชจ ํธ๋์ญ์
๋กค๋ฐฑ, ์์์ ์ปค๋ฐ")
void testRequiresNewMethodWithRollback() {
assertThrows(RuntimeException.class, () -> serviceA.callRequiresNewInOtherMethod(1L));
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Updated Name B1", updatedMember.getName());
}
}
ServiceB์ ๋ฉ์๋์ REQUIRES_NEW๋ฅผ ์ฌ์ฉํ์ผ๋ฏ๋ก, ServiceA์ ํธ๋์ญ์ ๊ณผ๋ ๋ ๋ฆฝ์ ์ธ ์๋ก์ด ํธ๋์ญ์ ์ด ์์ฑ๋๋ค.
- ServiceB์ ํธ๋์ญ์ ์ด ๋จผ์ ์คํ๋์ด "Updated Name B1"๋ก ์ ๋ฐ์ดํธ ๋ฐ ์ปค๋ฐ๋๋ค.
- ์ดํ ServiceA์์๋ "Updated Name A1"์ผ๋ก ์ ๋ฐ์ดํธํ์ง๋ง RuntimeException์ด ๋ฐ์ํ์ฌ ServiceA์ ํธ๋์ญ์ ์ ๋กค๋ฐฑ๋๋ค.
- ํ์ง๋ง ServiceB์ ํธ๋์ญ์ ์ ์ด๋ฏธ ์ปค๋ฐ๋์๊ณ ๋ ๋ฆฝ์ ์ด๋ฏ๋ก, "Updated Name B1"์ด ์ ์ง๋๋ค.
REQUIRES_NEW๋ ํญ์ ์๋ก์ด ํธ๋์ญ์ ์ ์์ฑํ๊ณ , ์ด ํธ๋์ญ์ ์ ํธ์ถํ ๊ณณ์ ํธ๋์ญ์ ๊ณผ ์์ ํ ๋ ๋ฆฝ์ ์ผ๋ก ๋์ํ๋ค. ๋ฐ๋ผ์ ๋ถ๋ชจ ํธ๋์ญ์ (ServiceA)์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋๋ผ๋ ์์ ํธ๋์ญ์ (ServiceB)์ ๋ณ๊ฒฝ์ฌํญ์ ์ํฅ์ ๋ฐ์ง ์๊ณ ์ ์ง๋๋ค.
2. ServiceA try-catch ์์ธ์ฒ๋ฆฌ -> ServiceB(์๋ฌ) ๋ฉ์๋ ํธ์ถ
@Service
public class ServiceA {
@Transactional
public void callMethodInServiceBWithTryCatch(Long memberId, String newName) {
try {
serviceB.updateNameWithException(memberId, newName);
} catch (RuntimeException e) {
// Exception handled
}
}
}
@Service
public class ServiceB {
@Transactional
public void updateNameWithException(Long memberId, String newName) {
memberRepository.updateNameById(memberId, newName);
throw new RuntimeException("Intentional Exception");
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA try-catch ์์ธ์ฒ๋ฆฌ -> ServiceB(์๋ฌ) ๋ฉ์๋ ํธ์ถ ์ ๋กค๋ฐฑ๋จ")
void testTryCatchCommitInServiceB() {
serviceA.callMethodInServiceBWithTryCatch(1L, "Updated Name B2");
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Updated Name B2", updatedMember.getName());
}
}
์ด๋ฒ ๊ฒ์๊ธ์์ ์ด๋ ค์ด ์ผ์ด์ค ์ค ์ฒซ๋ฒ์งธ์ด๋ค. try-catch๋ก ์์ธ๋ฅผ ์ฒ๋ฆฌํ์์๋ ๋ถ๊ตฌํ๊ณ ์ rollback-only ๋ฐ์ํ๊ณ , ๊ฒฐ๊ตญ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋์ง ์ดํดํ๊ธฐ ์ฝ์ง ์์๋ค. ํธ๋์ญ์ ์ด ์ด๋ป๊ฒ ์ฒ๋ฆฌ๋๋์ง ๋จ๊ณ๋ณ๋ก ์ดํด๋ณด์.
1. ServiceA์ ํธ๋์ญ์ ์์
- @Transactional์ด ์ ์ฉ๋ ServiceA์ ๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด์ ์๋ก์ด ํธ๋์ญ์ ์ด ์์ฑ
2. ServiceB์ ๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด์ ํธ๋์ญ์ ์ ์ฐธ์ฌ
- ServiceB์ @Transactional์ ๊ธฐ๋ณธ๊ฐ์ธ REQUIRED๋ฅผ ์ฌ์ฉํ์ฌ ์๋ก์ด ํธ๋์ญ์ ์ ์์ฑํ์ง ์๊ณ , ์ด๋ฏธ ์กด์ฌํ๋ ServiceA์ ํธ๋์ญ์ ์ ์ฐธ์ฌ
3. ServiceB์์ RuntimeException ๋ฐ์
- ์ด๋ ์คํ๋ง์ ๋ด๋ถ์ ์ผ๋ก TransactionAspectSupport.completeTransactionAfterThrowing() ์คํ
- ์ฌ๊ธฐ์ ํด๋น ํธ๋์ญ์ ์ "rollback-only"๋ก ๋งํนํ๊ณ ๋กค๋ฐฑ์ด ๋ฐ์
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo.transactionAttribute.rollbackOn(ex)) {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
}
4. ServiceA์์ ์์ธ๋ฅผ catch๋ก ์ก์
- ์์ธ๋ ์ ์์ ์ผ๋ก catch ๋์ด ์ฒ๋ฆฌ๋์ง๋ง ์ด๋ฏธ ํธ๋์ญ์ ์ "rollback-only"๋ก ๋งํน๋ ์ํ
5. ํธ๋์ญ์ ์ปค๋ฐ ์๋
- ServiceA์ ๋ฉ์๋๊ฐ ์ข ๋ฃ๋๋ฉด์ ํธ๋์ญ์ ์ปค๋ฐ์ด ์๋๋์ง๋ง, "rollback-only" ์ํ๋ก ๋งํน๋ ํธ๋์ญ์ ์ ์ปค๋ฐ์ด ๋ถ๊ฐ๋ฅ
- ๊ฒฐ๊ณผ์ ์ผ๋ก UnexpectedRollbackException์ด ๋ฐ์ํ๊ณ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋จ
์ ๋ฆฌํ๋ฉด ServiceB์์ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ํธ๋์ญ์ ์ TransactionAspectSupport.completeTransactionAfterThrowing()์ ์ํด "rollback-only" ์ํ๋ก ๋งํน๋๋ค. ์ด ์ํ์์ ServiceA์์ ์์ธ๋ฅผ try-catch๋ก ์ฒ๋ฆฌํ๋๋ผ๋ ์ด๋ฏธ ๋กค๋ฐฑ ์ํ๋ก ๋งํน๋ ํธ๋์ญ์ ์ ์ปค๋ฐ๋ ์ ์๊ณ , ๊ฒฐ๊ตญ UnexpectedRollbackException์ด ๋ฐ์ํ๋ฉฐ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ค. ๋ ์์ธํ ๋ด์ฉ์ ์ฐ์ํํ์ ๋ค ๊ธฐ์ ๋ธ๋ก๊ทธ์์ ๋ค๋ฃฌ ๋ด์ฉ์ ์ฐธ๊ณ ํ๋ฉด ๋์์ด ๋ ๊ฒ ๊ฐ๋ค.
3. ServiceA -> ServiceB(์๋ฌ)์ noRollbackFor ์ค์ ๋ฉ์๋ ํธ์ถ
@Service
public class ServiceA {
@Transactional
public void callMethodInServiceBWithNoRollback(Long memberId, String newName) {
serviceB.updateNameWithNoRollback(memberId, newName);
}
}
@Service
public class ServiceB {
@Transactional(noRollbackFor = RuntimeException.class)
public void updateNameWithNoRollback(Long memberId, String newName) {
memberRepository.updateNameById(memberId, newName);
throw new RuntimeException("Intentional Exception");
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA -> ServiceB(์๋ฌ)์ noRollbackFor ์ค์ ๋ฉ์๋ ํธ์ถ ์ ๋กค๋ฐฑ")
void testNoRollbackForCommitInServiceB() {
assertThrows(RuntimeException.class, () -> serviceA.callMethodInServiceBWithNoRollback(1L, "Updated Name B3"));
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Initial Name", updatedMember.getName());
}
}
์ด๋ฒ ๊ฒ์๊ธ์์ ์ด๋ ค์ด ์ผ์ด์ค ์ค ๋ ๋ฒ์งธ์ด๋ค. ์ด ์ผ์ด์ค๋ ์ฒ์์ ServiceB์ ๋ฉ์๋์ noRollbackFor๋ฅผ ์ค์ ํ์ผ๋ ๋กค๋ฐฑ์ด ์ผ์ด๋์ง ์์ ๊ฒ์ด๋ผ ์๊ฐํ์ผ๋ ์ค์ ๋ก๋ ๋กค๋ฐฑ์ด ๋ฐ์ํ์ฌ "Initial Name"์ด ์ ์ง๋์๋ค.
์ด์ ๊ฐ์ด noRollbackFor ์ค์ ์ด ์์๋๋ก ๋์ํ์ง ์๋ ์ด์ ๋ noRollbackFor ์ค์ ์ ํด๋น ๋ฉ์๋๋ ํด๋์ค์์ ์ง์ ๋ฐ์ํ ์์ธ์ ๋ํด์๋ง ์ ์ฉ๋๊ธฐ ๋๋ฌธ์ด๋ค. ServiceA์์ ํธ์ถ๋๋ ServiceB์ ๋ฉ์๋๋ noRollbackFor๋ก ์ค์ ํ์ง๋ง ์ค์ ์ ServiceB์ ํธ๋์ญ์ ๋ด์์๋ง ์ ์ฉ๋๊ณ , ServiceA์ ํธ๋์ญ์ ์์๋ ์ฌ์ ํ RuntimeException์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ๋กค๋ฐฑ์ด ๋ฐ์ํ ์ ์๋ ์ํฉ์ด๋ค. ๋ฐ๋ผ์ ServiceB์์ RuntimeException์ด ๋ฐ์ํ ํ ๋ถ๋ชจ ํธ๋์ญ์ ์ธ ServiceA์์ ๋กค๋ฐฑ์ด ์ผ์ด๋๊ฒ ๋๋ค.
4. ServiceA(noRollbackFor) -> ServiceB(์๋ฌ) ๋ฉ์๋ ํธ์ถ
๊ทธ๋ ๋ค๋ฉด ์ด๋ฒ์๋ ๋ฐ๋๋ก ํธ์ถํ๋ ์ชฝ์์ noRollbackFor๊ฐ ๊ฑธ๋ ค์๋ค๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
@Service
public class ServiceA {
@Transactional(noRollbackFor = RuntimeException.class)
public void callNoRollbackInServiceAWithExceptionInServiceB(Long memberId, String newName) {
serviceB.updateNameWithException(memberId, newName);
}
}
@Service
public class ServiceB {
@Transactional
public void updateNameWithException(Long memberId, String newName) {
memberRepository.updateNameById(memberId, newName);
throw new RuntimeException("Intentional Exception in ServiceB");
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA(noRollbackFor) -> ServiceB(์๋ฌ) ๋ฉ์๋ ํธ์ถ ์ ๋กค๋ฐฑ")
void testNoRollbackInServiceAWithExceptionInServiceB() {
assertThrows(RuntimeException.class, () ->
serviceA.callNoRollbackInServiceAWithExceptionInServiceB(1L, "Updated Name A6"));
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Initial Name", updatedMember.getName());
}
}
3๋ฒ์์ ๋ดค๋ฏ์ด noRollbackFor ์ค์ ์ ํด๋น ๋ฉ์๋๋ ํด๋์ค์์ ์ง์ ๋ฐ์ํ ์์ธ์ ๋ํด์๋ง ์ ์ฉ๋๊ธฐ ๋๋ฌธ์ ServiceB์์ ๋ฐ์ํ ์์ธ์ ๋ํด์๋ ServiceA์ noRollbackFor ์ค์ ์ด ์ํฅ์ ๋ฏธ์น์ง ์์ ๋กค๋ฐฑ์ด ์ผ์ด๋๋ค.
5. ServiceA try-catch ์์ธ์ฒ๋ฆฌ -> ServiceB(์๋ฌ)์ noRollbackFor ์ค์ ๋ฉ์๋ ํธ์ถ
@Service
public class ServiceA {
@Transactional
public void callMethodInServiceBWithNoRollbackAndTryCatch(Long memberId, String newName) {
try {
serviceB.updateNameWithNoRollback(memberId, newName);
} catch (RuntimeException e) {
// Exception handled
}
}
}
@Service
public class ServiceB {
@Transactional(noRollbackFor = RuntimeException.class)
public void updateNameWithNoRollback(Long memberId, String newName) {
memberRepository.updateNameById(memberId, newName);
throw new RuntimeException("Intentional Exception");
}
}
@SpringBootTest
class TransactionTestApplicationTests {
@Test
@DisplayName("ServiceA try-catch ์์ธ์ฒ๋ฆฌ -> ServiceB(์๋ฌ)์ noRollbackFor ์ค์ ๋ฉ์
๋ ํธ์ถ ์ ์ปค๋ฐ")
void testNoRollbackForWithTryCatchInServiceB() {
serviceA.callMethodInServiceBWithNoRollbackAndTryCatch(1L, "Updated Name B6");
Member updatedMember = memberRepository.findById(1L).orElseThrow();
assertEquals("Updated Name B6", updatedMember.getName());
}
}
๋ด ๊ธฐ์ค์์๋ ์ฒ์์ ๊ฐ์ฅ ์ดํดํ๊ธฐ ์ด๋ ค์ด ์ผ์ด์ค์๋ค. 2๋ฒ ์ผ์ด์ค์์๋ RuntimeException ๋ฐ์ ์ ํธ๋์ญ์ ์ด ์ฆ์ "rollback-only"๋ก ๋งํน๋์๊ธฐ ๋๋ฌธ์ ServiceA์ try-catch๋ก ์์ธ๋ฅผ ์ก๋๋ผ๋ ์ด๋ฏธ ๋กค๋ฐฑ์ด ์์ฝ๋ ์ํ๋ผ ๊ฒฐ๊ตญ ๋กค๋ฐฑ๋์๋ค. ๊ทธ๋ฆฌ๊ณ 3๋ฒ ์ผ์ด์ค์์๋ ServiceB์ noRollbackFor ์ค์ ์ด ์์์ง๋ง, ์์ธ๊ฐ ServiceA๊น์ง ์ ํ๋์ด ๊ฒฐ๊ตญ ๋กค๋ฐฑ๋์๋ค.
๊ทธ๋ฆฌ๊ณ ์ด๋ฒ ์ผ์ด์ค๋ 2๋ฒ๊ณผ 3๋ฒ ์ผ์ด์ค๋ฅผ ์ ๋ถ ๋์์ ๋ง์กฑํ๋ ์ํฉ์ด๋ผ ๋น์ฐํ ๋กค๋ฐฑ์ด ์ผ์ด๋ ๊ฒ์ผ๋ก ์์ํ์ผ๋ ์ค์ ๋ก๋ ์ปค๋ฐ์ด ์ด๋ฃจ์ด์ก๋ค. ๊ทธ ์ด์ ๋ ๋ค์๊ณผ ๊ฐ๋ค.
- ServiceB์ noRollbackFor ์ค์ ์ผ๋ก RuntimeException ๋ฐ์ ์ "rollback-only" ๋งํน์ด ๋์ง ์์
- ServiceA์ try-catch๋ก ์์ธ๊ฐ ์์๋ก ์ ํ๋๋ ๊ฒ์ ๋ฐฉ์ง
์ฆ ServiceB์์ ์์ธ๊ฐ ๋ฐ์ํด๋ noRollbackFor ์ค์ ์ผ๋ก ์ธํด ๋กค๋ฐฑ ๋งํน์ด ๋์ง ์๊ณ , ๋กค๋ฐฑ ๋งํน์ด ์๋ ์ํ์์ ServiceA์ try-catch๊ฐ ์์ธ๋ฅผ ์ ์์ ์ผ๋ก ์ฒ๋ฆฌํ์ฌ ํธ๋์ญ์ ์ด ์ ์์ ์ผ๋ก ์ปค๋ฐ๋๊ฒ ๋๋ค.
์ ๋ฆฌ
1ํธ์์๋ Spring์ @Transactional ์ด๋ ธํ ์ด์ ์ ๊ธฐ๋ณธ์ ์ธ ๋์๊ณผ ์์ธ ์ฒ๋ฆฌ์ ๋ฐ๋ฅธ ๋กค๋ฐฑ ๋ฉ์ปค๋์ฆ์ ๋ํด ์ดํด๋ณด์๊ณ , 2ํธ์์๋ ์ค์ ํ ์คํธ ์ฝ๋๋ฅผ ํตํด ๋ค์ํ ์ํฉ์์์ ํธ๋์ญ์ ๋์์ ์์๋ณด์๋ค. ๋์ผ ํด๋์ค ๋ด ํธ๋์ญ์ ๋์์ ๋ํด์๋ ์ด๋ ์ ๋ ์์ํ ๋๋ก ๋์ํ์ง๋ง, ๋ค๋ฅธ ํด๋์ค ๊ฐ ํธ๋์ญ์ ๋์์์๋ try-catch์ noRollbackFor ์ค์ ์ ์กฐํฉ์ ๋ฐ๋ผ ํธ๋์ญ์ ์ ๋กค๋ฐฑ ์ฌ๋ถ๊ฐ ๊ฒฐ์ ๋๋ ์์์น ๋ชปํ ๊ฒฝ์ฐ๋ค์ด ๋ง์ด ๋ฐ์ํ๋ค.
๋ง์ง๋ง์ผ๋ก.. ์ด๋ฐ ์ด๋ก ์ ์์๋๋ ๊ฒ๋ ๋ฌผ๋ก ์ข์ง๋ง ๋ณธ์ง์ ์ผ๋ก ํธ๋์ญ์ ๋ด์์ RuntimeException์ catch ํ๋ ค ํ๋ ์๋ ์์ฒด๊ฐ ์ฌ๋ฐ๋ฅธ ๊ฒ์ด์์๊น? ์๊ฐํด ๋ณด๋ ๊ฒ๋ ์ข์ ๊ฒ ๊ฐ๋ค.
'BackEnd๐ฑ > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Spring ํธ๋์ญ์ ์ ์ธ์ ์ด๋ป๊ฒ ๋กค๋ฐฑ ๋ ๊น? -1ํธ (2) | 2024.10.04 |
---|---|
[gradle] implementation, api ์ฐจ์ด (0) | 2024.09.07 |
[JPA] deleteAll(), deleteAllInBatch(), deleteInBatch() ์ ๋ฆฌ (0) | 2024.08.12 |
๋จ๋ฐฉํฅ @OneToMany์ ๋ฌธ์ ์ (2) | 2024.07.26 |
@TransactionalEventListener ์ฌ์ฉ ์ ์ฃผ์์ (0) | 2024.07.03 |
ํ์ ์ ์ํด Swagger ์ข ๋ ์ ์ฌ์ฉํด๋ณด๊ธฐ (0) | 2024.05.15 |
๋๊ธ