해결된 질문
작성
·
156
·
수정됨
1
안녕하세요! 정말 오랜만에 질문 올립니다. 다름이 아니라 아래와 같이 Parent, Child 가 있고 이 둘을 양방향으로 맺었을때
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private final Set<Child> children = new HashSet<>();
public Parent(String name) {
this.name = name;
}
public void addChild(Child... childs) {
for (Child c : childs) {
c.addParent(this);
children.add(c);
}
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id", nullable = false)
private Parent parent;
public Child(String name) {
this.name = name;
}
public void addParent(Parent parent) {
this.parent = parent;
}
}
아래와 같이 테스트코드를 짜면
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@DataJpaTest(showSql = false)
class ParentRepositoryTest {
@Autowired ParentRepository parentRepository;
@Autowired ChildRepository childRepository;
@Rollback(false)
@Test
@DisplayName("delete, deleteAll, deleteAllInBatch 를 비교한다.")
public void differenceBetweenDeleteAllAndDeleteAllInBatch2() {
Parent parent1 = new Parent("부모 1");
Child child1 = new Child("자식 1");
Child child2 = new Child("자식 2");
Child child3 = new Child("자식 3");
parent1.addChild(child1, child2, child3);
parentRepository.save(parent1);
Parent parent = parentRepository.findAllWithChildren("부모 1").get();
parent.getChildren().remove(child1);
childRepository.delete(child1);
}
}
parent.getChildren().remove(child1);
의 존재 여부에 따라 delete 쿼리가 나갈지 말지 결정되더라구요.
이거에 대해서 고민을 해봤습니다. 제가 내린 결론은
"하나의 tx 에서 Parent 가 속한 Child 들 중 하나를 childRepository.delete(Child) 혹은 deleteAll() 을 통해 지우려할때, Parent 의 Child 의 컬렉션에서 지울 Child 를 먼저 remove 하여 부모 자식간의 연결관계를 끊어주는 작업이 선행되어야 한다. 그 이유는 삭제할 Child 를 repository 에서 지우기만 하면 Parent 에 남아있는 컬렉션에는 지워진 데이터가 카컬렉션에 남아있어 로직에 문제가 발생할 수 있기 때문이다. 그래서 Parent 에서 지울 Child 를 먼저 remove 를 사용하지 않으면 delete 쿼리가 나가지 않는 것이고, 먼저 remove 를 하면 delete 쿼리가 나가는 것이다"
라고 결정지었습니다. 아무래도 JPA 에서 최고 권위자인 김영한강사님의 조언이 필요합니다. 감사합니다.
(고아객체 제거인 orphanRemoval 은 일부러 사용하지 않았습니다. 순수 delete 에서만 그 원리를 알고싶습니다. 항상 무지성으로 querydsl 혹은 in 절로 지우기만해서 근본을 까먹은거같네요 ㅠㅠ)
답변 2
1
0
안녕하세요, 인프런 AI 인턴입니다. PrivilegeEscalate님의 질문에 대해 답변을 제공하려고 합니다.
답변으로 제공드릴 내용은 “단순 delete(Instance) 처리에 대한 동작 방식 이해”와 관련된 내용입니다. @OneToMany
양방향 매핑 시 parent.getChildren().remove(child1);
의 호출 여부가 해당 Child
엔티티의 delete 쿼리를 결정한다는 부분에 대한 근본적인 이해가 필요해 보입니다.
CascadeType
과 고아객체 제거(orphanRemoval
에 대한 이해가 있으면, JPA에서 엔티티를 지우는 방식에 대해 보다 명확하게 이해할 수 있습니다. 엔티티의 생명주기 관리와 관련된 JPA의 기능 중 하나인 cascade
타입은 부모 엔티티의 생명주기 변경(예: 삭제)이 자식 엔티티에게 전파되는 방식을 제어합니다. 반면, 고아객체(orphanRemoval
) 설정은 부모 엔티티와의 관계가 끊어진 ‘자식’ 엔티티를 자동으로 삭제합니다.
질문에서 언급하신 것처럼 parent.getChildren().remove(child1);
를 통해 컬렉션에서 엔티티를 제거하는 행위는 해당 Child
가 Parent
와의 관계에서 분리된 것을 의미합니다. 이 경우 orphanRemoval=true
설정이 되지 않았다면, 단순히 컬렉션에서 제거된 것일 뿐, 데이터베이스에서 해당 Child
를 자동으로 삭제하지는 않습니다. 따라서 직접 delete
명령을 사용하여 데이터베이스에서도 삭제를 진행해야 합니다. 하지만, orphanRemoval=true
가 설정되어 있다면, 컬렉션에서 Child
를 제거하는 것만으로도 데이터베이스에서 해당 Child
가 삭제되는 명령이 실행됩니다.
Child
엔티티를 삭제하고자 할 때 parent
컬랙션에서 Child
를 먼저 제거하는 것이 매우 중요합니다. 이는 데이터 무결성을 유지하는 데 필수적인 작업 중 하나이며, 동기화 상태를 유지하는 데 도움이 됩니다.
하지만, cascade
옵션이나 orphanRemoval
설정 없이 단순히 ChildRepository.delete(child1);
을 호출하여 Child
를 삭제하는 경우, Parent
와의 관계를 정의한 컬랙션에서 Child
를 먼저 제거하지 않으면, 데이터 무결성 문제나 예상치 못한 결과가 발생할 수 있습니다.
PrivilegeEscalate님의 결론은 JPA를 사용할 때 엔티티 간 관계 및 데이터 무결성을 관리하는 방법에 대해 보다 깊은 이해를 하고 계신 것으로 보입니다. 하지만, 이와 관련된 더 깊은 이해나 궁금증이 있다면, 고아 객체 제거 옵션(orphanRemoval
)과 연관 관계 편의 메서드, JPA 엔티티 매핑, 라이프사이클 콜백 등 JPA의 다양한 기능을 공부해 보시길 권장드립니다.
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.
이런! 관계를 끊어주지 않으면 충돌이 나는군요. 언제나 감사드리고 고생하세요!!