[JPA TIL] 일대다 단방향 매핑 함부로 하지 마세요

Hyebin Lee·2021년 12월 28일
0

JPA TIL

목록 보기
2/7
post-thumbnail

매핑의 방식에는 크게 두 가지가 있다.

  1. 조인테이블
  2. 조인컬럼

일대다 단방향 매핑의 가장 큰 문제는 조인테이블에서 나타난다.
일단 일대다 단방향 매핑의 조인테이블 문제를 살펴보자.

일대다 단방향 매핑 조인테이블

일단 설명하기에 앞서서 이 방법은 그냥 하지마 라고 결론 내고 싶다.
일대다 단방향 매핑 조인테이블 방식은 그냥 직관적으로 @OneToMany만 달랑 붙여서 조인하는 방식이다.

@Entity
@Getter
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> children = new ArrayList<>();

    protected Parent() {}

    public Parent(String name) {
        this.name = name;
    }

    public Parent(String name, List<Child> children) {
        this.name = name;
        this.children.addAll(children);
    }
}

@Entity
@Getter
public class Child {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    protected Child() {
    }

    public Child(String name) {
        this.name = name;
    }
}

코드는 다음과 같이 생성될 것이다.

이 방식의 가장 큰 문제점은 insert가 거의 매 순간마다 나간다는 점이고 그것이 엄청난 오버헤드를 불러일으키기 때문이다.

parent_child 테이블이 조인되어 하나 생성되면 일대다 매핑이기 때문에 child_id 행에 따라 단독적으로 행동할 수가 절대 없다.

문제는 여기서 발생한다.
만약 Parent 1 에게 1~10까지의 Child가 있다고 할 때 child 1,2 만 삭제하고 싶다면

  1. parent_children 10개의 child가 모두 delete 된다.
  2. parent_children 테이블에서 children_id가 1, 2인 것을 제외한 8개의 레코드에 대해 모두 8회의 insert가 실행된다.
  3. 마지막에 child 테이블에서 2회의 delete가 실행된다.

다음과 같이 쓸데없는 insert가 실행되어버린다.
원인은 parent_child 테이블에서 각 child 행에 대한 단독적인 행동을 할 수 없기 때문에 할 수 없이 child 1,2만 지우는 것이 아니라 child1,2를 가지고 있는 parent의 모든 child가 삭제되고 다시 insert되는 노가다가 반복된다.

이 문제를 해결하기 위해서는 조인컬럼과 양방향 연관관계 매핑을 할 수 있다.
그러나 일대다 단방향 연관관계 매핑 조인컬럼의 방법에도 문제는 존재한다..두둥

일대다 단방향 매핑 조인컬럼

조인컬럼의 방식은 단순하다. 조인테이블에서는 @OneToMany 만 붙였다면 여기서는 그 밑에 @JoinColumn을 추가해주면 된다.

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "parent_id")  // <-- 여기!!
private List<Child> children = new ArrayList<>();

이 경우 예상치 못하게 초기 insert 단계에서 update가 일어나고 delete 마다 update가 일어난다.

이유는 이번에도 단방향이기 때문이다. 조인컬럼 방식으로 변환하면서 child 테이블에 parent_id 컬럼이 추가되기는 했지만, 단방향이라서 child는 parent의 존재를 모르므로 parent_id의 값을 알 수는 없다.

그래서 개별 행 단위로는 parent_id 컬럼에 값이 없는 채로 insert 되고, insert 된 10개의 행의 parent_id 컬럼에는 dbParent.getChildren()에서 알아낼 수 있는 parent_id 값을 update 를 통해 설정한다. 하지만 이건 최초에 데이터가 세팅될 때 1회만 그런거고, 이렇게 parent_id 값이 저장된 후에는 삭제를 원하는 레코드만 삭제할 수 있게 되므로 조인테이블 방식의 문제는 해결했다고 볼 수 있다.

delete 마다 update가 먼저 이루어지는 이유는 parent 입장에서 child.remove는 단순히 child 데이터가 삭제되는 것이 아니라 parent - child 연관관계를 끊는 것을 의미한다. 따라서 remove 메서드가 실행되면 remove 될 각 child의 parent_id가 null로 변환되지 직접적으로 삭제되지는 않는다. 만약 삭제를 하고 싶으면 cascade 옵션을 사용해야 한다.

성능 상 update 쿼리가 나가는 것은 큰 차이를 일으키지는 않는다.
근데 문제는 이것이 개발할 때 다른 쪽 query 가 update 나가서 매우 헷갈린다는 점이다.... 😂😂

정리하자면

웬만하면 일대다 양방향 연관관계를 쓰는게 깔끔하다.
끝!

0개의 댓글