[JPA] 영속성 전이

RID·2024년 6월 26일
0

JPA 이해하기

목록 보기
3/4

배경


지난 글에서 Entity 사이의 연관관계 매핑에 대해서 살펴보았다. 이번에는 연관된 Entity 들의 영속성을 다룰 수 있는 옵션 중 하나인 영속성 전이에 대해서 살펴보자. 아래와 같은 상황을 생각해보자.

  • 게시글과 댓글을 1:N 관계이며, 게시글 쪽에 @OneToMany로 연관관계가 설정되어 있다.
  • 어플리케이션 정책 상, 게시글이 삭제될 경우 관련된 댓글도 반드시 삭제되도록 한다.
@Entity
@Table(name = "post")
class Post(
	@Column(name = "content")
    var body : String,
    
    @OneToMany
    val comments : List<Comment>
){
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null
}

이 상태에서 아래와 같은 코드를 이용해서 post를 삭제하면 어떻게 될까?


@Transactional
fun deletePost(postId: Long){
	val post = postRepository.findByIdOrNull(postId) ?: throw RuntimeException()
    postRepository.delete(post)
}

먼저 findByIdOrNull() 함수의 호출로 인해서 post, 그리고 연관된 comment가 영속성 컨텍스트의 관리를 받게 된다.

그리고 delete()의 호출을 받아 쓰기지연 SQL 저장소에 해당 post에 대한 delete 쿼리가 올라간다.

이후 트랜잭션이 종료되어 flush()가 호출되는 시점에 DB에 쿼리가 날아가 삭제된다.

이 흐름에서 단지 연관관계를 맺었다고 해서 (그러니까 post 객체 내에 연관된 comment가 포함되었다고 해서) post를 삭제하는 행위로 연관된 comment까지 삭제되길 기대하면 안된다는 것이다. (DB에 FK 삭제에 대한 constraint를 걸지 않으면 post를 삭제할 수 없어 오류가 발생한다.)

JPA는 DB의 데이터를 객체로 관리할 수 있게 도와준다고 했다. 그렇기 때문에 항상 어플리케이션에서 관리하는 Entity와 실제 영속성을 가진 DB 데이터와의 흐름을 잘 살펴보아야 한다.

JPA는 위와 같은 상황에 활용가능한 영속성 전이, Cascade 옵션을 제공한다. 아래에서 살펴보자.

Persistence cascade


영속성 전이는 Entity의 상태변화를 만들어내는 함수를 연관된 Entity에도 그대로 적용하는 것이다.

Entity의 영속상태 변화를 만들어내는 함수는 persist(), remove(), detach(), refresh(), merge() 등이 있고, 각각에 대한 Cascade 옵션이 존재한다.

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

위의 옵션들이 EntityManager 에서 사용되는 함수의 이름이랑 정확히 동일하다. 즉, CASCADE 옵션을 걸어둔 Entity에 대해서 위의 이름을 가진 함수가 호출되는 경우, 연관된 Entity에도 동일한 함수를 호출하는 것이다.

아래 상황을 살펴보자.

@Entity
@Table(name = "post")
class Post(
	@Column(name = "content")
    var body : String,
    
    @OneToMany()
    val comments : MutableList<Comment> = mutableListOf()
){
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null
    
    fun addComment(comment: Comment){
    	comments.add(comment)
    }
}
@Transactional
fun test(){
	val post = Post("test post")
    val comment = Comment("test comment")
    post.addComment(comment)
    postRepository.save(post)
}

위의 함수에서 우리는 Post가 저장될 때 연관된 Comment도 저장되었으면 좋겠어! 라고 생각하며 코드를 작성하겠지만, 실제 의도대로 동작하지 않는다. (Comment에 대한 insert 쿼리가 발생하지 않는다!)

Post를 저장할 때 필요한 Comment 는 실제 DB에 존재하지 않고, 영속성 컨텍스트에 의해 관리되고 있는 상태도 아니기 때문에 Post 입장에서는 필요한 Entity가 없다고 판단되는 것이다.

이 때 만약 @OneToMany 의 cascade 옵션을 CascadeType.PERSIST로 주게되면, new 상태의 post가 save() 메소드에 의해 호출된 persist()로 영속화가 진행되고, 연관된 Comment에 대해서도 cascade 옵션에 의해 persist()가 호출되어 성공적으로 저장이 된다.

이렇게 cascade 옵션을 주게 되면 연관된 Entity에 대해서 동일한 EntityManager의 함수 호출을 자동으로 발생하게 도와준다.

Cascade 쓰지 마세요!


사실 이런 영속성 전이 옵션에 대해서 조금 찾아보면 해당 옵션을 쓰지 말라고 얘기하는 글이나 영상을 많이 찾아볼 수 있다. 이유를 한 번 살펴보자.

우리가 영속성 전이에 대해서 헷갈릴 수 있는 부분이 있는데, CASCADE 옵션이 특정 행위 를 전파해주는 역할이라고 생각한다는 것이다.

예를 들어, persist 옵션에 대해 'Entity가 저장될 때 연관된 Entity도 저장된다' 라며 저장 행위를 전파해준다고 인식하고 있으면 예상치 못한 상황에 대해 대처하지 못한다.

아래 상황을 한 번 보자. 사실 아래 상황과 관련된 글은 이전에 작성한 적이 있다.

Spring Data JPA에서 양방향 영속성 전파 문제

CourseLecture는 1:n 관계로 서로 양방향 연관관계를 맺고있으며, Course@OneToMany에 cascade 옵션을 ALL로 설정한 상태이다.

    @Transactional
    override fun addLecture(courseId: Long, request: AddLectureRequest): LectureResponse {
        val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
        val lecture = Lecture(
            title = request.title,
            videoUrl = request.videoUrl,
            course = course
        )
        course.addLecture(lecture) 
        courseRepository.save(course) 
        return lecture.toResponse() 
    }

문제가 되었던 부분은 위의 함수에서 lecture 를 dto 형태로 변환하는 toResponse()에서 발생한다.

지난번에 save() 함수는 PK필드가 null인 Entity에 대해서는 persist(), PK가 null이 아닌Entity에 대해서는 merge()를 호출한다고 했다.

여기서 course는 이미 id를 가지고 있기 때문에 save() 내부에서 merge()가 호출된다. 이때 cascade 옵션 ALL에 의해서 연관된 lecture 역시 merge()가 호출된다는 사실을 인지해야 한다.

하지만 만약 우리가 cascade 옵션이 저장 행위를 전파한다라고 생각하게 되면 lecture가 새로 저장되어 id를 부여받고, toResponse()가 정상 작동할 것이라 예상하게 된다.

하지만 실제 lecture의 경우 cascade 옵션에 의해 merge()메소드의 인자로 들어가게 되고, 그로 인해 실제 영속성 컨텍스트에서 관리되는 Entity는 인자로 전달해준 lecture와 다른 객체가 된다.

Spring Data JPA가 repository를 제공해서 영속성 컨텍스트의 관리를 편하게 해주는 것과 마찬가지로 내부적으로 진행해주는 것들에 대해서 흐름을 완벽히 읽고있지 않으면 문제가 생기는 상황들이 많은 것 같다.

개인적으로 cascade 옵션을 사용해서 자동으로 작업이 진행되길 기대하는 것보다 직접 연관 Entity를 관리하는 것이 훨씬 더 나은 것 같다.

참고 자료


JPA 기초 17 영속성 전파 & 연관 고려사항
[2019] Spring JPA의 사실과 오해

0개의 댓글