์ค์ ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ๋ง๋ฌ๋ N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ณผ์ ์ ์ค์ฌ์ผ๋ก ์์ฑํด ๋ณด์์ต๋๋ค.
โ N+1 ๋ฌธ์ ๋?
: ์์ฒญ์ด 1๊ฐ์ ์ฟผ๋ฆฌ๋ก ์ฒ๋ฆฌ๋๊ธธ ๊ธฐ๋ํ๋๋ฐ ์ถ๊ฐ๋ก N๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ํ์
๊ฐ๋ ์ ์ ๋ง ๊ฐ๋จํฉ๋๋ค.
N๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์กฐํํ๋ ์ฟผ๋ฆฌ 1๊ฐ๋ฅผ ๋ ๋ ธ๋๋ฐ, ์ถ๊ฐ์ ์ผ๋ก N๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ๋ ๋ฐ์ํ๋ ํ์์ ๋๋ค.
(์ฌ์ค ํ๋ฆ์ 1+N ๋ฌธ์ ๋ผ๊ณ ํ๋ ๊ฒ์ด ๋ ์ ์ ํฉ๋๋ค)
์ด์ ์ค์ ํ๋ก์ ํธ ์ํฉ์ ์ค์ฌ์ผ๋ก `N+1` ๋ฌธ์ ๊ฐ ์ธ์ , ์ด๋ค ์ํฉ์์ ๋ฐ์ํ๋์ง ์์๋ณด๊ฒ ์ต๋๋ค.
โ ํ๋ก์ ํธ ์ํฉ
๐ ERD
"ํ๋ก์ ํธ ํ์์ ๋ชจ์งํ๋ ๊ธฐ๋ฅ"์ ๊ตฌํํ๊ธฐ ์ํด ์๋์ ๊ฐ์ด `ERD`๋ฅผ ์ค๊ณํ์ต๋๋ค.
ํ์ ๋ชจ์ง ๊ฒ์๋ฌผ์๋ ์ ํธํ๋ ์ฑ๊ฒฉ, ๋ชจ์งํ๋ ๋ถ์ผ, ๋ชจ์งํ๋ ์ธ์ด๋ฅผ ์ฌ๋ฌ ๊ฐ ์ค์ ํ ์ ์์ต๋๋ค.
๋ฐ๋ผ์ ๊ฐ๊ฐ 1:N์ ๊ด๊ณ๋ก ์ฐ๊ฒฐํ์์ต๋๋ค.
- ํ์ ๋ชจ์ง ๊ฒ์๋ฌผ: ์ ํธ ์ฑ๊ฒฉ = `1:N`
- ํ์ ๋ชจ์ง ๊ฒ์๋ฌผ: ๋ถ์ผ = `1:N`
- ํ์ ๋ชจ์ง ๊ฒ์๋ฌผ: ์ธ์ด = `1:N`
๐ Entity
์ ERD์ ๋ฐ๋ผ ํ์ ๋ชจ์ง ๊ฒ์๋ฌผ(RecruitPost) ์ํฐํฐ๋ ๋ค์๊ณผ ๊ฐ์ด ๊ตฌํํ์ต๋๋ค.

๋ก์ง ๊ตฌํ ๊ณผ์ ์์ `RecruitPost` -> `RecruitField`, `RecruitLanguage`, `RecruitPersonality` ๋ฐฉํฅ์ด ์์ฃผ ํ์ํ๋ค๊ณ ์๊ฐ๋์ด ์๋ฐฉํฅ์ผ๋ก ์ค์ ํ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ๊ฐ๊ฐ์ ๊ด๊ณ๋ ์ง์ฐ ๋ก๋ฉ(`FetchType.LAZY`)์ผ๋ก ์ค์ ๋์ด ์๋ ์ํ์ ๋๋ค.
์ง์ฐ ๋ก๋ฉ: ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ์ฒ์์๋ ์กฐํํ์ง ์๊ณ , ์ค์ ๋ก ํด๋น ์ํฐํฐ๊ฐ ํ์ํ ์์ ์ ์กฐํํ๋ ๋ฐฉ์
๐ Service & Repository
`N+1` ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๋น์ฆ๋์ค ๋ก์ง์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์ฌ์ฉ์์๊ฒ ์ ์ ํ ํ์ ๋ชจ์ง ๊ฒ์๋ฌผ์ ์ถ์ฒํด ์ฃผ๋ ๋ก์ง์ ๋๋ค.
๋จผ์ JpaRepository์ `findAllByIsCompleteAndUserIdNot(false, user)`๋ฅผ ํตํด
ํน์ ์กฐ๊ฑด์ ๋ง์กฑํ๋ ๋ชจ๋ ํ์ ๋ชจ์ง ๊ฒ์๋ฌผ ๋ฆฌ์คํธ๋ฅผ ์กฐํํด์ค๊ณ ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ `calculatePriorities()`์์ ์ด ๊ฒฐ๊ณผ ๋ฆฌ์คํธ๋ฅผ ์ํํ๋ฉฐ ์ฐ๊ด๋ ์ํฐํฐ๋ค(์ธ์ด, ๊ด์ฌ๋ถ์ผ, ์ฑ๊ฒฉ)์ `get`ํ๊ณ ์์ต๋๋ค.
โ ํธ๋ฌ๋ธ ์ํ
๐ ๋ก์ง ์คํ ๊ฒฐ๊ณผ
์ ๋ก์ง์ ์คํํ๋ฉด, ์ฐํ๋ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ ์๋์ ๊ฐ์ต๋๋ค.
์ฌ๊ธฐ์ `N`์ ํ์ฌ DB์ ๋ค์ด์๋ ํ์ ๋ชจ์ง ๊ฒ์๋ฌผ RecruitPost์ ๊ฐ์์ ๋๋ค.
`RecruitPost` ๋ฆฌ์คํธ๋ฅผ ์กฐํํ๋ ์ฟผ๋ฆฌ 1๋ฒ + ๋ฆฌ์คํธ๋ฅผ ์ํํ๋ฉด์ ๊ฐ `RecruitPost`์ ์ฐ๊ด๋ ์ธ์ด, ๋ถ์ผ, ์ฑ๊ฒฉ์ ์กฐํํ๋ ์ฟผ๋ฆฌ N๋ฒ
= `1+N`
= `N+1`
์ ๊ฐ JpaRepository์ ํจ์๋ฅผ ํธ์ถํ ๊ฒ์ 1๋ฒ์์๋, ์ถ๊ฐ๋ก N๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ๋ ๋ฐ์ํ ๊ฒ์ ์ ์ ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด ์ํฉ์ด ๋ฐ๋ก N+1 ๋ฌธ์ ์ ๋๋ค.
๐ N+1 ๋ฌธ์ ๋ฐ์
๋น์ฆ๋์ค ๋ก์ง์ ์ด๋ค ๋ถ๋ถ์์ ์ด ์ฟผ๋ฆฌ๋ค์ด ๋ฐ์ํ๋์ง ์ดํด๋ณด๋ฉด ์์ ๊ฐ์ต๋๋ค.
ํ์ฌ `RecruitPost`์ ์ฐ๊ด๋ ์ํฐํฐ๊ฐ 3๊ฐ(์ธ์ด, ๋ถ์ผ, ์ฑ๊ฒฉ)์ด๊ธฐ ๋๋ฌธ์ ์ ํํ๊ฒ๋ `1+3N`๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ ๊ฒ์ ๋๋ค.
๐์์ธ ๋ถ์
JPQL: ์ํฐํฐ ๊ฐ์ฒด๋ฅผ ์กฐํํ๋ ๊ฐ์ฒด์งํฅ ์ฟผ๋ฆฌ
Spring Data Jpa์์๋ ๋ฉ์๋์ ์ด๋ฆ์ ๋ถ์ํด์ `JPQL`์ ์์ฑํ์ฌ ์คํํฉ๋๋ค.
์๋ ๋ฉ์๋๋ช ์ ํตํด WHERE ์กฐ๊ฑด์ ์ ๋ง๋ค๊ณ , `FROM RecruitPost`๋ก ๊ฒ์๋ฌผ์ ๊ฐ์ ธ์ฌ ๊ฒ์ ๋๋ค.
๊ทธ๋ฐ๋ฐ, ์ด์ ์ ๋ดค๋ `RecruitPost` ํด๋์ค ์์๋ `RecruitField`, `RecruitLanguage`, `RecruitPersonality`์ ๋ฆฌ์คํธ๊ฐ ์๋ฐฉํฅ์ผ๋ก ์ ์ธ๋์ด ์์ต๋๋ค.
๊ทธ๋ผ ์ด ๋ฆฌ์คํธ๋ค๋ ์ ๋ฉ์๋๋ฅผ ์คํํ๋ฉด ๊ฐ์ด ์กฐํํด ์ฌ๊น์?
์ ๋ฆฌ์คํธ๋ค์ RecruitPost ๋ฆฌ์คํธ๋ฅผ ์กฐํํด ์ฌ ๋ ํจ๊ป ์ค์ DB์์ ์กฐํํด์ค์ง ์์ต๋๋ค.
์๋ํ๋ฉด "์ง์ฐ ๋ก๋ฉ"์ผ๋ก ์ค์ ๋์ด ์๊ธฐ ๋๋ฌธ์ธ๋ฐ์!
์ง์ฐ ๋ก๋ฉ์ ์ฌ์ฉํ๋ฉด RecruitPost๋ฅผ ์กฐํํ ๋๋ ๋ฆฌ์คํธ๋ค์ด ํ๋ก์ ๊ฐ์ฒด๋ก ์กด์ฌํ๊ฒ ๋ฉ๋๋ค.
์ดํ ํด๋น ๋ฆฌ์คํธ์ ์ ๊ทผํ๊ธฐ ์ํด post.getXXX() ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฉด, ๊ทธ ์์ ์ ์ค์ ๊ฐ์ฒด๋ฅผ ์กฐํํ๊ธฐ ์ํ ์ฟผ๋ฆฌ๊ฐ ์คํ๋ฉ๋๋ค.
โ ํด๊ฒฐ ๊ณผ์
์ด์ ํ๋ํ๋ ํด๊ฒฐ ๊ณผ์ ์ ๋๋ค.
๊ณผ๊ฑฐ ๊น์ํ ๋ ๊ฐ์์์ N+1 ๋ฌธ์ ์ ๋ํด ํ์ตํ ๊ฒฝํ์ด ์๊ธฐ์, ๊ธ๋ฐฉ ํด๊ฒฐ๋ ์ค ์์์ง๋ง ๊ทธ๋ ์ง ์์์ต๋๋ค..
๐ JPQL์ FETCH JOIN ์ฌ์ฉ & @EntityGraph ์ฌ์ฉ
`FETCH JOIN`์ ์ฌ์ฉํ๋ฉด ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ๊ฐ์ด ์กฐํํ ์ ์์ต๋๋ค.
๊ทธ๋์ ์๋์ ๊ฐ์ด ๊ด๋ จ๋ 3๊ฐ์ ์ํฐํฐ์ ๋ชจ๋ `FETCH JOIN`์ ์ฌ์ฉํด์ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ๊ฐ์ด ์กฐํํด์ค๋ ค ํ์ต๋๋ค.
๊ทธ๋ฌ๋, ์คํํด ๋ณด๋ "Hibernate MultipleBagFetchException"์ด ๋ฐ์ํ์ต๋๋ค.
์ฒ์์๋ `JPQL`์ `FETCH JOIN`์์๋ง ๋ฐ์ํ๋ ์์ธ์ธ๊ฐ? ์๊ฐํ๊ณ `@EntityGraph`๋ก ๋ฐฉ์์ ๋ณ๊ฒฝํด ๋ณด์์ต๋๋ค.
`@EntityGraph`๋ฅผ ์ฌ์ฉํ๋ฉด ์ง์ฐ ๋ก๋ฉ์ผ๋ก ์ค์ ๋ ์ฐ๊ด๊ด๊ณ ์ํฐํฐ๋ค์ ํ ๋ฒ์ (Left Join์ผ๋ก) ์กฐํํด ์ต๋๋ค.
๋ง์ฐฌ๊ฐ์ง๋ก "Hibernate MultipleBagFetchException"์ด ๋ฐ์ํ์ต๋๋ค.
๐ MultipleBagFetchException
: 2๊ฐ ์ด์์ `OneToMany` ์์ ํ ์ด๋ธ์ `FETCH JOIN`ํ์ ๋ ๋ฐ์ํ๋ ์์ธ
์ ๊ฐ OneToMany ๊ด๊ณ๋ฅผ ๋ชจ๋ List๋ก ์ ์ธํด ๋์๋๋ฐ, Hibernate๋ ์ด๋ฅผ `Bag`๋ก ์ทจ๊ธํฉ๋๋ค.
๋ ๊ฐ ์ด์์ `Bag`๋ฅผ ํจ๊ป `Fetch Join`ํ๋ฉด, ์ค๋ณต ๋ฐ์ดํฐ๊ฐ ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ Hibernate๋ ์ด ์ค๋ณต ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง ๊ฒฐ์ ํ ์๊ฐ ์๊ธฐ ๋๋ฌธ์, MultipleBagFetchException์ ๋ฐ์์ํค๋ ๊ฒ์ ๋๋ค.
์ฆ, 2๊ฐ ์ด์์ `@OneToMany` ๊ด๊ณ์์๋ fetch join์ ์ฌ์ฉํ๋ฉด ๋ฐ์ํ๋ ๋ฌธ์ ์ ๋๋ค!
์ด ์์ธ๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์ผ๋ก๋ 2๊ฐ์ง๋ฅผ ์ฐพ์ ์ ์์์ต๋๋ค.
(1) List๋ฅผ Set์ผ๋ก ๋ณ๊ฒฝ
(2) fetch size ์ค์
๐ MultipleBagFetchException ํด๊ฒฐ: List -> Set ๋ณ๊ฒฝ
`List`๋ ์ค๋ณต์ ๊ฐ๋ฅ์ฑ์ด ์์์ง๋ง, `Set`์ ์ค๋ณต ๋ฐ์ดํฐ ๋ฐ์ํ์ง ์์ต๋๋ค.
๋ฐ๋ผ์ ์ค๋ณต ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ ์ง ๊ณ ๋ฏผํ์ง ์์๋ ๋๋ฏ๋ก ์๋ฌ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค.
๋ค์๊ณผ ๊ฐ์ด `RecruitPost` ์ํฐํฐ ๋ด๋ถ์์ ๋ชจ๋ `Set`์ผ๋ก ๋ณ๊ฒฝํ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์คํํด ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ฟผ๋ฆฌ๊ฐ ์ฐํ๋๋ค.
์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ํตํด `Fetch Join`ํ์ฌ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ์ฐ๊ด๋ ์ํฐํฐ ๋ชจ๋ ์กฐํํด ์ค๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.
ํ์ง๋ง Set์ผ๋ก ๋ณ๊ฒฝํจ์ ๋ฐ๋ผ ์ด๋ฏธ ์์ฑํด ๋ ๋ก์ง์์ ์ฌ๋ฌ ๋ถ๋ถ์ ์์ ํด์ผ ํ๋ ์ํฉ์ด ๋ฐ์ํ์ต๋๋ค.
๐ MultipleBagFetchException ํด๊ฒฐ: batch size ์ค์
batch size: ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ๋ช ๊ฐ์ฉ ๊ฐ์ ธ์ฌ์ง ์ค์ (IN์ ์ ๋ค์ด๊ฐ ๊ฐ ๊ฐ์๋ฅผ ์ค์ )
batch size๋ฅผ ์ ์ฉํ๋ฉด, WHERE์ ์ด ๊ฐ์ ์ฌ๋ฌ ๊ฐ์ SELECT ์ฟผ๋ฆฌ๋ค์ ํ๋์ IN ์ฟผ๋ฆฌ๋ก ๋ง๋ค์ด์ค๋๋ค.
์ฌ๊ธฐ์ ์ด๋ ค์ด ์ ์ batch size๋ ์ํฉ์ ์ ์ ํ๊ฒ ์ค์ ํด์ผ ํ๋ค๋ ๊ฒ์ ๋๋ค.
๋ณดํต 100~1000์ผ๋ก ์ค์ ํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ด๋ผ๊ณ ๋ณด๊ธด ํ์ต๋๋ค.
ํ์ง๋ง, ๋ ์ฐพ์๋ณด๋ batch size ์บ์ฑ ์ผ์ด์ค ์ต์ ํ๋ก ์ธํด (batch size!= IN์ ์ ๋ค์ด๊ฐ ๊ฐ ๊ฐ์)๋ผ๊ณ ํฉ๋๋ค.
์ด ๋ถ๋ถ์ ์ถํ์ ๋ ์ฐพ์์ ๋ฐ๋ก ๊ณต๋ถํด์ผ๊ฒ ์ต๋๋ค.
batch size๋ฅผ ์ค์ ํ๋ ๋ฐฉ๋ฒ์๋ ๋ ๊ฐ์ง๊ฐ ์์ต๋๋ค.
1. ํ๋ก์ ํธ ์ ์ญ์ ๋ฐฐ์น ์ฌ์ด์ฆ ์ ์ฉ
2. ๊ฐ๊ฐ ๋ฐฐ์น ์ฌ์ด์ฆ ์ ์ฉ
โ ํด๊ฒฐ๋ฐฉ๋ฒ ์ ๋ฆฌ
๐ OneToMany ๊ด๊ณ๊ฐ ํ๋์ผ ๋
1. JPQL Fetch Join
2. @EntityGraph
3. Querydsl Fetch Join (Querydsl์์๋ `fetchJoin()`์ ์ด์ฉํ ์ ์์ต๋๋ค)
=> ํ์ง๋ง, ์ฐ๊ด๋ ๊ฒ์ด 2๊ฐ ์ด์์ด๋ฉด ์ 3 ๋ฐฉ์ ๋ชจ๋ MultipleBagFetchException ๋ฐ์
๐ OneToMany ๊ด๊ณ๊ฐ 2๊ฐ ์ด์์ผ ๋
1. List -> Set ๋ณ๊ฒฝ
2. Batch Size ์ค์
๐ ๊ฐ์ธ์ ๊ฒฐ๋ก
- Fetch Join์ ๋ฌธ์ ์ : ํ์ด์ง์ด ์ ๋๊ณ , OneToMany๊ด๊ณ์์๋ ๋ฐ์ดํฐ๊ฐ ์นดํ ์์ ๊ณฑ๋งํผ ๋์ด๋์ ์กฐํ๋๋ค
- ๋ฐ๋ผ์ OneToMany ๊ด๊ณ์์์ N+1 ๋ฌธ์ ์์๋ ์ฐ๊ด๊ด๊ณ๊ฐ ํ ๊ฐ์ด๋ ๋ ๊ฐ์ด๋ ํ์ด์ง์ด ๊ฐ๋ฅํ๊ณ ์ต์ ์ฑ๋ฅ์ ๋ณด์ฅํด ์ฃผ๋ batch size๋ฅผ ์ฌ์ฉํ์
์นดํ ์์ ๊ณฑ (Cartesian Product): N๊ฐ์ ํ์ ๊ฐ์ง ํ ์ด๋ธ๊ณผ M๊ฐ์ ํ์ ๊ฐ์ง ํ ์ด๋ธ์ Join ํ๋ฉด, N*M๊ฐ์ ๊ฒฐ๊ด๊ฐ์ด ๋ฐํ๋๋ ๊ฒ
'๐ฏ Programming' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[์คํ๋ง] ์คํ๋ง ์ด๋ฒคํธ๋ ์ธ์ ์จ์ผํ ๊น? (2) | 2025.04.02 |
---|---|
[ISSUE] error: connect econnrefused (1) | 2025.02.17 |
[JAVA] ์ค๋ณต ์ ๊ฑฐ ๋ฐฉ๋ฒ(+์ ๋ ฌ) (1) | 2025.01.31 |
[Github Action] build ์ค๋ฅ ์์ธ ๋ ์์ธํ ๋ณด๋ ๋ฒ (0) | 2024.10.08 |