[Hibernate] Hibernate가 관리하는 컬렉션의 참조(Reference)

호호빵·2025년 12월 24일

기존 코드와 문제점

  • 기존 코드
if (!isNewGroup) {
    group.groupAttachFiles = groupAttachFileRepository.findByGroup(group.id)
        .toMutableList()
}

// Hibernate가 관리하는 컬렉션(PersistentBag)을 일반 List로 덮어씌워서 연결이 끊어짐
  • 문제점
    • Hibernate가 관리하는 컬렉션의 참조(Reference)를 끊어버림.
    • Hibernate는 엔티티를 조회할 때 List나 Set 같은 컬렉션을 PersistentBag 이라는 특수한 래퍼(Wrapper) 객체로 감싸서 관리함
    • 이 객체는 변경 사항(추가, 삭제)을 감지하여 DB에 반영하는 역할을 힘
      하지만 코드에서 campaign.campaignAttachFiles = ... 로 새로운 리스트를 대입하면, Hibernate가 감시하던 PersistentBag이 버려지고 일반 ArrayList로 교체되어버림
    • 이 순간 Hibernate는 "관리하던 자식 객체들의 연결이 끊어졌다"고 판단하여 에러를 발생시킴

해결 방안

// DB의 값을 미리 가져옴
val groupAttachFiles = groupAttachFileRepository.findByGroup(group.id)

// 수정 시, 항상 DB의 데이터를 반영하도록 한다.
val isNewGroup = group.id == 0L
if (!isNewGroup) {
   group.groupAttachFiles.clear()        // 기존 리스트를 비움 (참조 유지) (delete-orphan 동작)
   groupAttachFiles.forEach {
        group.groupnAttachFiles.add(it)  // 가져온 파일들을 채워 넣음
   }
}
  • 리스트 자체를 교체해버리지 말고, 기존 리스트의 내용을 비우고(clear) 새로 채워 넣는(addAll) 방식으로 변경해야 함

더 알아보기

  • 영속성 컨텍스트와 컬렉션 래퍼 (Persistent Collection)
    핵심: Hibernate가 엔티티의 List, Set을 어떻게 PersistentBag, PersistentSet으로 감싸는지(Proxy) 이해해야 함

    - 포인트: "왜 내 리스트의 클래스 타입을 찍어보면 java.util.ArrayList가 아니라 org.hibernate.collection...이 나올까?"
  • 고아 객체 제거 (Orphan Removal) vs Cascade.REMOVE
    핵심: orphanRemoval = true 옵션의 정확한 동작 원리.

    - 포인트 : list.remove(entity)를 했을 때 DB에서 DELETE 쿼리가 나가는 원리. 왜 컬렉션을 통째로 교체하면(=) Hibernate가 이를 "모든 자식을 고아로 만들었다"고 오해하거나 에러를 뱉는지.
  • 변경 감지 (Dirty Checking)와 Merge
    핵심: save()를 호출하지 않아도 트랜잭션이 끝날 때 UPDATE 쿼리가 나가는 원리.

    - 포인트: JPA에서 데이터를 수정할 때 왜 setter만 호출하고 save를 부르지 않아도 되는지, 
              그리고 컬렉션 내부의 변경은 어떻게 감지되는지.
  • 연관 관계 편의 메서드 (Helper Method)
    핵심: 양방향 연관 관계(부모 ↔ 자식)에서 실수하기 쉬운 부분을 방지하는 패턴.

    - 포인트: setParent(this)와 list.add(child)를 묶어서 하나의 메서드로 관리하는 방법 (예: campaign.addFile(file)).
profile
하루에 한 개념씩

0개의 댓글