• ABOUT
  • POSTS
  • GUESTBOOK

ยฉ 2025 BlueCool12 All rights reserved.

2025.08.30ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๐Ÿž JPA N+1 ๋ฌธ์ œ - Fetch Join & EntityGraph

[๋ฌธ์ œ ์š”์•ฝ] 

  • ์ฆ์ƒ: ๊ธ€ ๋ชฉ๋ก ํŽ˜์ด์ง€์—์„œ ๊ธ€ ์กฐํšŒ ์‹œ Category๋ฅผ ๊ฑด๋ณ„๋กœ ์ถ”๊ฐ€ ์กฐํšŒ (N+1 ๋ฌธ์ œ ๋ฐœ์ƒ) 
  • ์›์ธ: JPA ์ฟผ๋ฆฌ์—์„œ fetch join์„ ๋ช…์‹œํ•˜์ง€ ์•Š์•„ ๋ฐœ์ƒ 
  • ํ•ด๊ฒฐ: @EntityGraph(attributePaths = โ€œcategoryโ€)๋กœ ํ•œ ๋ฒˆ์— ์กฐํšŒ 

 



N+1 ๋ฌธ์ œ๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

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 ์ฟผ๋ฆฌ๋กœ ๊ฐ€์ ธ์˜ค๋„๋ก ๊ฐ•์ œํ•ด์•ผ ํ•œ๋‹ค.


ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•  

1) JPQL Fetch 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์„ ์„ ํƒํ•˜์˜€๋‹ค. 


2) @EntityGraph

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๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ•˜์˜€๋‹ค. 


์ถ”๊ฐ€) DTO Projection

์œ„์˜ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์œผ๋กœ๋„ ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ์—”ํ‹ฐํ‹ฐ ์ž์ฒด๋ฅผ ์กฐํšŒํ•˜๋Š” ๋Œ€์‹  ํ™”๋ฉด์ด๋‚˜ 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์— ์ž๋™ ๋ฐ˜์˜๋˜์ง€ ์•Š๋Š”๋‹ค.

์ด์ „ ๊ธ€
๐Ÿ”ฃ ๋ฉ”ํƒ€๋ฌธ์ž ์ดํ•ด๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ •๊ทœํ‘œํ˜„์‹
๋‹ค์Œ ๊ธ€
๐ŸŽฎ Controlled์™€ Uncontrolled - ๋ฆฌ์•กํŠธ ํผ ์ปดํฌ๋„ŒํŠธ ์ดํ•ดํ•˜๊ธฐ
์žฅ์‹์šฉ ๋กœ๊ณ