DevLog ๐Ÿ˜ถ

[JPA] CascadeType.REMOVE vs orphanRemoval=true ์ฐจ์ด์  ์•Œ์•„๋ณด๊ธฐ - 1ํŽธ ๋ณธ๋ฌธ

Back-end/JPA

[JPA] CascadeType.REMOVE vs orphanRemoval=true ์ฐจ์ด์  ์•Œ์•„๋ณด๊ธฐ - 1ํŽธ

dolmeng2 2023. 6. 29. 23:58

๐ŸŒฑ ๋“ค์–ด๊ฐ€๊ธฐ ์ „

JPA๋ฅผ ๊ณต๋ถ€ํ•˜๋ฉด์„œ CascadeType.REMOVE์™€ orphanRemoval = true ์˜ต์…˜์— ๋Œ€ํ•ด์„œ ์–ด๋–ค ์ฐจ์ด๊ฐ€ ์žˆ๋Š”์ง€ ์ œ๋Œ€๋กœ ์ธ์ง€ํ•œ ์ ์ด ์—†๋Š” ๊ฒƒ ๊ฐ™์•„์„œ, ์ด๋ฒˆ์— ๊ณต๋ถ€ํ•  ๊ฒธ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด๋ณด๋ฉฐ ๋‘ ์˜ต์…˜์˜ ์ฐจ์ด๋ฅผ ๊ณต๋ถ€ํ•ด๋ณด์•˜๋‹ค.

๊ฒฐ๊ณผ์ ์œผ๋กœ ๋งํ•˜์ž๋ฉด Cascade ์˜ต์…˜์€ ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ์™€ ์ž์‹ ์—”ํ‹ฐํ‹ฐ์˜ ์˜์† ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด๊ณ , orphanRemoval์€ ์กฐ๊ธˆ ๋” ์„ธ๋ถ€์ ์œผ๋กœ ๊ณ ์•„ ๊ฐ์ฒด์— ๋Œ€ํ•œ ๊ด€๋ฆฌ๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. ์ง€๊ธˆ๋ถ€ํ„ฐ ์—ฌ๋Ÿฌ ์ผ€์ŠคํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ฉฐ ํ•˜๋‚˜์”ฉ ์•Œ์•„๋ณด์ž.

 


 

๐ŸŒฑ ์—”ํ‹ฐํ‹ฐ ์„ธํŒ…ํ•˜๊ธฐ

1:N ๊ด€๊ณ„๋ฅผ ๋‹ด๋‹นํ•ด์ค„ '์ฝ˜์„œํŠธ' ์—”ํ‹ฐํ‹ฐ์™€ '์ฝ˜์„œํŠธ ํ‹ฐ์ผ“' ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•˜์˜€๋‹ค.

๋‘˜ ์‚ฌ์ด์˜ ๊ด€๊ณ„๋Š” ์–‘๋ฐฉํ–ฅ์œผ๋กœ ์„ค์ •ํ•˜์˜€์œผ๋ฉฐ, ํ•˜๋‚˜์˜ ์ฝ˜์„œํŠธ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“์„ ๋ณด์œ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ•˜์˜€๋‹ค.

 

<Concert>

@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val name: String,

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList<ConcertTicket> = Collections.emptyList()
) {

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "concert")
    val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()

    fun addTicket(concertTicket: ConcertTicket) {
        concertTickets.add(concertTicket)
    }
}

 

<ConcertTicket>

@Entity
class ConcertTicket(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val userId: Long,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(
        name = "concert_id",
        foreignKey = ForeignKey(name = "fk_ticket_concert_id")
    )
    var concert: Concert
)

์ฝ˜์„œํŠธ ์—”ํ‹ฐํ‹ฐ๋Š” ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ•™์Šต์„ ์œ„ํ•ด ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด์„ ์„ค์ •ํ•˜์˜€๋‹ค.

 


 

๐ŸŒฑ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ธํŒ…ํ•˜๊ธฐ

<application-test.yml>

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb;MODE=MySQL

  jpa:
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        globally_quoted_identifiers: true
        format_sql: true
    show-sql: true

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ธํŒ…์„ ์œ„ํ•ด application-test.yml ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜์˜€๋‹ค.

์ด๋•Œ show-sql ์˜ต์…˜์„ ํ†ตํ•ด์„œ JPA์—์„œ ์–ด๋–ค ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”์ง€ ํ™•์ธํ•˜์ž.

 

@DataJpaTest
@ActiveProfiles("test")
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class ConcertTest(
    private val concertRepository: ConcertRepository,
    private val concertTicketRepository: ConcertTicketRepository
) {
}

ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉํ•œ ๋ผˆ๋Œ€ ์ฝ”๋“œ์ด๋‹ค.

@DataJpaTest๋ฅผ ํ†ตํ•ด์„œ JPA ํ™˜๊ฒฝ์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ค์—ˆ์œผ๋ฉฐ, ํ™œ์„ฑ ํ”„๋กœํŒŒ์ผ์€ test๋กœ ์ฃผ์–ด์„œ application-test.yml์˜ ์ •๋ณด๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค. ๋˜ํ•œ, ์ƒ์„ฑ์ž์—์„œ ๋ฐ”๋กœ ์ฃผ์ž…๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก autowireMode๋ฅผ TestConstructor.AutowireMode.ALL๋กœ ์„ค์ •ํ•˜์˜€๋‹ค.

 


 

๐ŸŒฑ @OneToMany์—์„œ CascadeType.REMOVE 

@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val name: String,

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList<ConcertTicket> = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        mappedBy = "concert",
        cascade = [CascadeType.REMOVE, CascadeType.PERSIST] // Here!
    )
    val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()

ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด์„œ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด์„œ CascadeType.REMOVE๋ฅผ ์„ค์ •ํ•ด์ฃผ์—ˆ๋‹ค.

์ด๋•Œ, ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ ์ €์žฅ ์‹œ ์ž์‹ ์—”ํ‹ฐํ‹ฐ๋„ ํ•จ๊ป˜ ์ €์žฅ๋˜๋„๋ก ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ CascadeType.PERSIST๋กœ ์„ค์ •ํ•ด์ฃผ์—ˆ๋‹ค.

 


 

๐Ÿ’ฌ ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐํ•˜๊ธฐ - ์ฝ˜์„œํŠธ ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐ

@Test
@DisplayName("@OneToMany: CascadeType.REMOVE ํ…Œ์ŠคํŠธ - ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐ")
fun oneToMany_cascadeType_REMOVE_test_๋ถ€๋ชจ_์—”ํ‹ฐํ‹ฐ_์ œ๊ฑฐ() {
    // given
    val ์ฝ˜์„œํŠธ = Concert(name = "์ธ๊ธฐ ๋งŽ์€ ์ฝ˜์„œํŠธ", ticketLimit = 10)

    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1 = ConcertTicket(userId = 1L, concert = ์ฝ˜์„œํŠธ)
    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2 = ConcertTicket(userId = 2L, concert = ์ฝ˜์„œํŠธ)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2)
    concertRepository.save(์ฝ˜์„œํŠธ)

    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(1)
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(2)

    // when
    concertRepository.delete(์ฝ˜์„œํŠธ)

    // then
    val ์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()

    assertThat(์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(0)
    assertThat(์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(0)
}

ํ•˜๋‚˜์˜ ์ฝ˜์„œํŠธ์— ๋Œ€ํ•ด์„œ 2๊ฐœ์˜ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“์„ ๋ฐœ๊ธ‰๋ฐ›๊ณ , ์ €์žฅ ์งํ›„ findAll์„ ํ†ตํ•ด ์ž˜ ์ €์žฅ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ–ˆ๋‹ค.

์ดํ›„, ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ์ธ '์ฝ˜์„œํŠธ ์—”ํ‹ฐํ‹ฐ'๋ฅผ ์ œ๊ฑฐํ•œ ๋‹ค์Œ ์กฐํšŒ๋ฅผ ํ•ด๋ณด๋ฉด ์ฝ˜์„œํŠธ ์—”ํ‹ฐํ‹ฐ์™€ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ ๋ชจ๋‘๊ฐ€ ์ œ๊ฑฐ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์‹ค์ œ๋กœ ๋ฐœ์ƒํ•œ ์ฟผ๋ฆฌ๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•œ delete ์ฟผ๋ฆฌ๊ฐ€ 2๋ฒˆ, ์ฝ˜์„œํŠธ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•œ delete ์ฟผ๋ฆฌ๊ฐ€ 1๋ฒˆ ๋ฐœ์ƒํ•˜์—ฌ ์ด 3๋ฒˆ์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋‚˜๊ฐ„ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 


 

๐Ÿ’ฌ ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ์™€ ์ž์‹ ์—”ํ‹ฐํ‹ฐ ์—ฐ๊ด€๊ด€๊ณ„ ๋Š๊ธฐ

@Test
@DisplayName("@OneToMany: CascadeType.REMOVE ํ…Œ์ŠคํŠธ - ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ์—์„œ ์ž์‹ ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐ")
fun oneToMany_cascadeType_REMOVE_test_๋ถ€๋ชจ_์—”ํ‹ฐํ‹ฐ์—์„œ_์ž์‹_์—”ํ‹ฐํ‹ฐ_์ œ๊ฑฐ() {
    // given
    val ์ฝ˜์„œํŠธ = Concert(name = "์ธ๊ธฐ ๋งŽ์€ ์ฝ˜์„œํŠธ", ticketLimit = 10)

    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1 = ConcertTicket(userId = 1L, concert = ์ฝ˜์„œํŠธ)
    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2 = ConcertTicket(userId = 2L, concert = ์ฝ˜์„œํŠธ)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2)
    concertRepository.save(์ฝ˜์„œํŠธ)

    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(1)
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(2)

    // when
    ์ฝ˜์„œํŠธ.concertTickets.remove(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    ์ฝ˜์„œํŠธ.concertTickets.remove(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2)

    // then
    val ์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()

    assertThat(์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(1)
    assertThat(์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(2)
}

๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ์ธ ์ฝ˜์„œํŠธ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•œ ํ›„, ํ•ด๋‹น ์—”ํ‹ฐํ‹ฐ์™€ ๊ด€๋ จ๋œ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด remove๋ฅผ ์ง„ํ–‰ํ•˜์—ฌ ๋ถ€๋ชจ์™€ ์ž์‹๊ฐ„์˜ ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ๋Š์–ด์ฃผ์—ˆ๋‹ค. ์ด๋•Œ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ 2๊ฐœ๋Š” '๊ณ ์•„ ๊ฐ์ฒด'๊ฐ€ ๋˜์—ˆ๋‹ค๊ณ  ํŒ๋‹จํ•˜๋Š”๋ฐ, CascadeType.REMOVE์—์„œ๋Š” ๊ณ ์•„ ๊ฐ์ฒด๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ์‹ค์ œ DB์—์„œ ์ œ๊ฑฐ๋˜์ง€๋Š” ์•Š๋Š”๋‹ค.

 


 

๐ŸŒฑ @ManyToOne์—์„œ CascadeType.REMOVE 

cascadeType์˜ ๊ฒฝ์šฐ @OneToMany๋ฟ๋งŒ ์•„๋‹ˆ๋ผ @ManyToOne์—์„œ๋„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val name: String,

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList<ConcertTicket> = Collections.emptyList()
) {

    @OneToMany(
        fetch = FetchType.LAZY,
        mappedBy = "concert",
        cascade = [CascadeType.PERSIST] // here!
    )
    val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()
}

@Entity
class ConcertTicket(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val userId: Long,

    @ManyToOne(
        fetch = FetchType.LAZY,
        cascade = [CascadeType.REMOVE]
    )
    @JoinColumn(
        name = "concert_id",
        foreignKey = ForeignKey(name = "fk_ticket_concert_id")
    )
    var concert: Concert
)

๊ธฐ์กด ์ฝ˜์„œํŠธ ์—”ํ‹ฐํ‹ฐ์— ์ ์šฉํ•˜์˜€๋˜ CascadeType.REMOVE๋ฅผ ์ œ๊ฑฐํ•˜๊ณ , ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด์„œ ์ ์šฉํ•˜์˜€๋‹ค.

 


 

๐Ÿ’ฌ ์ž์‹ ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐํ•˜๊ธฐ - ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐ

@Test
@DisplayName("@ManyToOne: CascadeType.REMOVE ํ…Œ์ŠคํŠธ - ์ž์‹ ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐ")
fun manyToOne_cascadeType_REMOVE_test_์ž์‹_์—”ํ‹ฐํ‹ฐ_์ œ๊ฑฐ() {
    // given
    val ์ฝ˜์„œํŠธ = Concert(name = "์ธ๊ธฐ ๋งŽ์€ ์ฝ˜์„œํŠธ", ticketLimit = 10)
    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1 = ConcertTicket(userId = 1L, concert = ์ฝ˜์„œํŠธ)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    concertRepository.save(์ฝ˜์„œํŠธ)

    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(1)
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(1)

    // when
    concertTicketRepository.delete(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1);

    // then
    val ์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()

    assertThat(์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(0)
    assertThat(์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(0)
}

์œ„ ํ…Œ์ŠคํŠธ์˜ ๊ฒฝ์šฐ, ์ฝ˜์„œํŠธ ์—”ํ‹ฐํ‹ฐ์— ํ•œ ๊ฐœ์˜ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์ด๋‹ค. (์ž์‹์ด 1๊ฐœ๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ)

์ด๋•Œ ์ž์‹ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ฒŒ ๋œ๋‹ค๋ฉด ํ•ด๋‹น ์ž์‹ ์—”ํ‹ฐํ‹ฐ์™€ ์—ฐ๊ด€๋œ ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ์™€ ์ž์‹ ์—”ํ‹ฐํ‹ฐ ๋ชจ๋‘๊ฐ€ ์ œ๊ฑฐ๋œ๋‹ค.

- ์ฐธ๊ณ ๋กœ, ์—ฌ๊ธฐ์„œ ์—ฐ๊ด€์ด ๋˜์–ด ์žˆ๋‹ค๋Š” ๊ฒƒ์€ ๋‹จ์ˆœํžˆ 'DB์—์„œ FK๋กœ ์—ฐ๊ฒฐ์ด ๋˜์–ด ์žˆ๋‹ค'๋ผ๋Š” ์˜๋ฏธ๋ณด๋‹ค, ์˜์†ํ™”๊ฐ€ ๋˜์–ด ์žˆ๋Š”์ง€๋ฅผ ํ™•์ธํ•œ๋‹ค. ์˜์†ํ™”๋˜์ง€ ์•Š๊ณ  ๋‹จ์ˆœํžˆ FK๋กœ๋งŒ ์—ฐ๊ฒฐ์ด ๋˜์–ด ์žˆ๋‹ค๋ฉด ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ œ๊ฑฐํ•˜๋”๋ผ๋„ ์ฝ˜์„œํŠธ ์—”ํ‹ฐํ‹ฐ๊นŒ์ง€ ์ œ๊ฑฐ๋˜์ง€๋Š” ์•Š๋Š”๋‹ค.

 

๊ทธ๋ ‡๋‹ค๋ฉด, ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ž์‹์ด ์žˆ์„ ๋•Œ๋Š” ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?

@Test
@DisplayName("@ManyToOne: CascadeType.REMOVE ํ…Œ์ŠคํŠธ - ์ž์‹ ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐ : ์ž์‹์ด ์—ฌ๋Ÿฌ ๊ฐœ์ผ ๊ฒฝ์šฐ")
fun manyToOne_cascadeType_REMOVE_test_์ž์‹_์—”ํ‹ฐํ‹ฐ_์ œ๊ฑฐ_์ž์‹์ด_์—ฌ๋Ÿฌ๊ฐœ์ผ_๊ฒฝ์šฐ() {
    // given
    val ์ฝ˜์„œํŠธ = Concert(name = "์ธ๊ธฐ ๋งŽ์€ ์ฝ˜์„œํŠธ", ticketLimit = 10)
    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1 = ConcertTicket(userId = 1L, concert = ์ฝ˜์„œํŠธ)
    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2 = ConcertTicket(userId = 2L, concert = ์ฝ˜์„œํŠธ)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2)
    concertRepository.save(์ฝ˜์„œํŠธ)

    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(1)
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(2)

    // when
    concertTicketRepository.delete(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)

    // then
    assertThatThrownBy {
        concertRepository.findAll()
		// concertTicketRepository.findAll()
    }.isInstanceOf(DataIntegrityViolationException::class.java)
}

์ฝ˜์„œํŠธ ์—”ํ‹ฐํ‹ฐ์— 2๊ฐœ์˜ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์žˆ๊ณ , ์ฒซ ๋ฒˆ์งธ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด์„œ ์ œ๊ฑฐํ•œ ์ƒํ™ฉ์ด๋‹ค.

์ด๋•Œ, ์ฝ˜์„œํŠธ์— ๋Œ€ํ•ด ์กฐํšŒํ•˜๋Š” ์‹œ์ ์—์„œ DataIntegrityViolationException์ด ๋ฐœ์ƒํ•œ๋‹ค.

- ์ฐธ๊ณ ๋กœ, ์ฃผ์„์œผ๋กœ concertTicketRepository.findAll()์„ ํ•ด๋‘์—ˆ๋Š”๋ฐ, ์ฝ˜์„œํŠธ๋“  ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์—”ํ‹ฐํ‹ฐ๋“  ์กฐํšŒํ•˜๋Š” ์ฟผ๋ฆฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ๋•Œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

์œ„์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋Œ๋ฆฌ๋Š” ๊ฐ€์žฅ ์ฒ˜์Œ ์‹œ์ ์ธ ์—”ํ‹ฐํ‹ฐ ์ €์žฅ ๋•Œ DB ์ƒํ™ฉ์€ ์•„๋ž˜์™€ ๊ฐ™์„ ๊ฒƒ์ด๋‹ค.

 

[concert]

id name ticket_limit
1 ์ธ๊ธฐ ๋งŽ์€ ์ฝ˜์„œํŠธ 10

 

[concert_ticket]

id user_id concert_id
1 1 1
2 2 1

 

PK = 1์ธ ์ฝ˜์„œํŠธ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๊ณ , ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ๋ฅผ FK๋กœ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” PK = 1, 2์ธ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๋Š” ํ˜•ํƒœ์ด๋‹ค.

 

ํ•˜์ง€๋งŒ, ์‹ค์ œ ์ƒ์„ฑ๋œ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ฉด 1๊ฐœ์˜ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“์— ๋Œ€ํ•ด ์ œ๊ฑฐํ•˜๊ณ , ๋ฐ”๋กœ ์ฝ˜์„œํŠธ๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. 

concert_ticket ํ…Œ์ด๋ธ”์˜ PK=1์ธ ์ฒซ ๋ฒˆ์งธ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ œ๊ฑฐ๋˜๊ณ , concert ํ…Œ์ด๋ธ”์˜ PK=1์ธ ์ฒซ ๋ฒˆ์งธ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ œ๊ฑฐ๋˜๋Š” ์ƒํ™ฉ์ด๋‹ค. ํ•˜์ง€๋งŒ, concert ํ…Œ์ด๋ธ”์˜ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ œ๊ฑฐ๋˜๋ ค๊ณ  ํ•  ๋•Œ ์—”ํ‹ฐํ‹ฐ์— ๊ฑธ์–ด๋‘์—ˆ๋˜ ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด์— ์˜ํ•ด์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋œ๋‹ค.

@Entity
class ConcertTicket(
    ....
    @JoinColumn(
        name = "concert_id",
        foreignKey = ForeignKey(name = "fk_ticket_concert_id") // here
    )
    var concert: Concert
)

๋•Œ๋ฌธ์— concert ํ…Œ์ด๋ธ”์˜ PK=1์ธ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๋ฉด, concert_ticket ํ…Œ์ด๋ธ”์˜ ๋‘ ๋ฒˆ์งธ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— fk ๊ฐ’์ด null์ด ๋  ์ˆ˜ ์—†์–ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

 

โญ๏ธ ์™œ deleteํ•˜๋Š” ์‹œ์ ์ด ์•„๋‹Œ, findAll()์„ ํ†ตํ•ด ์กฐํšŒํ•˜๋Š” ์‹œ์ ์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฑธ๊นŒ?
@Test
@DisplayName("@ManyToOne: CascadeType.REMOVE ํ…Œ์ŠคํŠธ - ์ž์‹ ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐ : ์ž์‹์ด ์—ฌ๋Ÿฌ ๊ฐœ์ผ ๊ฒฝ์šฐ")
fun manyToOne_cascadeType_REMOVE_test_์ž์‹_์—”ํ‹ฐํ‹ฐ_์ œ๊ฑฐ_์ž์‹์ด_์—ฌ๋Ÿฌ๊ฐœ์ผ_๊ฒฝ์šฐ() {
    // given
    val ์ฝ˜์„œํŠธ = Concert(name = "์ธ๊ธฐ ๋งŽ์€ ์ฝ˜์„œํŠธ", ticketLimit = 10)
    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1 = ConcertTicket(userId = 1L, concert = ์ฝ˜์„œํŠธ)
    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2 = ConcertTicket(userId = 2L, concert = ์ฝ˜์„œํŠธ)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2)
    concertRepository.save(์ฝ˜์„œํŠธ)

    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(1)
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(2)

    // when
    assertDoesNotThrow {
        concertTicketRepository.delete(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    }
}

์‹ค์ œ๋กœ, ๋‹จ์ˆœํžˆ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ๋ ˆํŒŒ์ง€ํ† ๋ฆฌ์—์„œ delete๋ฅผ ํ•œ๋‹ค๊ณ  ํ•ด์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.

์• ์ดˆ์— ์กฐํšŒํ•˜๊ธฐ ์ „๊นŒ์ง€ delete ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€๋„ ์•Š๋Š”๋‹ค. (์œ„์˜ select๋Š” ์ฝ˜์„œํŠธ, ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“์— ๋Œ€ํ•œ findAll())

JPA์˜ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ๊ฐํ•˜๋ฉด ๊ฐ„๋‹จํ•˜๊ฒŒ ๋‹ต์ด ๋‚˜์˜ฌ ๊ฒƒ์ด๋‹ค.

 

delete์˜ ๊ฒฝ์šฐ ์‹ค์ œ DB๋กœ ์ œ๊ฑฐ๋ฅผ ์œ„ํ•œ ์ฟผ๋ฆฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๊ฒƒ์ด ์•„๋‹Œ, ๋‹จ์ˆœํžˆ '์˜์†์„ฑ ์ปจํ…์ŠคํŠธ'์—์„œ ํ•ด๋‹น ์—”ํ‹ฐํ‹ฐ๋ฅผ '์ œ๊ฑฐ ์ฒ˜๋ฆฌ'๋ฅผ ํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

@Override
@Transactional
@SuppressWarnings("unchecked")
public void delete(T entity) {

	Assert.notNull(entity, "Entity must not be null");

	if (entityInformation.isNew(entity)) {
		return;
	}

	Class<?> type = ProxyUtils.getUserClass(entity);

	T existing = (T) em.find(type, entityInformation.getId(entity));

	// if the entity to be deleted doesn't exist, delete is a NOOP
	if (existing == null) {
		return;
	}

	em.remove(em.contains(entity) ? entity : em.merge(entity));
}

์‹ค์ œ๋กœ, ๊ตฌํ˜„์ฒด๋ฅผ ์Šฌ์ฉ ๋ณด๋ฉด em.remove()๋ฅผ ํ†ตํ•ด์„œ entityManager์— ์กด์žฌํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด์„œ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

๊ทธ๋ž˜์„œ, ์‹ค์ œ DB์— ์ œ๊ฑฐ ์ฟผ๋ฆฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•œ๋‹ค.

fun manyToOne_cascadeType_REMOVE_test_์ž์‹_์—”ํ‹ฐํ‹ฐ_์ œ๊ฑฐ_์ž์‹์ด_์—ฌ๋Ÿฌ๊ฐœ์ผ_๊ฒฝ์šฐ() {
    // given
    val ์ฝ˜์„œํŠธ = Concert(name = "์ธ๊ธฐ ๋งŽ์€ ์ฝ˜์„œํŠธ", ticketLimit = 10)
    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1 = ConcertTicket(userId = 1L, concert = ์ฝ˜์„œํŠธ)
    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2 = ConcertTicket(userId = 2L, concert = ์ฝ˜์„œํŠธ)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2)
    concertRepository.save(์ฝ˜์„œํŠธ)

    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(1)
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(2)

    // when
    assertDoesNotThrow {
        concertTicketRepository.delete(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    }

    assertThatThrownBy {
        entityManager.flush()
    }.isInstanceOf(ConstraintViolationException::class.java)
}

entityManager์— ๋Œ€ํ•ด ๊ฐ•์ œ๋กœ ํ”Œ๋Ÿฌ์‹œ๋ฅผ ํ•ด์„œ, ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์˜ ๋‚ด์šฉ์„ DB์— ๋ฐ˜์˜์‹œํ‚ค๋Š” ๊ฒƒ์ด๋‹ค.

์ด๋ ‡๊ฒŒ ๋˜๋ฉด ์•„๊นŒ์™€ ๋‹ค๋ฅด๊ฒŒ ๋ณ„๋„์˜ ์กฐํšŒ ์ฟผ๋ฆฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค์ง€ ์•Š๋”๋ผ๋„ delete ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๋‹ค๋งŒ, ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด์ด ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋˜‘๊ฐ™์ด ConstraintViolationException์€ ์ƒ๊ธธ ์ˆ˜๋ฐ–์— ์—†๋‹ค.

๐Ÿ’ก DataIntegrityViolationException์™€ ConstraintViolationException์€ ๋ชจ๋‘ ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด ์œ„๋ฐ˜์— ์˜ํ•ด์„œ ๋ฐœ์ƒํ•œ๋‹ค. ๋‹ค๋งŒ, ConstraintViolationException์˜ ๊ฒฝ์šฐ ์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ์˜ˆ์™ธ ํด๋ž˜์Šค์ด๊ณ , ConstraintViolationException๋Š” ํ•˜์ด๋ฒ„๋„ค์ดํŠธ์—์„œ ์ œ๊ณตํ•˜๋Š” ์˜ˆ์™ธ ํด๋ž˜์Šค์ด๋‹ค.

 


 

๐Ÿ’ฌ ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ์™€ ์ž์‹ ์—”ํ‹ฐํ‹ฐ ์—ฐ๊ด€๊ด€๊ณ„ ๋Š๊ธฐ

@Test
@DisplayName("@ManyToOne: CascadeType.REMOVE ํ…Œ์ŠคํŠธ - ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ์—์„œ ์ž์‹ ์—”ํ‹ฐํ‹ฐ ์ œ๊ฑฐ")
fun manyToOne_cascadeType_REMOVE_test_๋ถ€๋ชจ_์—”ํ‹ฐํ‹ฐ์—์„œ_์ž์‹_์—”ํ‹ฐํ‹ฐ_์ œ๊ฑฐ() {
    // given
    val ์ฝ˜์„œํŠธ = Concert(name = "์ธ๊ธฐ ๋งŽ์€ ์ฝ˜์„œํŠธ", ticketLimit = 10)

    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1 = ConcertTicket(userId = 1L, concert = ์ฝ˜์„œํŠธ)
    val ์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2 = ConcertTicket(userId = 2L, concert = ์ฝ˜์„œํŠธ)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    ์ฝ˜์„œํŠธ.addTicket(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2)
    concertRepository.save(์ฝ˜์„œํŠธ)

    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(1)
    assertThat(์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(2)

    // when
    ์ฝ˜์„œํŠธ.concertTickets.remove(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“1)
    ์ฝ˜์„œํŠธ.concertTickets.remove(์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“2)

    // then
    val ์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค = concertRepository.findAll()
    val ์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค = concertTicketRepository.findAll()

    assertThat(์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ๋“ค).hasSize(1)
    assertThat(์‚ญ์ œ_์ดํ›„_์ €์žฅ๋œ_์ฝ˜์„œํŠธ_ํ‹ฐ์ผ“๋“ค).hasSize(2)
}

์ด์ „์˜ @OneToMany์˜ ๊ฒฝ์šฐ, ๋ถ€๋ชจ ์—”ํ‹ฐํ‹ฐ์—์„œ ์ž์‹ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ œ๊ฑฐํ•˜๋”๋ผ๋„ ์•„๋ฌด ์ผ๋„ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•˜์—ˆ๋‹ค.

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ @ManyToOne์˜ ์—ญ์‹œ ๊ณ ์•„ ๊ฐ์ฒด๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ์‹ค์ œ DB์— delete ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•„์„œ ๊ทธ๋Œ€๋กœ ์กฐํšŒ๊ฐ€ ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 


 

์ƒ๊ฐ๋ณด๋‹ค ๊ธ€์ด ๊ธธ์–ด์ ธ์„œ orphanRemoval์— ๋Œ€ํ•œ ๋‚ด์šฉ์€ 2ํŽธ์—์„œ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž!

Comments