Cascade란?

Cascde는 영속성 전이라고 합니다.  Cascade 옵션을 정의한 엔티티가 영속화되면 연관된 엔티티도 영속화되고 삭제되면 삭제되는 것 속성입니다.

@ManyToOne, @OneToMany 등 연관관계를 설정하는 부분에 옵션으로 정의되어 있습니다.

 

 

주의 : 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화한다는 편리함만 제공할 뿐 연관관계를 설정하는 어노테이션과는 관련이 없습니다.

 

Cascade 종류

  • ALL
  • PERSIST
  • REMOVE
  • MERGE
  • REFRESH
  • DETACH

 

Cascade옵션 중 ALL, PERSIST, REMOVE, MERGE, DETACH에 대해서만 예제를 다루고 REFRESH에 대해서는 개념만 알아보겠습니다.

 

예제 공통 코드

 

// Image 엔티티
@Entity
class Image(
    @Id
    @GeneratedValue
    var id: Long,
    var title: String,
    var address: String,
)   

=============================

// Board 엔티티
@Entity
class Board(
    @Id
    @GeneratedValue
    var id: Long,
    var title: String,
    var content: String,
    @OneToMany
    @JoinColumn(name = "board_id")
    var images: MutableList<Image>,
)

 

CascadeType  예제

 

CascadeType.ALL - PERSIST

@Entity
class Board(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0,
    var title: String,
    var content: String,
    @OneToMany(cascade = [CascadeType.ALL])
    @JoinColumn(name = "board_id")
    var images: MutableList<Image>,
)

 

 

@Service
class BoardService(
    private val em: EntityManager,
) {
    @Transactional
    fun save() {
        val images: MutableList<Image> = mutableListOf(
            Image(title = "이미지1", address = "http1"),
            Image(title = "이미지2", address = "http2"),
        )
        val board: Board = Board(title = "제목", content = "내용", images = images)
        em.persist(board)
        return em.find(Board::class.java,1L)
    }
}

 

@SpringBootTest
class BoardServiceTest(
    private val boardService: BoardService,
) : FreeSpec({
    "CascadeType.ALL" - {
        "save" - {
            val save = boardService.save()
        }
    }
})

 

발생하는 쿼리

 

 

필자는 Image를 저장한 적이 없습니다. 그런데 쿼리를 보면 image에 대한 INSERT쿼리가 발생하고 있습니다. 

이유는 Cascade 옵션을 ALL로 하면 Cascade에 있는 PERSIST, REMOVE, REFRESH, MERGE, DETACH가 적용이 됩니다.

즉 위 예제는 PERSIST 속성이 적용된것을 확인할 수 있습니다.

 

CascadeType.ALL - REMOVE

 

@Service
class BoardService(
    private val em: EntityManager,
) {
    @Transactional
    fun delete() {
        val images: MutableList<Image> = mutableListOf(
            Image(title = "이미지1", address = "http1"),
            Image(title = "이미지2", address = "http2"),
        )
        val board: Board = Board(title = "제목", content = "내용", images = images)
        em.persist(board)
        em.flush()
        em.remove(board)
    }
}

 

@SpringBootTest
class BoardServiceTest(
    private val boardService: BoardService,
) : FreeSpec({
    "CascadeType.ALL" - {
        "delete" - {
            val save = boardService.delete()
        }
    }
})

 

발생하는 쿼리

 

 

CascadeType.ALL - DETACH

 

@Service
class BoardService(
    private val em: EntityManager,
) {
    @Transactional
    fun detach() {
        val images: MutableList<Image> = mutableListOf(
            Image(title = "이미지1", address = "http1"),
            Image(title = "이미지2", address = "http2"),
        )
        val board: Board = Board(title = "제목", content = "내용", images = images)
        em.persist(board)
        val image1 = em.find(Image::class.java, 1L)
        val image2 = em.find(Image::class.java, 2L)
        em.flush()
        em.detach(board)
        println("------------detach 후 쿼리--------------")
        val find = em.find(Board::class.java, 1L)
        em.find(Image::class.java, 1L)
        em.find(Image::class.java, 2L)
    }

    @Transactional
    fun notDetach() {
        val images: MutableList<Image> = mutableListOf(
            Image(title = "이미지1", address = "http1"),
            Image(title = "이미지2", address = "http2"),
        )
        val board: Board = Board(title = "제목", content = "내용", images = images)
        em.persist(board)
        em.find(Image::class.java, 1L)
        em.find(Image::class.java, 2L)
        em.flush()
        println("------------detach 안했을때 쿼리--------------")
        val find = em.find(Board::class.java, 1L)
        em.find(Image::class.java, 1L)
        em.find(Image::class.java, 2L)
    }
}

 

@SpringBootTest
class BoardServiceTest(
    private val boardService: BoardService,
) : FreeSpec({
    "CascadeType.ALL" - {
         "detach" - {
            teamApiService.detach()
        }
        "notDetach" - {
            teamApiService.notDetach()
        }
    }
})

 

발생하는 쿼리 - DETACH

 

 

em.persist를 하여 1차 캐시에 Board와 Image에 대한 정보가 담겨 있는데 영속성 컨텍스트를 em.detach를 하여 엔티티 생명주기를 준영속 상태가 되었습니다. 그래서 Board와 Image 엔티티를 찾아올 때 SELECT 쿼리가 발생하는 것을 확인할 수 있습니다.

 

발생하는 쿼리 - NOT DETACH

 

 

DETACH를 안 했을 때 em.persist로 Board와 Image 엔티티는 1차 캐시에 영속 상태로 있어 Board와 Image 엔티티를 찾아올 때 DB를 통해 가져오는 게 아닌 1차 캐시에서 가져오기 때문에 SELECT 쿼리가 발생하지 않는 것을 확인할 수 있습니다.

 

CascadeType.ALL - MERGE

 

@Service
class BoardService(
    private val em: EntityManager,
) {
    @Transactional
    fun merge() {
        val images: MutableList<Image> = mutableListOf(
            Image(title = "이미지1", address = "http1"),
            Image(title = "이미지2", address = "http2"),
        )
        val board: Board = Board(title = "제목", content = "내용", images = images)
        em.persist(board)
        val image1 = em.find(Image::class.java, 1L)
        val image2 = em.find(Image::class.java, 2L)
        em.flush()
        em.detach(board)

        val mergeBoard = em.merge(board)
        val mergeImage1 = em.find(Image::class.java, 1L)
        val mergeImage2 = em.find(Image::class.java, 2L)

        println("board == mergeBoard => ${board == mergeBoard}")
        println("image1 == mergeImage1 => ${image1 == mergeImage1}")
        println("image2 == mergeImage2 => ${image2 == mergeImage2}")
    }

}

 

@SpringBootTest
class BoardServiceTest(
    private val boardService: BoardService,
) : FreeSpec({
    "CascadeType.ALL" - {
          "merge" - {
            teamApiService.merge()
        }
    }
})

 

결과

 

결과는 false가 나옵니다. 이유는 준영속 상태에서 merge를 통해 영속상태가 되었다고 하더라도 준영속되기  전 영속상태의 객체와는 전혀 다른 객체이기 때문입니다.

 

REFRESH

REFRESH옵션은 데이터베이스로부터 데이터를 다시 읽어 들이는 refresh 동작을 부모에서 자식 엔티티로 전파하는 옵션입니다.

 

 

Cascade 설정을 안 하고 Board를 저장을 하게 되면 어떻게 될까?

 

결과는 아래와 같은 오류가 발생합니다.

org.hibernate.TransientObjectException: object references an unsaved transient instance -
save the transient instance before flushing: team.guin.domain.example.Image
java.lang.IllegalStateException: org.hibernate.TransientObjectException: object references an unsaved transient instance -
save the transient instance before flushing: team.guin.domain.example.Image

 

이유 

Board에 대해 저장이 이루어질 때 Image는 Board와 달리 비영속 상태이기 때문에 발생하는 오류입니다. 만약 Board가 저장될 때 Image가 같이 저장되길 원하고 위와 같은 오류를 안 만나기 위해서는 CascadeType.PERSIST를 사용하거나 @Transient를 사용합니다.

 

@Transient 사용

@Entity
class Board(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0,
    var title: String,
    var content: String,
    @OneToMany
    @JoinColumn(name = "board_id")
    @Transient
    var images: MutableList<Image>,
)

 

@Transient를 사용했을 때 발생하는 쿼리

 

 

Board만 저장되는 insert쿼리를 확인할 수 있습니다. 이유는 @Transient를 사용하면 Image를 영속화를 하지 않은 상태에서 Board를 저장하는 것이기 때문입니다. 그런데 필자는 Board가 저장될 때 Image도 같이 저장되는 것을 원합니다.  그러기 위해선 위에서 말한 설명한 Cascade옵션을 설정해 주면 가능합니다.

 

정리

Cascade는 영속성 전이입니다. Cascade 옵션을 사용하는 엔티티가 영속화가 되면 연관된 엔티티에게 영속성 전이가 됩니다.

위 예제를 보면 CascadeType.ALL로 설정을 해놓고 Board에 대해서만 em.persist, em.detach, em.merge, em.remove 행위를 하였습니다. 그런데 결과를 보면 연관되어 있는 Image엔티티에도 Board와 동일하게 적용된 것을 확인할 수 있었습니다.

만약 CascadeType.ALL이 아닌 PERSIST, REMOVE, DETACH, MERGE, REFRESH로 설정하게 된다면 설정된 옵션만 적용됩니다.

 

    @OneToMany(cascade = [CascadeType.PERSIST, CascadeType.REMOVE])

 

  • ALL : PERSIST, REMOVE, MERGE, DETACH, REFRESH 모두 적용됩니다.
  • PERSIST : 연관된 엔티티를 영속 상태로 만들어줍니다.
  • REMOVE : 연관된 엔티티도 함께 삭제됩니다.
  • DETACH : 연관된 엔티티도 함께 준영속상태로 만들어줍니다.
  • MERGE : 연관된 엔티티도 함께 새로운 영속 상태로 만들어줍니다.
  • REFRESH : 데이터베이스로부터 데이터를 다시 읽어 들이는 refresh 동작을 연관된 엔티티로 전파합니다.