๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BackEnd๐ŸŒฑ/Spring

Spring ํŠธ๋žœ์žญ์…˜์€ ์–ธ์ œ ์–ด๋–ป๊ฒŒ ๋กค๋ฐฑ ๋ ๊นŒ? -2ํŽธ

by dkswnkk 2024. 12. 22.

๊ฐœ์š”

์ด์ „ ๊ธ€์—์„œ๋Š” 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());
    }
}

์ด ๊ฒฝ์šฐ๋Š” ๋‘ ๊ฐ€์ง€ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ๋™์‹œ์— ์ ์šฉ๋œ๋‹ค.

  1. noRollbackFor ์„ค์ •์œผ๋กœ RuntimeException์— ๋Œ€ํ•œ ๋กค๋ฐฑ ๋ฐฉ์ง€
  2. 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์˜ ํŠธ๋žœ์žญ์…˜๊ณผ๋Š” ๋…๋ฆฝ์ ์ธ ์ƒˆ๋กœ์šด ํŠธ๋žœ์žญ์…˜์ด ์ƒ์„ฑ๋œ๋‹ค.

  1. ServiceB์˜ ํŠธ๋žœ์žญ์…˜์ด ๋จผ์ € ์‹คํ–‰๋˜์–ด "Updated Name B1"๋กœ ์—…๋ฐ์ดํŠธ ๋ฐ ์ปค๋ฐ‹๋œ๋‹ค.
  2. ์ดํ›„ ServiceA์—์„œ๋„ "Updated Name A1"์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜์ง€๋งŒ RuntimeException์ด ๋ฐœ์ƒํ•˜์—ฌ ServiceA์˜ ํŠธ๋žœ์žญ์…˜์€ ๋กค๋ฐฑ๋œ๋‹ค.
  3. ํ•˜์ง€๋งŒ 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๋ฒˆ ์ผ€์ด์Šค๋ฅผ ์ „๋ถ€ ๋™์‹œ์— ๋งŒ์กฑํ•˜๋Š” ์ƒํ™ฉ์ด๋ผ ๋‹น์—ฐํžˆ ๋กค๋ฐฑ์ด ์ผ์–ด๋‚  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒํ–ˆ์œผ๋‚˜ ์‹ค์ œ๋กœ๋Š” ์ปค๋ฐ‹์ด ์ด๋ฃจ์–ด์กŒ๋‹ค. ๊ทธ ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. ServiceB์˜ noRollbackFor ์„ค์ •์œผ๋กœ RuntimeException ๋ฐœ์ƒ ์‹œ "rollback-only" ๋งˆํ‚น์ด ๋˜์ง€ ์•Š์Œ
  2. ServiceA์˜ try-catch๋กœ ์˜ˆ์™ธ๊ฐ€ ์ƒ์œ„๋กœ ์ „ํŒŒ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€

์ฆ‰ ServiceB์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ noRollbackFor ์„ค์ •์œผ๋กœ ์ธํ•ด ๋กค๋ฐฑ ๋งˆํ‚น์ด ๋˜์ง€ ์•Š๊ณ , ๋กค๋ฐฑ ๋งˆํ‚น์ด ์—†๋Š” ์ƒํƒœ์—์„œ ServiceA์˜ try-catch๊ฐ€ ์˜ˆ์™ธ๋ฅผ ์ •์ƒ์ ์œผ๋กœ  ์ฒ˜๋ฆฌํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜์ด ์ •์ƒ์ ์œผ๋กœ ์ปค๋ฐ‹๋˜๊ฒŒ ๋œ๋‹ค.

 

 

์ •๋ฆฌ

1ํŽธ์—์„œ๋Š” Spring์˜ @Transactional ์–ด๋…ธํ…Œ์ด์…˜์˜ ๊ธฐ๋ณธ์ ์ธ ๋™์ž‘๊ณผ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์— ๋”ฐ๋ฅธ ๋กค๋ฐฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜์— ๋Œ€ํ•ด ์‚ดํŽด๋ณด์•˜๊ณ , 2ํŽธ์—์„œ๋Š” ์‹ค์ œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ๋‹ค์–‘ํ•œ ์ƒํ™ฉ์—์„œ์˜ ํŠธ๋žœ์žญ์…˜ ๋™์ž‘์„ ์•Œ์•„๋ณด์•˜๋‹ค. ๋™์ผ ํด๋ž˜์Šค ๋‚ด ํŠธ๋žœ์žญ์…˜ ๋™์ž‘์— ๋Œ€ํ•ด์„œ๋Š” ์–ด๋Š ์ •๋„ ์˜ˆ์ƒํ•œ ๋Œ€๋กœ ๋™์ž‘ํ–ˆ์ง€๋งŒ, ๋‹ค๋ฅธ ํด๋ž˜์Šค ๊ฐ„ ํŠธ๋žœ์žญ์…˜ ๋™์ž‘์—์„œ๋Š” try-catch์™€ noRollbackFor ์„ค์ •์˜ ์กฐํ•ฉ์— ๋”ฐ๋ผ ํŠธ๋žœ์žญ์…˜์˜ ๋กค๋ฐฑ ์—ฌ๋ถ€๊ฐ€ ๊ฒฐ์ •๋˜๋Š” ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒฝ์šฐ๋“ค์ด ๋งŽ์ด ๋ฐœ์ƒํ–ˆ๋‹ค.

๋งˆ์ง€๋ง‰์œผ๋กœ.. ์ด๋Ÿฐ ์ด๋ก ์„ ์•Œ์•„๋‘๋Š” ๊ฒƒ๋„ ๋ฌผ๋ก  ์ข‹์ง€๋งŒ ๋ณธ์งˆ์ ์œผ๋กœ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ RuntimeException์„ catch ํ•˜๋ ค ํ–ˆ๋˜ ์‹œ๋„ ์ž์ฒด๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฒƒ์ด์—ˆ์„๊นŒ? ์ƒ๊ฐํ•ด ๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

๋Œ“๊ธ€