📌 QueryDsl과 fetchJoin()에 대한 자세한 개념들은 아래 포스팅을 참고해주세요.
프로젝트에서 N+1 문제가 발생해서 fetchJoin()
을 사용하여 이를 해결하는 과정에서,
yata 게시물에 신청한 yataMember 중, yata 등록자에게 수락되어 차를 타기로 확정된 yataMember를 계산해서 아래와 같이 보여줄 필요가 있었다.
yata와 yataMember는 1:N 관계로 연관관계가 맺어져 있고,
@OneToMany
애너테이션의 경우 카테시안 곱 문제나
여러개의 fetchJoin을 사용할 경우 발생하는 MulipleBagFetchException
을 고려해야한다.
➜ N개의 행을 가진 테이블과 M개의 행을 가진 테이블이 Join된 경우, N*M개의 결과값이 산출
Ex. A 테이블에 요소가 3가지 있고, B 테이블에도 요소가 3가지 있다고 할 경우,
( 각각 A_id, B_id가 3개씩인 경우 )SELECT * FROM A LEFT OUTER JOIN B ON 1 = 1;
위와 같은 쿼리를 적었다고 가정한다면 쿼리 실행 시에 3*3개인 9개의 결과가 나온다고 한다.
👉 즉, 카테시안 곱은 join 쿼리 중에 WHERE 절에 기술하는 join 조건이 잘못 기술되었거나 아예 없을 경우 발생하는 현상
한번의 쿼리로 yataMember를 조회할 때, 그와 연관관계가 있는 yata까지 한번에 조회할 수 있다.
fetchType이 Lazy로 설정되어있어도 fetchJoin이 우선순위를 가지기 때문에 한번의 조회로 연관된 엔티티를 같이 조회한다.
ManyToOne
관계에서는 fetchJoin을 쓰면 N+1 문제를 해결할 수 있다.
❗❗ But, OneToMany
관계에서는 데이터가 카테시안 곱만큼 늘어나서 조회된다는 것이다.
⠀
우리 프로젝트의 예를 들어,
아래와 같이 Yata와 YataMember 테이블이 있다고 할 때,
SELECT *
FROM Yata A LEFT OUTER JOIN
YataMember B ON 1 = 1
위와 같은 쿼리로 YataMember와 Join된 Yata 테이블을 조회할 경우,
YATA 테이블과 YATAMEMBER 테이블의 값이 중복되어
결국 3 * 6 = 18개의 결과값이 조회되는 카테시안 곱 현상이 발생한다.
이 문제를 해결하기 위해, 자료의 중복을 제거하는 두가지 방법을 일반적으로 사용한다.
1. 컬렉션 Set 사용 (중복 허용 X)
2. distinct()
사용
⠀
우리 프로젝트에서는 YataMapper
클래스에서 yataMemeber를 얻어올 때 Distinct를 사용해 중복을 제거해주어 문제를 해결하였다.
yataMapper
클래스 default YataDto.Response yataToYataResponse(Yata yata) { if (yata == null) { return null; } ⠀ YataDto.Response.ResponseBuilder response = YataDto.Response.builder(); ⠀ if (yata.getYataId() != null) { response.yataId(yata.getYataId()); } if (yata.getYataMembers() == null) response.reservedMemberNum(0); else response.reservedMemberNum(yata.getYataMembers().stream().distinct().mapToInt(YataMember::getBoardingPersonCount).sum()); //중복제거 . . . return response.build(); } ⠀ default List<YataDto.AcceptedResponse> yataToMyYatas(List<Yata> yatas , String email){ if (yatas == null) { return null; } return yatas.stream().map(yata -> { YataDto.AcceptedResponse response = new YataDto.AcceptedResponse(); response.setYataResponse(yataToYataResponse(yata)); response.getYataResponse().setYataMembers(null); YataMember yataMember = yata.getYataMembers() .stream().distinct() // 중복 제거 .filter(yataMember1 -> yataMember1.getMember().getEmail().equals(email)).findFirst().orElse(null); response.setYataMemberId(yataMember != null ? yataMember.getYataMemberId() : null); . . . return response; }).collect(Collectors.toList()); }
💬 도움이 된 사이트