Transaction Propagation


Transaction Propagation, νŠΈλžœμž­μ…˜ μ „νŒŒλŠ” νŠΈλžœμž­μ…˜μ˜ κ²½κ³„μ—μ„œ 이미 진행쀑인 νŠΈλžœμž­μ…˜μ΄ μžˆμ„ λ•Œ λ˜λŠ” 없을 λ•Œ μ–΄λ–»κ²Œ λ™μž‘ν•  것인가λ₯Ό κ²°μ •ν•˜λŠ” 방식을 μ˜λ―Έν•œλ‹€.

@Transactional 의 propagation 속성을 톡해 ν”Όν˜ΈμΆœ νŠΈλžœμž­μ…˜ μž…μž₯μ—μ„œ ν˜ΈμΆœν•œ μͺ½μ˜ νŠΈλžœμž­μ…˜μ„ κ·ΈλŒ€λ‘œ μ‚¬μš©ν•  μˆ˜λ„ 있고, μƒˆλ‘­κ²Œ νŠΈλžœμž­μ…˜μ„ 생성할 수 도 μžˆλ‹€.

Propagation μ’…λ₯˜

Transaction Propagation μ—λŠ” 총 7가지 μ „νŒŒ 속성이 μ‘΄μž¬ν•œλ‹€.

  • REQUIRED
  • SUPPORTS
  • MANDATORY
  • REQUIRES_NEW
  • NOT_SUPPORTED
  • NEVER
  • NESTED

Physical Transaction & Logical Transaction


Physical Transaction

  • 물리적 νŠΈλžœμž­μ…˜μ€ μ‹€μ œ νŠΈλžœμž­μ…˜μ΄ μ‹€ν–‰λ˜λŠ” μŠ€λ ˆλ“œλ₯Ό μ˜λ―Έν•œλ‹€.

Logical Transaction

  • 논리적 νŠΈλžœμž­μ…˜μ€ 선언적 νŠΈλžœμž­μ…˜μ΄ 적용된 λͺ¨λ“  λ©”μ„œλ“œλ₯Ό μ˜λ―Έν•œλ‹€.
    • 즉, μ™ΈλΆ€ νŠΈλžœμž­μ…˜κ³Ό λ‚΄λΆ€ νŠΈλžœμž­μ…˜μ˜ 논리적 λ²”μœ„λŠ” 독립적이닀.
  • λ‘€λ°± μƒνƒœλ₯Ό κ°œλ³„μ μœΌλ‘œ κ²°μ •ν•  수 있고 μ΄λŠ” 물리적 νŠΈλžœμž­μ…˜μ— λ§€ν•‘λœλ‹€.
    • λ”°λΌμ„œ, λ‚΄λΆ€ νŠΈλžœμž­μ…˜ μ„€μ •μœΌλ‘œ 인해 μ™ΈλΆ€ νŠΈλžœμž­μ…˜μ˜ 컀밋에 영ν–₯을 λ―ΈμΉ  수 μžˆλ‹€.
  • λ‚΄λΆ€ νŠΈλžœμž­μ…˜μœΌλ‘œ μΈν•œ 둀백은 μ™ΈλΆ€ νŠΈλžœμž­μ…˜ μž…μž₯에선 μ˜ˆμƒν•  수 μ—†λŠ” 둀백이닀.
    • UnexpectedRollbackException 을 λ°œμƒμ‹œν‚΄μœΌλ‘œμ¨ μ™ΈλΆ€ νŠΈλžœμž­μ…˜μ΄ κ·Έ 사싀을 μ•Œ 수 μžˆλ‹€.

REQUIRED (DEFAULT)


@Nested
class testRequired {
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” Required")
    void withParent() {
        final var actual = parentService.required(ChildService::required);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(1);
    }
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” Required")
    void withoutParent() {
        final var actual = parentService.nonTransactional(ChildService::required);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(1);
    }
}
  • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλ‹€λ©΄ λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ— ν•©λ₯˜ν•œλ‹€.
  • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λ‹€λ©΄ μƒˆλ‘œμš΄ νŠΈλžœμž­μ…˜μ„ μƒμ„±ν•œλ‹€.

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” 경우

ParentService   : Parent Transaction Active : 🟒
ChildService    :  Child Transaction Active : 🟒
PropagationTest : transactions : [ParentService]

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” 경우

ParentService   : Parent Transaction Active : 🚫
ChildService    :  Child Transaction Active : 🟒
PropagationTest : transactions : [ChildService]

SUPPORTS


@Nested
class testSupports {
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” Supports")
    void withParent() {
        final var actual = parentService.required(ChildService::supports);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(1);
    }
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” Supports")
    void withoutParent() {
        final var actual = parentService.nonTransactional(ChildService::supports);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(1);
    }
}
  • νŠΈλžœμž­μ…˜μ΄ ν•„μš”ν•œ μ²˜λ¦¬λŠ” μ—†μ§€λ§Œ μ‹€νŒ¨ν•˜λ©΄ λ‘€λ°±ν•΄μ•Ό ν•˜λŠ” 경우 μ‚¬μš©ν•˜λŠ” μ „νŒŒ 속성이닀.
    • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλ‹€λ©΄ λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ— ν•©λ₯˜ν•œλ‹€.
    • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λ‹€λ©΄ νŠΈλžœμž­μ…˜μ„ μƒμ„±ν•˜μ§€ μ•ŠλŠ”λ‹€.

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” 경우

ParentService   : Parent Transaction Active : 🟒
ChildService    :  Child Transaction Active : 🟒
PropagationTest : transactions : [ParentService]

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” 경우

ParentService   : Parent Transaction Active : 🚫
ChildService    :  Child Transaction Active : 🚫

MANDATORY


@Nested
class testMandatory {
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” Mandatory")
    void withParent() {
        final var actual = parentService.required(ChildService::mandatory);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(1);
    }
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” Mandatory")
    void withoutParent() {
        assertThatThrownBy(() -> parentService.nonTransactional(ChildService::mandatory))
                .isInstanceOf(IllegalTransactionStateException.class)
                .hasMessage("No existing transaction found for transaction marked with propagation 'mandatory'");
    }
}
  • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ— μ˜ν•΄ 항상 ν”Όν˜ΈμΆœλ˜μ–΄μ•Ό ν•˜λŠ” μ „νŒŒ 속성이닀.
    • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλ‹€λ©΄ λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ— ν•©λ₯˜ν•œλ‹€.
    • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λ‹€λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” 경우

ParentService   : Parent Transaction Active : 🟒
ChildService    :  Child Transaction Active : 🟒
PropagationTest : transactions : [ParentService]

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” 경우

No existing transaction found for transaction marked with propagation 'mandatory'
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

REQUIRES_NEW


@Nested
class testRequiredNew {
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” Requires_New")
    void withParent() {
        final var actual = parentService.required(ChildService::requiresNew);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(2);
    }
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” Requires_New")
    void withoutParent() {
        final var actual = parentService.nonTransactional(ChildService::requiresNew);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(1);
    }
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ˜ˆμ™Έλ₯Ό λ°œμƒν•˜λŠ” Requires_New")
    void withRollback() {
        assertThat(parentService.findAll()).hasSize(0);
 
        assertThatThrownBy(() -> parentService.exception(ChildService::requiresNew))
                .isInstanceOf(RuntimeException.class);
 
        assertThat(parentService.findAll()).hasSize(1);
    }
}
  • 무쑰건 μƒˆλ‘œμš΄ νŠΈλžœμž­μ…˜μ„ μƒμ„±ν•œλ‹€.
  • 항상 물리적 νŠΈλžœμž­μ…˜μ„ μƒˆλ‘œ μƒμ„±ν•˜κΈ° λ•Œλ¬Έμ— λ‚΄λΆ€ νŠΈλžœμž­μ…˜μ΄ μ™ΈλΆ€ νŠΈλžœμž­μ…˜μ— 영ν–₯을 주지 μ•ŠλŠ”λ‹€.

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” 경우

ParentService   : Parent Transaction Active : 🟒
ChildService    :  Child Transaction Active : 🟒
PropagationTest : transactions : [ParentService, ChildService]

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” 경우

ParentService   : Parent Transaction Active : 🚫
ChildService    :  Child Transaction Active : 🟒
PropagationTest : transactions : [ChildService]

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¬ 경우

ChildService    :  Child Transaction Active : 🟒
PropagationTest : transactions : [ChildService]

NOT_SUPPORTED


@Nested
class testNotSupported {
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” Not_Supported")
    void withParent() {
        final var actual = parentService.required(ChildService::notSupported);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(2);
    }
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” Not_Supported")
    void withoutParent() {
        final var actual = parentService.nonTransactional(ChildService::notSupported);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(1);
    }
}
  • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλ‹€λ©΄ ν•΄λ‹Ή νŠΈλžœμž­μ…˜μ„ 보λ₯˜μ‹œν‚€κ³  νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” μƒνƒœλ‘œ 처리λ₯Ό μˆ˜ν–‰ν•œλ‹€.
  • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λ‹€λ©΄ νŠΈλžœμž­μ…˜μ„ μƒμ„±ν•˜μ§€ μ•ŠλŠ”λ‹€.

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” 경우

ParentService   : Parent Transaction Active : 🟒
ChildService    :  Child Transaction Active : 🚫
PropagationTest : transactions : [ParentService]

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” 경우

ParentService   : Parent Transaction Active : 🚫
ChildService    :  Child Transaction Active : 🚫

NEVER


@Nested
class testNever {
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” Never")
    void withParent() {
        assertThatThrownBy(() -> parentService.required(ChildService::never))
                .isInstanceOf(IllegalTransactionStateException.class)
                .hasMessage("Existing transaction found for transaction marked with propagation 'never'");
    }
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” Never")
    void withoutParent() {
        final var actual = parentService.nonTransactional(ChildService::never);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(1);
    }
}
  • νŠΈλžœμž­μ…˜μ„ μ‚¬μš©ν•˜μ§€ μ•Šλ„λ‘ κ°•μ œν•˜λŠ” 속성이닀.
    • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλ‹€λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” 경우

Existing transaction found for transaction marked with propagation 'never'
org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” 경우

ParentService   : Parent Transaction Active : 🚫
ChildService    :  Child Transaction Active : 🚫

NESTED


@Nested
class testNested {
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” Nested")
    void withParent() {
        assertThatThrownBy(() -> parentService.required(ChildService::nested))
                .isInstanceOf(NestedTransactionNotSupportedException.class)
                .hasMessage("JpaDialect does not support savepoints - check your JPA provider's capabilities");
    }
 
    @Test
    @DisplayName("λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” Nested")
    void withoutParent() {
        final var actual = parentService.nonTransactional(ChildService::nested);
 
        log.info("transactions : {}", actual);
        assertThat(actual)
                .hasSize(1);
    }
}
  • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλ‹€λ©΄ JDBC Savepoint κΈ°λŠ₯을 μ΄μš©ν•΄ 내뢀에 쀑첩 νŠΈλžœμž­μ…˜μ„ μƒμ„±ν•œλ‹€.
    • 쀑첩 νŠΈλžœμž­μ…˜μ—μ„œ λ‘€λ°± λ°œμƒ μ‹œ, ν•΄λ‹Ή 쀑첩 νŠΈλžœμž­μ…˜ μ‹œμž‘ μ‹œμ κΉŒμ§€λ§Œ λ‘€λ°±λœλ‹€.
    • 쀑첩 νŠΈλžœμž­μ…˜μ€ λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ 컀밋될 λ•Œ 같이 μ»€λ°‹λœλ‹€.
  • 단일 물리적 νŠΈλžœμž­μ…˜μ— μ—¬λŸ¬ Savepoint λ₯Ό 두어 λ‚΄λΆ€ νŠΈλžœμž­μ…˜μ— 둀백이 μ‹€ν–‰λ˜μ–΄λ„ μ™ΈλΆ€ νŠΈλžœμž­μ…˜μ—μ„œλŠ” 물리적 νŠΈλžœμž­μ…˜μ„ 계속할 수 μžˆλ‹€.
  • λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λ‹€λ©΄ μƒˆλ‘œμš΄ νŠΈλžœμž­μ…˜μ„ μƒμƒν•œλ‹€.

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μžˆλŠ” 경우

JpaDialect does not support savepoints - check your JPA provider's capabilities
org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities
  • JPA 의 경우 Dirty Checking 을 ν†΅ν•œ μ“°κΈ° 지연 λ•Œλ¬Έμ— 쀑첩 νŠΈλžœμž­μ…˜μ„ 생성할 수 μ—†μ–΄ μ§€μ›ν•˜μ§€ μ•ŠλŠ”λ‹€.

λΆ€λͺ¨ νŠΈλžœμž­μ…˜μ΄ μ—†λŠ” 경우

ParentService   : Parent Transaction Active : 🚫
ChildService    :  Child Transaction Active : 🟒
PropagationTest : transactions : [ChildService]

References