Files
document-haness/.playwright-mcp/page-2026-09-04T05-50-47-178Z.yml

289 lines
19 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
- generic [ref=f30e3]:
- link "본문으로 건너뛰기" [ref=f30e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f30e5]:
- generic [ref=f30e6]:
- link "TechLog Studio" [ref=f30e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f30e8]: Studio
- navigation "Studio 주 탐색" [ref=f30e10]:
- link "작업본" [ref=f30e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f30e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f30e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f30e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f30e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f30e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f30e17]
- main [ref=f30e18]:
- generic [ref=f30e55]:
- generic [ref=f30e56]:
- generic [ref=f30e57]:
- paragraph [ref=f30e58]: PUBLIC PREVIEW · VERSION 35
- paragraph [ref=f30e59]: Public Preview
- paragraph [ref=f30e60]: Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제
- link "검증 결과 보기" [ref=f30e61] [cursor=pointer]:
- /url: /studio/documents/7ed75172-fd56-42bf-956a-8f9fc1cca235/validation
- region "Public Preview 상태" [ref=f30e62]:
- generic [ref=f30e63]:
- strong [ref=f30e64]: EXPIRED
- paragraph [ref=f30e65]: 이 Public Preview는 만료되었습니다.
- generic [ref=f30e66]:
- link "검증 화면으로 이동" [ref=f30e67] [cursor=pointer]:
- /url: /studio/documents/7ed75172-fd56-42bf-956a-8f9fc1cca235/validation
- link "편집으로 돌아가기" [ref=f30e68] [cursor=pointer]:
- /url: /studio/documents/7ed75172-fd56-42bf-956a-8f9fc1cca235/edit
- generic [ref=f30e69]:
- generic [ref=f30e70]:
- term [ref=f30e71]: Preview 버전
- definition [ref=f30e72]: "33"
- generic [ref=f30e73]:
- term [ref=f30e74]: 생성
- definition [ref=f30e75]: 2026. 9. 1. 오후 2:04
- generic [ref=f30e76]:
- term [ref=f30e77]: 만료
- definition [ref=f30e78]: 2026. 9. 2. 오후 2:04
- generic [ref=f30e80]:
- generic [ref=f30e81]:
- navigation "문서 경로" [ref=f30e82]:
- link "검증 기록" [ref=f30e83] [cursor=pointer]:
- /url: /explore/cases
- generic [aria-hidden] [ref=f30e84]: /
- link "JPA 피드 조회 성능" [ref=f30e85] [cursor=pointer]:
- /url: /topics/jpa-feed-query-performance
- generic [aria-hidden] [ref=f30e86]: /
- link "Liner N + 1문제" [ref=f30e87] [cursor=pointer]:
- /url: /projects/liner-n-plus-1
- heading "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제" [level=1] [ref=f30e88]
- paragraph [ref=f30e89]:
- text: 추가 쿼리를 줄이기 위해 필요한 연관 데이터를
- code [ref=f30e90]: fetch join
- text: 으로 한 번에 조회했다. 하지만 두 컬렉션을 동시에
- code [ref=f30e91]: fetch join
- text: 하자
- code [ref=f30e92]: MultipleBagFetchException
- text: 이 발생했다.
- paragraph [ref=f30e93]:
- text: 컬렉션 하나만
- code [ref=f30e94]: fetch join
- text: 했을 때는 쿼리 수가 줄었지만, 부모와 자식이 조인되면서 조회되는 행 수가 크게 늘었다. 실제 전송 행 수도 생성한 Highlight의 전체 개수만큼 증가했다.
- paragraph [ref=f30e95]:
- code [ref=f30e96]: fetch join
- text: 으로 쿼리 수는 줄일 수 있었지만, 그만큼 DB에서 읽고 애플리케이션에서 처리해야 하는 데이터와 메모리 사용량이 증가했다.
- region "문제와 결론" [ref=f30e97]:
- generic [ref=f30e98]:
- paragraph [ref=f30e99]: 문제
- paragraph [ref=f30e100]:
- code [ref=f30e101]: "@OneToMany"
- text:
- code [ref=f30e102]: "@ManyToOne"
- text: 에서 발생하는 추가 쿼리를 확인한 뒤, 먼저 컬렉션에 대해서
- code [ref=f30e103]: highlights
- text: ","
- code [ref=f30e104]: mentions
- text: 를 모두
- code [ref=f30e105]: fetch join
- text: 해 한 번의 쿼리로 조회해 보았다.
- paragraph [ref=f30e106]: mentions은 user와 feedItem의 다대다의 관계를 1대다와 다대1의 관계로 풀어내면서 나온 컬렉션이다.
- generic [ref=f30e107]:
- paragraph [ref=f30e108]: 결론
- paragraph [ref=f30e109]:
- text:
- code [ref=f30e110]: List
- text: 컬렉션을 동시에
- code [ref=f30e111]: fetch join
- text: 하면
- code [ref=f30e112]: MultipleBagFetchException
- text: 이 발생했다. Hibernate는 순서 컬럼이 없는 두
- code [ref=f30e113]: List
- text: 가 조인되면서
- code [ref=f30e114]: highlights × mentions
- text: 형태로 행이 늘어날 경우, 이 결과를 원래 두 컬렉션으로 정확하게 구성할 수 없기 때문에 쿼리 실행 전에 이를 막는다. 실제 데이터가 없는 상태에서도 같은 예외가 발생했다.
- paragraph [ref=f30e115]:
- code [ref=f30e116]: highlights
- text: 하나만
- code [ref=f30e117]: fetch join
- text: 하면 예외는 발생하지 않았다. 대신 부모인 Feed Item이 Highlight 수만큼 반복되면서 DB에서 전달되는 행 수가 늘어났다.
- paragraph [ref=f30e118]:
- text: Hibernate 6에서는
- code [ref=f30e119]: fetch join
- text: 결과의 루트 엔티티 중복을 제거하기 때문에 최종 목록에는 Feed Item이 N개만 남는다. 따라서 반환된 목록의 크기만 보면 조인으로 행이 얼마나 늘어났는지 알 수 없다.
- paragraph [ref=f30e120]:
- text: N=100에서는 전체 쿼리가 222개에서 121개로 줄었다. 하지만 120개는 여전히
- code [ref=f30e121]: User
- text:
- code [ref=f30e122]: Page
- text: 를 조회하는 추가 쿼리였고,
- code [ref=f30e123]: highlights
- text: 를 가져오는 하나의 조인 쿼리는 1,961행을 전달했다. 즉, 쿼리 수는 줄었지만 실제로 처리하는 데이터까지 같이 줄어든 건 아니었다.
- generic [ref=f30e124]:
- generic [ref=f30e125]:
- term [ref=f30e126]: 검증 환경
- definition [ref=f30e127]:
- paragraph [ref=f30e128]: "Java 21Spring Boot 4.0.0Hibernate ORM 7.1.8.FinalPostgreSQL : postgres:16-alpine (Testcontainers)Query : JPQLUnique Constraint : (feed_item_id, mentioned_user_id)"
- generic [ref=f30e129]:
- term [ref=f30e130]: 검증 데이터
- definition [ref=f30e131]:
- paragraph [ref=f30e132]: 1. highlights와 mentions를 동시에 join fetch하는 JPQL을 실행해 MultipleBagFetchException이 발생하는지 확인한다.
- paragraph [ref=f30e133]: 2. highlights만 fetch join한 뒤 FeedItem을 각각 10개, 100개, 1000개로 늘려가면서 조회한다.
- paragraph [ref=f30e134]: 3. Hibernete가 반환한 FeedItem 수와 실제 조인으로 만들어진 행의 수를 각각 비교해본다.
- paragraph [ref=f30e135]: 4. 같은 쿼리를 EXPLAIN (ANALYZE, BUFFERS)로 실행해 DB에서 실제로 처리한 행 수를 확인한다.
- paragraph [ref=f30e136]: 5. 기존 테스트를 다시 실행해 변경 전 동작이 유지되는지 확인, highlights는 접근할 때 Feed Item 수만큼 조회되고, 접근하지 않으면 추가 조회는 없어야 한다.
- generic [ref=f30e137]:
- term [ref=f30e138]: 기록
- definition [ref=f30e139]: 게시 2026.09.01 · 마지막 검증 2026.09.01
- group [ref=f30e141]:
- generic "목차 · 두 컬렉션을 동시 fetch join" [ref=f30e142] [cursor=pointer]
- article [ref=f30e144]:
- region [ref=f30e145]:
- heading [level=2] [ref=f30e146]:
- link "두 컬렉션을 동시 fetch join 바로가기" [ref=f30e147] [cursor=pointer]:
- /url: "#두-컬렉션을-동시-fetch-join"
- text: 두 컬렉션을 동시 fetch join
- generic [aria-hidden] [ref=f30e148]: "#"
- figure "JAVA LABEL=\"컬렉션 둘을 같이 FETCH JOIN\" ·코드 코드 복사" [ref=f30e149]:
- generic [ref=f30e150]:
- generic [ref=f30e151]: JAVA LABEL="컬렉션 둘을 같이 FETCH JOIN"
- generic [ref=f30e152]: ·코드
- button "코드 복사" [ref=f30e153] [cursor=pointer]: 복사
- region "코드 코드" [ref=f30e154]:
- code [ref=f30e155]: select distinct f from FeedItemJpaEntity f join fetch f.highlights join fetch f.mentions
- figure "TEXT LABEL=\"예외 원인\" ·코드 코드 복사" [ref=f30e157]:
- generic [ref=f30e158]:
- generic [ref=f30e159]: TEXT LABEL="예외 원인"
- generic [ref=f30e160]: ·코드
- button "코드 복사" [ref=f30e161] [cursor=pointer]: 복사
- region "코드 코드" [ref=f30e162]:
- code [ref=f30e163]: java.lang.IllegalArgumentException <- org.hibernate.loader.MultipleBagFetchException
- paragraph [ref=f30e165]: MultipleBagFetchException은 직접 발생하지 않았고 IllegalArgumentException에 감싸진 상태로 전달됐다. 전체 예외의 원인을 따라가면서 어떤 예외인지 확인했고 MultipleBagFetchException 해당 예외가 발생하는 것을 확인할 수 있었다.
- region [ref=f30e166]:
- heading [level=2] [ref=f30e167]:
- link "한 컬렉션만 fetch join 바로가기" [ref=f30e168] [cursor=pointer]:
- /url: "#한-컬렉션만-fetch-join"
- text: 한 컬렉션만 fetch join
- generic [aria-hidden] [ref=f30e169]: "#"
- region "표" [ref=f30e170]:
- table [ref=f30e171]:
- caption [ref=f30e172]
- rowgroup [ref=f30e173]:
- row [ref=f30e174]:
- columnheader "FeedItem 수" [ref=f30e175]
- columnheader "조인된 행 수" [ref=f30e176]
- columnheader "반환된 Feed Item 수" [ref=f30e177]
- columnheader "Highlight 수" [ref=f30e178]
- columnheader "FeedItem 대비 조인 행 수" [ref=f30e179]
- rowgroup [ref=f30e180]:
- row [ref=f30e181]:
- cell "10" [ref=f30e182]
- cell "1,285" [ref=f30e183]
- cell "10" [ref=f30e184]
- cell "1,285" [ref=f30e185]
- cell "128.5×" [ref=f30e186]
- row [ref=f30e187]:
- cell "100" [ref=f30e188]
- cell "1,961" [ref=f30e189]
- cell "100" [ref=f30e190]
- cell "1,961" [ref=f30e191]
- cell "19.6×" [ref=f30e192]
- row [ref=f30e193]:
- cell "1,000" [ref=f30e194]
- cell "2,917" [ref=f30e195]
- cell "1,000" [ref=f30e196]
- cell "2,917" [ref=f30e197]
- cell "2.9×" [ref=f30e198]
- paragraph [ref=f30e199]: highlights를 fetch join하자 조인된 행 수는 Highlight의 전체 개수와 같았다. FeedItem 하나에 Highlight가 여러 개 있으면 같은 FeedItem이 Highlight 수만큼 반복되기 때문이다.
- paragraph [ref=f30e200]: Hibernate는 중복된 Feed Item을 제거해 최종 목록에는 각각 10개, 100개, 1000개만 반환했다. 하지만 DB에서 만들어지는 조인 결과까지 줄어드는 건 아니었다.
- paragraph [ref=f30e201]: FeedItem이 늘어나면서 조인된 행 수는 1285개에서 2917개까지 계속 증가했다.
- region [ref=f30e202]:
- heading [level=2] [ref=f30e203]:
- link "쿼리 수만 보면 개선처럼 보인다 바로가기" [ref=f30e204] [cursor=pointer]:
- /url: "#쿼리-수만-보면-개선처럼-보인다"
- text: 쿼리 수만 보면 개선처럼 보인다
- generic [aria-hidden] [ref=f30e205]: "#"
- region "표" [ref=f30e206]:
- table [ref=f30e207]:
- caption [ref=f30e208]
- rowgroup [ref=f30e209]:
- row [ref=f30e210]:
- columnheader "구분" [ref=f30e211]
- columnheader "기존 조회" [ref=f30e212]
- columnheader "highlights fetch join" [ref=f30e213]
- columnheader "변화" [ref=f30e214]
- rowgroup [ref=f30e215]:
- row [ref=f30e216]:
- cell "Feed Item 조회" [ref=f30e217]
- cell "1" [ref=f30e218]
- cell "1" [ref=f30e219]
- cell "highlights를 같이 조회" [ref=f30e220]
- row [ref=f30e221]:
- cell "count 조회" [ref=f30e222]
- cell "1" [ref=f30e223]
- cell "0" [ref=f30e224]
- cell "JPQL로 조회" [ref=f30e225]
- row [ref=f30e226]:
- cell "Highlight 조회" [ref=f30e227]
- cell "100" [ref=f30e228]
- cell "0" [ref=f30e229]
- cell "개별 조회 제거" [ref=f30e230]
- row [ref=f30e231]:
- cell "User+Page 조회" [ref=f30e232]
- cell "120" [ref=f30e233]
- cell "120" [ref=f30e234]
- cell "변화 없음" [ref=f30e235]
- row [ref=f30e236]:
- cell "전체" [ref=f30e237]
- cell "222" [ref=f30e238]
- cell "121" [ref=f30e239]
- cell "101개 감소" [ref=f30e240]
- paragraph [ref=f30e241]: 전체 쿼리는 222개가 121개로 줄었다. 하지만 User 와 Page를 조회하는 120개의 쿼리는 그대로 남아있어서 n+1은 highlights만 제거된 상태다.
- region [ref=f30e242]:
- heading [level=2] [ref=f30e243]:
- link "조인이 행을 곱하는 것을 실행계획 바로가기" [ref=f30e244] [cursor=pointer]:
- /url: "#조인이-행을-곱하는-것을-실행계획"
- text: 조인이 행을 곱하는 것을 실행계획
- generic [aria-hidden] [ref=f30e245]: "#"
- figure "TEXT LABEL=\"N=100일 때, FETCH JOIN EXPLAIN\" ·코드 코드 복사" [ref=f30e246]:
- generic [ref=f30e247]:
- generic [ref=f30e248]: TEXT LABEL="N=100일 때, FETCH JOIN EXPLAIN"
- generic [ref=f30e249]: ·코드
- button "코드 복사" [ref=f30e250] [cursor=pointer]: 복사
- region "코드 코드" [ref=f30e251]:
- code [ref=f30e252]: "Hash Join (cost=77.18..512.34 rows=4202 width=32) (actual time=0.589..0.894 rows=1961 loops=1) Hash Cond: (h.feed_item_id = fi.id) -> Seq Scan on highlights h (actual ... rows=1961 loops=1) -> Hash (actual ... rows=100 loops=1) -> Seq Scan on feed_items fi (actual ... rows=100 loops=1) Execution Time: 0.959 ms"
- paragraph [ref=f30e254]: Feed Item은 100개가 반환되지만 실행계획에서 조인 결과는 1,961행이었다. 쿼리는 한 번만 실행됐지만, 각 Feed Item이 Highlight 수만큼 반복되면서 실제로 처리하고 전달한 행은 훨씬 많았다.
- paragraph [ref=f30e255]: Hibernate가 중복된 Feed Item을 제거해 최종 목록에는 100개만 남기 때문에 반환된 목록 크기만으로는 실제로 반환되는 행의 수를 알 수 없다.
- paragraph [ref=f30e256]: 실행계획의 예상 행 수는 4,202행이었지만 실제로는 1,961행이었다.
- region [ref=f30e257]:
- paragraph [ref=f30e258]: Next
- heading "다음에 읽을 것" [level=2] [ref=f30e259]
- list [ref=f30e260]:
- listitem [ref=f30e261]:
- link "적용 기준 Fetch Join · Batch · Projection 선택 기준" [ref=f30e262] [cursor=pointer]:
- /url: /references/fetch-strategy-selection
- generic [ref=f30e263]: 적용 기준
- generic [ref=f30e264]:
- strong [ref=f30e265]: Fetch Join · Batch · Projection 선택 기준
- paragraph [aria-hidden] [ref=f30e266]: 이 실패에서 나온 선택 기준이다.
- generic [aria-hidden] [ref=f30e267]:
- listitem [ref=f30e268]:
- link "검증 기록 DTO 변환 과정에서 발생한 Highlight 컬렉션 N+1" [ref=f30e269] [cursor=pointer]:
- /url: /cases/collection-nplus1-dto-mapping
- generic [ref=f30e270]: 검증 기록
- generic [ref=f30e271]:
- strong [ref=f30e272]: DTO 변환 과정에서 발생한 Highlight 컬렉션 N+1
- paragraph [aria-hidden] [ref=f30e273]: 이 시도가 풀려던 문제다.
- generic [aria-hidden] [ref=f30e274]:
- listitem [ref=f30e275]:
- link "검증 기록 Collection Fetch Join Pagination의 In-memory Paging" [ref=f30e276] [cursor=pointer]:
- /url: /cases/collection-fetch-join-in-memory-paging
- generic [ref=f30e277]: 검증 기록
- generic [ref=f30e278]:
- strong [ref=f30e279]: Collection Fetch Join Pagination의 In-memory Paging
- paragraph [aria-hidden] [ref=f30e280]: 한 bag만 fetch join한 상태에서 페이징을 적용한 다음 기록이다.
- generic [aria-hidden] [ref=f30e281]:
- paragraph [ref=f30e54]: 저장된 문서 검증이 완료되었습니다.