- 최근 진행하던 프로젝트에서 본격적으로 엔티티를 생성하고 매핑하는 과정을 진행하면서, 일대다 단방향 매핑을 고려해야 하는 상황이 왔었다.
- 별 고민없이 매핑하려 했으나, 찾아보니 해당 매핑 방법에 문제가 있는 것 같았다.
- 사실 이전에 김영한님의 강의를 통해 얼핏 듣긴 했지만 실제 겪어보지 않은 상황이라 딱히 중요하게 생각하지 않고 지나쳤던것 같다.
- 따라서 이번에 해당 내용에 대해 제대로 정리해보려고 한다.
💡 One To Many 단방향 매핑의 문제점
- 결론부터 말하자면, One To Many 단방향 매핑의 문제점은 다음과 같다.
- 엔티티가 관리하는 외래키가 다른 테이블에 있다. (Many에 외래키가 존재한다.)
- 따라서 연관관계 관리를 위해 추가로 UPDATE 쿼리가 실행된다.
- 이는 사실 성능상 큰 차이는 없다. 다만, 개발을 하다 보면 B를 만졌는데 A에서도 UPDATE 쿼리가 나가니 혼동이 발생할 수 있다.
- 따라서 필요하다면 트레이드오프(trade-off) 개념으로 일대다보다는 다대일 양방향 관계로 매핑하는 것이 좋다. (B는 A가 필요 없으므로 객체 지향적 관점에서는 손해가 될 수 있음)
- 글로만 보면 이해가 되지 않을 수 있으니 예제와 함께 살펴보자.
📄 One To Many 단방향 매핑
- 우선 아래는 One To Many 단방향 매핑의 예이다.
@Entity
public class Article {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<Image> images = new ArrayList<>();
}
@Entity
public class Image {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String url;
}
- 여기서는
Ariticle
이 One,Image
가 Many에 속한다. - 이후 각각의 엔티티를 저장하려고 하면 다음과 같은 쿼리가 수행되는 것을 확인할 수 있다.
Hibernate: insert into article (id, content) values (null, ?)
Hibernate: insert into image (id, url) values (null, ?)
Hibernate: insert into image (id, url) values (null, ?)
Hibernate: insert into image (id, url) values (null, ?)
Hibernate: insert into image (id, url) values (null, ?)
Hibernate: insert into article_images (article_id, images_id) values (?, ?)
Hibernate: insert into article_images (article_id, images_id) values (?, ?)
Hibernate: insert into article_images (article_id, images_id) values (?, ?)
Hibernate: insert into article_images (article_id, images_id) values (?, ?)
- One To Many 단방향 매핑 관계에서 따로 조인 설정을 넣어주지 않았기 때문에 단방향
@JoinTable
이 적용되며, One To Many에서 JoinTable을 사용하면Article
과Image
엔티티를 저장한 후에 매핑 테이블에 한 번 더 저장한다. - 이때, 하나의 외래키가 아닌 두 개의 외래키가 저장되는데, 이 경우 1:N 관계라기 보다는 N:N 관계처럼 보이며 매우 비효율적이다. 또한 세 개의 테이블이 사용되므로 생각했던 것보다 더 많은 공간을 사용하게 된다.
- 이후
Image
엔티티를 삭제하기 위해ImageRepository.delete()
와 같은 JPA 메서드를 사용하면 다음과 같은 에러가 발생한다.
PUBLIC.ARTICLE_IMAGES FOREIGN KEY(IMAGES_ID) REFERENCES PUBLIC.IMAGE(ID) (1)"; SQL statement:
delete from image where id=? [23503-199]
- 이는
article_images
테이블에서Image
의id
를 외래키로 가지고 있기 때문이며,Image
를 제거하기 위해서는Article
객체의 Image List에서remove()
를 통해 직접 제거해주어야 한다.
Image image = imageRepository.findById(1L).get();
article.getImages().remove(image);
testEntityManager.flush();
- 이후 삭제하는 쿼리문을 살펴보면 다음과 같다.
Hibernate: delete from article_images where article_id=?
Hibernate: insert into article_images (article_id, images_id) values (?, ?)
Hibernate: insert into article_images (article_id, images_id) values (?, ?)
Hibernate: insert into article_images (article_id, images_id) values (?, ?)
Hibernate: delete from image where id=?
- 생각대로라면
article_images
에서 1번,image
에서 1번 이렇게 총 2번의 삭제 쿼리가 실행될줄 알았지만 결과는 의외였다. - 위 쿼리대로라면
article_images
테이블에서article_id
에 따라 해당하는 모든 행을 지우고, 지우려는image
를 제외한 나머지image
들을article_images
에 다시 저장한 후, 최종적으로 지우려는image
를 테이블에서 삭제하는 방식으로 동작하는 것이다. - 위처럼 동작하는 이유는 다음과 같다. (사실 이게 맞는지는 모르겠다만 최대한 이해되는데로 정리해보았다.)
- One To Many 단방향 매핑 관계에서 Many(
image
)는 One(article
)을 모른다. 따라서article_id
를image
의 개별 행에 대한 삭제 조건으로 지정할 수가 없다. - 다만,
article_images
테이블에서는article_id
에 따라 특정 행을 모두 삭제할 수는 있다. 따라서article_id
에 따라 특정 레코드를 모두delete
한 후에 다시insert
를 반복하는 노가다를 하는 것이다.
- One To Many 단방향 매핑 관계에서 Many(
public class Article {
....
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name="article_id")
private List<Image> images = new ArrayList<>();
....
}
- 위처럼
@JoinColumn
을 사용하면, 날아가는 쿼리문이 다음과 같이 변경된다.
Hibernate: insert into article (id, content) values (null, ?)
Hibernate: insert into image (id, url) values (null, ?)
Hibernate: insert into image (id, url) values (null, ?)
Hibernate: insert into image (id, url) values (null, ?)
Hibernate: insert into image (id, url) values (null, ?)
Hibernate: update image set article_id=? where id=?
Hibernate: update image set article_id=? where id=?
Hibernate: update image set article_id=? where id=?
Hibernate: update image set article_id=? where id=?
image
를 DB에 저장할 때,article_id
를 모르기 때문에article
을 먼저 저장한 후update
문을 통해서article_id
를 한 번 더 저장하는 모습이다.- 삭제하는 쿼리문은 다음과 같다.
Hibernate: update image set article_id=null where article_id=? and id=?
Hibernate: delete from image where id=?
💡 One To Many 양방향
- 그렇다면 One To Many 양방향 매핑은 어떨까?
@Entity
public class Article{
@OneToMany(mappedBy = "article",cascade = CascadeType.ALL, orphanRemoval = true)
private List<Image> images = new ArrayList<>();
public void addImage(final Image image) {
images.add(image);
image.setArticle(this);
}
public void removeImage(final Image image){
images.remove(image);
image.setArticle(null);
}
}
@Entity
public class Image {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "article_id")
private Article article;
...
}
- 저장 SQL
Hibernate: insert into article (id, content) values (null, ?)
Hibernate: insert into image (id, article_id, url) values (null, ?, ?)
Hibernate: insert into image (id, article_id, url) values (null, ?, ?)
Hibernate: insert into image (id, article_id, url) values (null, ?, ?)
Hibernate: insert into image (id, article_id, url) values (null, ?, ?)
- 삭제 SQL
Hibernate: delete from image where id=?
- 두 엔티티가 서로를 참조하고 있기 때문에 당연하게도 저장, 삭제 쿼리문 모두 간단한 쿼리만으로 실행되는 것을 확인할 수 있다.
💡 정리
- One To Many 단방향 매핑은 외래키 위치 문제 때문에, UPDATE 쿼리가 또 한번 나가게 된다.
- 성능차이는 미비하나 쿼리를 보았을때 개발자가 헷갈릴 여부가 있기 때문에 단방향 관계의 경우에도 매핑자체는 양방향 매핑관계를 사용한뒤 Many 쪽에 로직을 두지 않는 식으로 개발을 진행하자.
📌 Reference
'🥑 Web Technoloy' 카테고리의 다른 글
CascadeType.REMOVE vs @OnDelete(action = OnDeleteAction.CASCADE) (0) | 2023.07.24 |
---|---|
Vue.js & Spring Boot 연동 및 개발환경 구축 (0) | 2023.06.30 |
@RequestBody vs @ModelAttribute (0) | 2023.06.24 |
더티 체킹(Dirty Checking)이란? (0) | 2023.06.13 |
AJAX란? (0) | 2023.06.13 |