[JPA] save() 작동 방식

이수진·2024년 5월 9일

발생한 문제

CourseLecture는 1:N 관계로 Course에 Lecture를 추가하는 addLecture 메소드를 다음과 같이 작성했다. 하지만 테스트 결과 NPE가 발생했다. lecture.toResponse()를 실행할때 id값이 null이었기 때문이다.

    @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() // -> NPE 발생
    }
    
    fun Lecture.toResponse(): LectureResponse {
    return LectureResponse(
        id = id!!,
        title = title,
        videoUrl = videoUrl
    )
}

CascadeType.ALL 설정이 되어있어서 courseRepository.save(course) 실행시 자식 엔티티인 Lecture도 영속성 컨텍스트에 들어가게 될 것이라 생각했다. 실제로 실행시 lecture 테이블 insert 쿼리가 로그에 찍히는 것을 확인할 수 있었다. 그런데 왜 NPE가 발생했을까?

    @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)

        val lectureFromCourse = course.lectures.last()
        return lectureFromCourse.toResponse()
    }

위 코드처럼 save() 실행 후 course에서 lectures를 꺼내와 저장된lecture 객체를 가져와서 처음에 만든 lectrue 객체와 비교해보았다.

lecture != lectureFromCourse
디버깅 결과 두 객체가 다른것으로 확인되었다.

왜 다를까? 아마 JPA 내부 동작 중에 객체가 바뀌는 것 같았다.

save() 작동 방식

JpaRepository의 구현체인 SimpleJpaRepository에 있는 save() 코드를 찾아보았다.

    @Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null");
        if (this.entityInformation.isNew(entity)) {
            this.entityManager.persist(entity);
            return entity;
        } else {
            return this.entityManager.merge(entity);
        }
    }

코드를 보면 새로운 엔티티이면 persist()를, 아니면 merge()를 실행한다는 것을 알 수 있다. 새로운 엔티티를 구분하는 방법은 엔티티의 id값이 존재하는지로 판단한다.

따라서 course는 db에서 가져와 id값이 들어가있기 때문에 save() 실행시 merge()가 실행되게 된다. 이때 자식 엔티티까지 적용되어 lectruemerge()를 실행한다.

db에서 가져온 course는 영속성 컨텍스트 안에 있기 때문에 merge()가 실행된다.

persist() vs merge()

  • persist() : 파라미터로 들어온 엔티티를 영속성 상태로 바꾸어 반환한다.
  • merge() : 영속성 상태의 새로운 엔티티를 만들어 반환한다.

따라서 처음에 NPE가 발생했던 이유는 merge()가 실행되면서 기존에 만든 Lecture 객체가 영속 상태로 변한게 아니라 영속 상태의 새로운 객체가 반환되어 course에 들어가있었기 때문이었다.

수정한 코드

따라서 course가 아니라lecture를 바로 save()하는 방식으로 코드를 수정하였다.

    @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)

        return lectureRepository.save(lecture).toResponse()
    }

0개의 댓글