JPA를 하면서 연관관계에 대해 공부하던 중 접하게 된 One To Many
장점과 단점이 있었지만 솔직히 장점보다는 단점이 더 많은 방법처럼 느껴졌습니다.
우선 장점으로는 관리의 주체가 도메인 스러워지고 Pk위주의 설계인점 이 있었고
단점으로는 단방향 참조이기에 페이징 , N+1 등 다양한 문제점들이 있었습니다.
😉그럼 좀더 깊~~~ 숙하게 One To Many를 알아보겠습니다.
😒맵핑 테이블?
우선 One To Many 는 기본적으로 중간에 맵핑 테이블을 생성합니다 하지만 이런 방식이 불편하게 느껴지기에 저는 강제적으로 맵핑 테이블이 생성되지 못하도록 처리해서 코드를 작성했습니다.
@Entity
@Table(name = "t_board")
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@ToString
public class Board extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer bno;
@Column(length = 200, nullable = false)
private String title;
private String content;
private String writer;
public void changeTitle(String title){
this.title = title;
}
public void changeContent(String content){
this.content = content;
}
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "board")
@Builder.Default //자료구조를 반환할때 null값을 체크안해주게 만들어주기
private Set<BoardImage> boardImages = new HashSet<>();
//이미지를 추가
public void addImage(BoardImage boardImage){
//처음에 들어갈때 size가 0번으로 들어간다
boardImage.fixOrd(boardImages.size());
boardImages.add(boardImage);
}
}
@OneToMany(fetch = FetchType.LAZY) , @JoinColumn(name = "board") 이 2가지를 설정을 안 해주고 OneToMany를 사용하면 맵핑 테이블이 만들어진다 그걸 사용하지 않으려면 이렇게 추가적인 설정을 해주어야 한다.
😱❗❗ 에러 발생!!! “object references an unsaved transient instance”
테스트를 진행하려던 중 “object references an unsaved transient instance” 가 발생했다.
진행하던 테스트 코드는 이렇다.
@Test
public void testInsertWithImage() {
for (int i = 0; i < 20; i++) {
Board board = Board.builder()
.title("fileTest.." +i)
.content("fileTest")
.writer("user" + (i % 10))
.build();
//게시물 하나당 이미지 2개씩 추가
for (int j = 0; j < 2; j++) {
BoardImage boardImage = BoardImage.builder()
.uuid(UUID.randomUUID().toString())
.fileName(i+"aaa.jpg")
.img(true)
.build();
board.addImage(boardImage); //관리의 주체가 board기 때문에 board에 추가해주어야한다
}//image
repository.save(board);
}
}
- 이 테스트를 할 때 Board <Entity>에 boardImages에 “cascade = CascadeType.ALL” 설정이 없다면 “object references an unsaved transient instance” 이 에러가 발생한다.
- 이 에러는 @OneToMany나 @ManyToMany인 상황에서 흔히 만나는 에러이다. 부모 객체에서 자식 객체를 바인딩하여 한번에 저장하려는데 자식 객체가 아직 데이터 베이스에 저장되지 않았기 때문에 발생한다.
- 해결 방안 = 부모 객체에 cascade 옵션을 추가하면 된다.
- cascade = CascadeType.ALL을 하면 되는데 이거만 해서는 안 먹힐 때가 있다.. 그럴 때는 자식 객체도 똑같이 cascade 옵션을 추가해주면 해결!
- CascadeType에는 PERSIST, MERGE, REMOVE, REFRESH, DETACH, ALL이 있는데 자세한 사항은 따로 찾아보자
Entity를 수정한 모습
package org.zerock.b2.entity;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "t_board")
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@ToString
public class Board extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //Auto_Increment 기능
private Integer bno;
//컬럼의 사이즈 지정해주고 not null 지정
@Column(length = 200, nullable = false)
private String title;
private String content;
private String writer;
public void changeTitle(String title){
this.title = title;
}
public void changeContent(String content){
this.content = content;
}
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "board")
@Builder.Default //자료구조를 반환할때 null값을 체크안해주게 만들어주기
private Set<BoardImage> boardImages = new HashSet<>();
//이미지를 추가
public void addImage(BoardImage boardImage){
//처음에 들어갈때 size가 0번으로 들어간다
boardImage.fixOrd(boardImages.size());
boardImages.add(boardImage);
}
}
위에서 설명한 것처럼 “cascade = CascadeType.ALL”를 추가해 주었다.
그리고 새롭게 orphanRemoval = true 추가해 주었는데 이걸 추가해준 이유는 Board 즉 <Entity>가 삭제된다면 이미지도 삭제되어야 하기는 데 orphanRemoval = true 이 코드가 없으면 부모 entity 가 없는데 자식 entity 가 있는 일이 생길 수 있다.
직접 만난 N+1....... 아후....🤯
우선 N+1을 만나게 된 테스트 코드는 이러하다.
@Transactional
@Test
public void testPageImage(){
//1페이지를 가져오고 Sort 로 역순으로 가져온다
Pageable pageable = PageRequest.of(0,10 , Sort.by("bno").descending());
//findAll 로 Board 를 가져온다 , 검색조건이 없는 상태로 목록을 가져온다
Page<Board> result = repository.findAll(pageable);
//게시물을 로그로 찍어보자
result.getContent().forEach(board -> {
log.info(board);
});
}
결과
이 테스트에서 중요한 점
- 연관관계가 복잡할 때 사용하는 여러 방법 fetchJoin 등등 사용할 때 가장 중요한 것은 Limit 가 걸리는지 확인을 해야 한다 테스트가 성공해도 Limit 가 안 걸리면 데이터가 100만 건이면 100만 개를 다 불러온다는 이야기이다 Limit 가 없으면 성공하나 마나 DB가 죽는다.
- 쿼리를 보면 from으로 t_bimage을 select를 하는 걸 볼 수 있는데 게시물마다 이미지 테이블을 전부 찍는 것이다
- 이러면 쿼리가 목록 가져오는 쿼리가 실행되고 카운트 쿼리가 실행되고 게시물 하나당 이미지 쿼리가 실행된다 이런 문제가 N+1이다 이러면 시스템은 바로 죽는다…
One To Many에서 만난 N+1 절망적이었습니다.
이걸 해결하기 위에 EntityGraph 나 BatchSize 등 다양한 방법을 사용해서 해결하긴 했지만 만족스럽지는 못 했다.
강제적으로 해결하는 방식이어서 좋은 방법이라고는 말하지 못할 것 같다.
One To Many는 사용해보니 현재 진행 중인 프로젝트에는 적합한 방법은 아닌 것 같다.
하지만 이 코드들을 작성하면서 배운 점이 많아 그 점이 장점처럼 느껴지는 것 같습니다.
One To Many 가 아닌 ElementCollection 방법으로 다시 만들어 봐야겠습니다.
'고민해 보기' 카테고리의 다른 글
Map 이 아닌 객체를 사용하는 이유 (0) | 2022.08.22 |
---|