N+1 ๋ฌธ์ ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์กฐํ ์ 1๋ฒ์ ์ฟผ๋ฆฌ๋ก N๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์๋๋ฐ ์ฐ๊ด๋ ๋ฐ์ดํฐ๋ฅผ ์ฐธ์กฐํ๊ธฐ ์ํด ์ถ๊ฐ๋ก N๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ๋ ์คํ๋๋ ํ์์ ๋งํ๋ค.
JPA(Hibernate)๋ ๊ฐ์ฒด ์งํฅ์ ์ธ ๊ด์ ์์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ค. Post๋ฅผ ์กฐํํ ๋ ์ฐ๊ด๋ Category๊ฐ ์๋ค๋ฉด JPA๋ ์ฐ์ Post ๋ชฉ๋ก์ ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ค. (1๋ฒ)
๊ทธ ํ ๊ฐ Post ๊ฐ์ฒด ๋ด์ Category๋ฅผ ์ฌ์ฉํ๋ ค ํ ๋(๋๋ EAGER ์ค์ ์ ์ํด) ๊ฐ๊ฐ์ Post๊ฐ ๊ฐ์ง category_id๋ฅผ ์กฐ๊ฑด์ผ๋ก ๊ฐ๋ณ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๊ฒ ๋๋ค. (N๋ฒ)
๊ธฐ์กด์๋ Post ๋ชฉ๋ก์ ์กฐํํ๊ธฐ ์ํด JpaRepository์์ ์๋์ ๊ฐ์ JPQL ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ค.
@Query("""
SELECT p FROM Post p
WHERE p.status = :status
ORDER BY p.createdAt DESC, p.id DESC
""")
Slice<Post> findPublishedPosts(@Param("status") PostStatus status, Pageable pageable);
์ค์ ์คํ๋ ์ฟผ๋ฆฌ๋ ์๋์ ๊ฐ๋ค.
-- 1. Post ๋ชฉ๋ก ์กฐํ (1ํ)
Hibernate:
select
p1_0.id,
p1_0.category_id,
p1_0.content,
-- ๋ฑ๋ฑ Post ํ๋๋ค
from
post p1_0
where
p1_0.status=?
order by
p1_0.created_at desc,
p1_0.id desc
fetch
first ? rows only
-- 2. ๊ฐ Post์ Category ์กฐํ (Nํ ๋ฐ๋ณต)
Hibernate:
select
c1_0.id,
c1_0.created_at,
-- ๋ฑ๋ฑ Category ํ๋๋ค
from
category c1_0
where
c1_0.id=?
Hibernate:
select
c1_0.id,
c1_0.created_at,
-- ๋ฑ๋ฑ Category ํ๋๋ค
from
category c1_0
where
c1_0.id=?
-- ์ดํ ์นดํ ๊ณ ๋ฆฌ ๊ฐ์๋งํผ ๋ฐ๋ณต
@ManyToOne์ ๊ธฐ๋ณธ ์ ๋ต์ EAGER(์ฆ์ ๋ก๋ฉ)์ด์ง๋ง JPQL์ ์ฐ๊ด๊ด๊ณ๋ฅผ ๋ฌด์ํ๊ณ ์์ฑ๋ SQL์ ๊ทธ๋๋ก ์คํํ๋ค.
์ฆ JPA๋ ์ฐ์ SELECT p FROM Post p ๋ผ๋ JPQL์ ํด์ํด post ํ
์ด๋ธ๋ง ์กฐํํ๋ SQL์ ๋จผ์ ๋ ๋ฆฐ ํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ Post ์ํฐํฐ๋ฅผ ํตํด Category๊ฐ EAGER๋ก ์ค์ ๋์ด ์๋ ๊ฒ์ ํ์ธํ๋ค.
์ดํ JPA๋ EAGER(์ฆ์ ๋ก๋ฉ)๋ผ๊ณ ํ๋จํ๊ณ ๊ฐ์ ธ์จ ๊ฐ Post์ ๋ด๊ธด category_id๋ฅผ ์ด์ฉํด category ํ
์ด๋ธ์ ๊ฑด๋ณ๋ก ๋ค์ ์กฐํํ๋ค. (N+1)
๋ํ ์ค๋ฌด์์๋ ๋ณดํต ๋ชจ๋ ์ฐ๊ด๊ด๊ณ์ ๋ํด FetchType.LAZY (์ง์ฐ ๋ก๋ฉ)๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ค์ ํ๋ ๊ฒ์ด ๊ด๋ก์ด์ ๊ถ์ฅ ์ฌํญ์ด๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ๋ณ๋์ ์ฟผ๋ฆฌ๊ฐ ์๋ ํ ๋ฒ์ JOIN ์ฟผ๋ฆฌ๋ก ๊ฐ์ ธ์ค๋๋ก ๊ฐ์ ํด์ผ ํ๋ค.
JOIN FETCH๋ฅผ ์ฌ์ฉํ์ฌ SQL ๋ ๋ฒจ์์ ์กฐ์ธ์ ์ํํ๋ค.
@Query("""
SELECT p FROM Post p
LEFT JOIN FETCH p.category
WHERE p.status = :status
ORDER BY p.createdAt DESC, p.id DESC
""")
Slice<Post> findPublishedPosts(@Param("status") PostStatus status, Pageable pageable);
* category๊ฐ nullable์ด๊ธฐ ๋๋ฌธ์ LEFT JOIN์ ์ ํํ์๋ค.
JPA์์ ์ ๊ณตํ๋ ์ด๋
ธํ
์ด์
์ผ๋ก ์ฟผ๋ฆฌ ๋ก์ง์ ์ ์งํ๋ฉด์ Fetch ์ ๋ต๋ง ์์ ํ๋ค.
@EntityGraph(attributePaths = { "category" })
@Query("""
SELECT p FROM Post p
WHERE p.status = :status
ORDER BY p.createdAt DESC, p.id DESC
""")
Slice<Post> findPublishedPosts(@Param("status") PostStatus status, Pageable pageable);
๋ ๋ฐฉ๋ฒ ๋ชจ๋ left join์ด ํฌํจ๋ ๋จ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ์กฐํ๋๋ค.
Hibernate:
select
p1_0.id,
c1_0.id,
c1_0.created_at,
-- ๋ฑ๋ฑ ํ๋๋ค
from
post p1_0
left join
category c1_0
on c1_0.id=p1_0.category_id
where
p1_0.status=?
order by
p1_0.created_at desc,
p1_0.id desc
fetch join์ ๋ช
์์ ์ผ๋ก @Query ์์ ์์ฑํ๋ ๊ฒฝ์ฐ ์กฐ์ธ ํ์
๊ณผ ์กฐ๊ฑด์ ๋ช
ํํ๊ฒ ์ ์ดํ ์ ์๋ค๋ ์ ์ด ์ฅ์ ์ด์ง๋ง ํ์์ ์์๋ ๊ธ ๋ชฉ๋ก ์กฐํ ์ฟผ๋ฆฌ๊ฐ ๋ณต์กํ์ง ์๊ธฐ ๋๋ฌธ์ ๊ฐ๋
์ฑ์ด ์ข์ @EntityGraph๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์ ํ์๋ค.
์์ ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก๋ ํด๊ฒฐ ๊ฐ๋ฅํ์ง๋ง ์ํฐํฐ ์์ฒด๋ฅผ ์กฐํํ๋ ๋์ ํ๋ฉด์ด๋ API ์๋ต์ ํ์ํ ํ๋๋ค๋ง DTO๋ก ์ง์ ์กฐํํ๋ DTO Projection๋ ๊ฐ๋ฅํ๋ค.
@Query("""
SELECT new com.example.project.dto.PostResponse(
p.id,
p.title,
c.name
)
FROM Post p
LEFT JOIN p.category c
WHERE p.status = :status
""")
Slice<PostResponse> findPublishedPostDtos(
@Param("status") PostStatus status, Pageable pageable
);
์ฒ์๋ถํฐ ํ์ํ ํ๋๋ง JOINํด์ ๊ฐ์ ธ์ค๋ฏ๋ก N+1์ด ๋ฐ์ํ ์ฌ์ง๊ฐ ์๋ค. ๋ํ SELECT * ์ด ์๋๋ผ SELECT p.id, p.title... ์ฒ๋ผ ํ์ํ ์ปฌ๋ผ๋ง ๋ช
์ํ๋ฏ๋ก DB์ ์ ํ๋ฆฌ์ผ์ด์
์ฌ์ด์ ๋ฐ์ดํฐ ์ ์ก๋์ด ์ค์ด๋ ๋ค.
๋ค๋ง JPQL์์ DTO์ ์ ์ฒด ํจํค์ง ๊ฒฝ๋ก๋ฅผ ์ ์ด์ผ ํด์ ๊ฐ๋
์ฑ์ด ๋จ์ด์ง ์ ์๊ณ ์กฐํ๋ ๊ฒฐ๊ณผ๊ฐ ์์์ํ(Managed)๊ฐ ์๋๋ฏ๋ก ๋ฐ์ดํฐ๋ฅผ ์์ ํด๋ DB์ ์๋ ๋ฐ์๋์ง ์๋๋ค.
