매핑의 방식에는 크게 두 가지가 있다.
- 조인테이블
- 조인컬럼
일대다 단방향 매핑의 가장 큰 문제는 조인테이블에서 나타난다.
일단 일대다 단방향 매핑의 조인테이블 문제를 살펴보자.
일단 설명하기에 앞서서 이 방법은 그냥 하지마 라고 결론 내고 싶다.
일대다 단방향 매핑 조인테이블 방식은 그냥 직관적으로 @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 만 삭제하고 싶다면
- parent_children 10개의 child가 모두 delete 된다.
- parent_children 테이블에서 children_id가 1, 2인 것을 제외한 8개의 레코드에 대해 모두 8회의 insert가 실행된다.
- 마지막에 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 나가서 매우 헷갈린다는 점이다.... 😂😂
웬만하면 일대다 양방향 연관관계를 쓰는게 깔끔하다.
끝!