일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
- 깊이우선탐색
- 브루트포스
- BFS
- 너비우선탐색
- DynamicProgramming
- 네트워크
- JPA
- DFS
- HashMap
- dynamic programming
- greedy
- 백트래킹
- 해시
- Spring
- 백준
- 해시맵
- 프로그래머스
- Algorithm
- programmers
- Backtracking
- 알고리즘
- 구현
- Network
- 이분탐색
- 부분수열의합
- 그리디
- 스프링
- 동적계획법
- DP
- boj
- Today
- Total
옌의 로그
[JPA] @OneToMany 연관관계, List vs Set 뭐가 더 나을까? 본문
현재 회사에서 진행중인 프로젝트가 api 서버를 먼저 만들고 admin 서버를 추후에(지금ㅋ) 만들고 있는데,
그러다 보니 조회만 할 땐 문제없던 엔티티들이 어드민에서 CRUD를 하려니 JPA를 활용할 수 없는 등 문제가 발생해서(ㅠㅠ)
그 중 고민했던 내용을 포스팅하고자 한다
List로 관리하는 경우
(JPA에서 연관관계를 맺을 때 가장 흔하게 사용하는 방식)
@Entity
public class Author {
@OneToMany(mappedBy = "author")
private List<AuthorEvent> events = new ArrayList<>();
@OneToMany(mappedBy = "author")
private List<AuthorAward> awards = new ArrayList<>();
// ...
}
장점
- 순서가 보장되어 클라이언트에 그대로 응답하거나 순서를 기준으로 로직을 구현할 때 편리
- 대부분의 상황에서 직관적으로 사용 가능
단점
Hibernate의 MultipleBagFetchException
List 타입 컬렉션을 2개 이상 Fetch Join하면 예외 발생
@Override
public Optional<Author> findAuthorById(Long id) {
return Optional.ofNullable(
queryFactory
.selectFrom(author)
.leftJoin(author.events, authorEvent).fetchJoin()
.leftJoin(author.awards, authorAward).fetchJoin()
.where(author.id.eq(id))
.fetchOne()
);
}
위 쿼리처럼 List 형태의 @OneToMany 컬렉션을 여러 개 Fetch Join하면 MultipleBagFetchException이 발생한다.
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
-> Hibernate 내부적으로 Bag 타입(List)을 중복 제거 없이 매핑하려 할 때, 조인 결과가 예상보다 기하급수적으로 증가하기 때문
어떻게 해결?
- Fetch Join은 꼭 필요한(중요한) 연관 객체 1개만 사용
- 나머지 컬렉션은 @BatchSize를 사용하여 지연 로딩 시 N+1 문제 완화
- 아니면 전역으로 설정 : hibernate.default_batch_fetch_size
- 영속성 컨텍스트에서 지연 로딩이 동작할 수 있도록, 조회 메서드는 @Transactional(readOnly = true)로 감싸는 것이 중요
@BatchSize(size = 100)
@OneToMany(mappedBy = "author")
private List<AuthorAward> authorAwards;
DTO 프로젝션
public record AuthorAwardDto(
Long awardId,
String awardName,
LocalDate awardDate
) {}
public List<AuthorAwardDto> findAuthorAwards(Long authorId) {
QAuthor author = QAuthor.author;
QAuthorAward award = QAuthorAward.authorAward;
return queryFactory
.select(Projections.constructor(AuthorAwardDto.class,
award.id,
award.name,
award.awardDate
))
.from(award)
.join(award.author, author)
.where(author.id.eq(authorId))
.fetch();
}
- 필요한 필드만 뽑아서 보내고 싶은 경우
- 네트워크 비용 절감 + 쿼리효율 증가
- 페이징을 사용하는 경우
- fetch join과 Pageable은 Hibernate에서 오류를 발생시킨다
Set으로 관리하는 경우
@OneToMany(mappedBy = "author")
private Set<AuthorEvent> events;
@OneToMany(mappedBy = "author")
private Set<AuthorAward> awards;
Set은 중복을 허용하지 않기 때문에, 객체가 중복되었을 때 문제가 생기지 않도록 하는 데 유리하다
장점
- 중복 제거 기능이 있어 Join 시 중복 데이터 필터링에 유리
- Hibernate 내부적으로는 Bag이 아니라 Set으로 인식되기 때문에 여러 개 Fetch Join 가능
But? 카디널리티 곱...
Fetch Join을 여러 개 사용한다고 해도, 결국 조인된 결과가 곱집합(Cartesian Product) 형태로 나오기 때문에 성능상 불리한 건 마찬가지
-> DB 레벨에서 조인이 중복되어 row수가 기하급수적으로 늘어나는 문제 발생
But? equals/hashCode 지옥...
Set의 핵심은 중복 제거 → JPA는 객체 동등성 비교를 위해 equals/hashCode가 잘 정의되어 있어야 함
@Entity
public class AuthorAward {
@Id @GeneratedValue
private Long id;
private String title;
private int year;
@ManyToOne(fetch = FetchType.LAZY)
private Author author;
// 비즈니스 키 기반 equals/hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AuthorAward)) return false;
AuthorAward that = (AuthorAward) o;
return Objects.equals(title, that.title) &&
year == that.year &&
Objects.equals(author, that.author); // author.id만 비교할 수도 있음
}
@Override
public int hashCode() {
return Objects.hash(title, year, author);
}
}
Author가 영속화되지 않은 경우 author.getId()는 null일 수 있으므로, 비즈니스 키를 기준으로 equals, hashCode를 수정해야한다.
(이걸 toMany 객체들마다 따로따로 구현해줘야한다;;)
마무리하며...
연관관계에서 List냐 Set이냐는 단순한 컬렉션 선택이라 생각했었는데....
Fetch 전략, 성능, 유지보수성, 그리고 JPA의 동작 방식과 깊게 연결되어 있다는걸 깨달았다.
(사실 admin부터 만들었으면 지금 이런 고민도 안했을것 같다. ㅋㅋ)
List + @Transactional(readOnly = true) + @BatchSize 조합으로 정리하고,
Fetch Join은 꼭 필요한 컬렉션 1개만 가져오도록 규칙을 세워 사용하기로 했다
'스터디 > 스프링' 카테고리의 다른 글
Redis 캐시 저장, DTO 직렬화/역직렬화 깨짐 해결기 (3) | 2025.09.12 |
---|---|
[Spring] Swagger에서 ErrorCode Enum 자동화하기 (1) | 2025.08.27 |
[Spring] JPA 연관관계 매핑과 성능 최적화 (Fetch Join vs. Open-in-view) (7) | 2025.08.25 |
[Spring] ApplicationEventPublisher에 대한 고찰 (feat. Fcm push) (3) | 2025.08.08 |
[Spring] 스프링 핵심 원리 기본편 | 1. 객체 지향 설계와 스프링 (0) | 2023.08.24 |