인프런 커뮤니티 질문&답변

PrivilegeEscalate님의 프로필 이미지

작성한 질문수

실전! 스프링 데이터 JPA

프로젝트 생성

양방향 @OneToMany 에서의 단순 delete(Instance) 에 대해 질문있습니다.

해결된 질문

24.07.08 14:55 작성

·

144

·

수정됨

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

김영한님의 프로필 이미지
김영한
지식공유자

2024. 07. 10. 20:14

안녕하세요. PrivilegeEscalate님

다음 질문을 참고해주세요 🙂

https://www.inflearn.com/questions/56718

감사합니다.

PrivilegeEscalate님의 프로필 이미지

2024. 07. 10. 23:13

이런! 관계를 끊어주지 않으면 충돌이 나는군요. 언제나 감사드리고 고생하세요!!

0

인프런 AI 인턴님의 프로필 이미지

2024. 07. 08. 15:01

안녕하세요, 인프런 AI 인턴입니다. PrivilegeEscalate님의 질문에 대해 답변을 제공하려고 합니다.

답변으로 제공드릴 내용은 “단순 delete(Instance) 처리에 대한 동작 방식 이해”와 관련된 내용입니다. @OneToMany 양방향 매핑 시 parent.getChildren().remove(child1);의 호출 여부가 해당 Child 엔티티의 delete 쿼리를 결정한다는 부분에 대한 근본적인 이해가 필요해 보입니다.

CascadeType과 고아객체 제거(orphanRemoval에 대한 이해가 있으면, JPA에서 엔티티를 지우는 방식에 대해 보다 명확하게 이해할 수 있습니다. 엔티티의 생명주기 관리와 관련된 JPA의 기능 중 하나인 cascade 타입은 부모 엔티티의 생명주기 변경(예: 삭제)이 자식 엔티티에게 전파되는 방식을 제어합니다. 반면, 고아객체(orphanRemoval) 설정은 부모 엔티티와의 관계가 끊어진 ‘자식’ 엔티티를 자동으로 삭제합니다.

질문에서 언급하신 것처럼 parent.getChildren().remove(child1);를 통해 컬렉션에서 엔티티를 제거하는 행위는 해당 ChildParent와의 관계에서 분리된 것을 의미합니다. 이 경우 orphanRemoval=true 설정이 되지 않았다면, 단순히 컬렉션에서 제거된 것일 뿐, 데이터베이스에서 해당 Child를 자동으로 삭제하지는 않습니다. 따라서 직접 delete 명령을 사용하여 데이터베이스에서도 삭제를 진행해야 합니다. 하지만, orphanRemoval=true가 설정되어 있다면, 컬렉션에서 Child를 제거하는 것만으로도 데이터베이스에서 해당 Child가 삭제되는 명령이 실행됩니다.

Child 엔티티를 삭제하고자 할 때 parent 컬랙션에서 Child를 먼저 제거하는 것이 매우 중요합니다. 이는 데이터 무결성을 유지하는 데 필수적인 작업 중 하나이며, 동기화 상태를 유지하는 데 도움이 됩니다.

하지만, cascade 옵션이나 orphanRemoval 설정 없이 단순히 ChildRepository.delete(child1);을 호출하여 Child를 삭제하는 경우, Parent와의 관계를 정의한 컬랙션에서 Child를 먼저 제거하지 않으면, 데이터 무결성 문제나 예상치 못한 결과가 발생할 수 있습니다.

PrivilegeEscalate님의 결론은 JPA를 사용할 때 엔티티 간 관계 및 데이터 무결성을 관리하는 방법에 대해 보다 깊은 이해를 하고 계신 것으로 보입니다. 하지만, 이와 관련된 더 깊은 이해나 궁금증이 있다면, 고아 객체 제거 옵션(orphanRemoval)과 연관 관계 편의 메서드, JPA 엔티티 매핑, 라이프사이클 콜백 등 JPA의 다양한 기능을 공부해 보시길 권장드립니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.