• ABOUT
  • PORTFOLIO
  • POSTS
  • GUESTBOOK

ยฉ 2025 BlueCool12 All rights reserved.

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

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

1. ๋ฌธ์ œ ์ƒํ™ฉ

๋ธ”๋กœ๊ทธ์—์„œ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๊ธฐ ์œ„ํ•ด 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);

์‹ค์ œ๋กœ ์‹คํ–‰๋œ SQL์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

-- 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 ์กฐํšŒ (์ถ”๊ฐ€ ๋ฐ˜๋ณต)
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=?
-- ์ดํ•˜ ํ•„์š”ํ•œ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐœ์ˆ˜๋งŒํผ ๋ฐ˜๋ณต

๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋Š” ์ฟผ๋ฆฌ๊ฐ€ 1ํšŒ ์‹คํ–‰๋œ ํ›„ ๊ฐ ๊ฒŒ์‹œ๊ธ€์— ๋Œ€ํ•ด ์—ฐ๊ด€๋œ category ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ์ฟผ๋ฆฌ๊ฐ€ ๋ฐ˜๋ณต ์‹คํ–‰๋˜์—ˆ๋‹ค. ์ด๋Ÿฌํ•œ ํ˜„์ƒ์„ N+1 ๋ฌธ์ œ๋ผ๊ณ  ํ•œ๋‹ค.


2. N+1 ๋ฌธ์ œ

N+1 ๋ฌธ์ œ๋Š” ์ตœ์ดˆ์˜ ๋‹จ์ผ ์กฐํšŒ ์ฟผ๋ฆฌ(1๋ฒˆ) ์‹คํ–‰ ์ดํ›„ ํ•ด๋‹น ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋กœ ๋ฐ˜ํ™˜๋œ ๋ฐ์ดํ„ฐ์™€ ์—ฐ๊ด€๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐ˜๋ณต์ ์œผ๋กœ ๋ฐœ์ƒํ•˜๋Š” ์„ฑ๋Šฅ ์ €ํ•˜ ํ˜„์ƒ์„ ์˜๋ฏธํ•œ๋‹ค.

JPA ํ™˜๊ฒฝ์—์„œ N+1 ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์ฃผ์š” ์›์ธ์€ JPQL์˜ ์ฟผ๋ฆฌ ๋ณ€ํ™˜ ๋ฐฉ์‹๊ณผ JPA ๊ตฌํ˜„์ฒด์˜ ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์ „๋žต ๊ฐ„์˜ ์ฐจ์ด์— ์žˆ๋‹ค.

JPQL์€ ์—ฐ๊ด€๊ด€๊ณ„ ๋งคํ•‘ ์ •๋ณด๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž๋™์œผ๋กœ ์กฐ์ธ์„ ์ƒ์„ฑํ•˜์ง€ ์•Š์œผ๋ฉฐ ์ž‘์„ฑ๋œ ์ฟผ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ SQL์„ ์ƒ์„ฑํ•œ๋‹ค. ๊ทธ ๊ฒฐ๊ณผ ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š์€ ์ƒํƒœ๋กœ ์ง€์ •๋œ ๋Œ€์ƒ ํ…Œ์ด๋ธ”๋งŒ ์กฐํšŒํ•˜๋Š” 1ํšŒ์˜ SQL์ด ์‹คํ–‰๋œ๋‹ค.

์ดํ›„ JPA ๊ตฌํ˜„์ฒด๋Š” ๋ฐ˜ํ™˜๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ์ €์žฅํ•˜๋Š” ๊ณผ์ •์—์„œ ์—”ํ‹ฐํ‹ฐ ๋งคํ•‘ ์ •๋ณด์™€ Fetch ์ „๋žต์„ ํ™•์ธํ•œ๋‹ค. ์ด๋•Œ ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„์ง ๋กœ๋”ฉ๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์ถ”๊ฐ€์ ์ธ ์กฐํšŒ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

Fetch ์ „๋žต์ด ์ฆ‰์‹œ ๋กœ๋”ฉ(EAGER)์œผ๋กœ ์„ค์ •๋œ ๊ฒฝ์šฐ์—๋Š” ์—ฐ๊ด€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฆ‰์‹œ ์กฐํšŒํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€ ๋‹จ๊ฑด ์กฐํšŒ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋˜๋ฉฐ ์ง€์—ฐ ๋กœ๋”ฉ(LAZY)์œผ๋กœ ์„ค์ •๋œ ๊ฒฝ์šฐ์—๋„ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ์— ์‹ค์ œ ์ ‘๊ทผํ•˜๋Š” ์‹œ์ ์— ๊ฐœ๋ณ„ ์กฐํšŒ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

์ด๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์ถ”๊ฐ€ ์ฟผ๋ฆฌ์˜ ํšŸ์ˆ˜๋Š” ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์˜ ์ƒํƒœ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง„๋‹ค. JPA๋Š” ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ ์กฐํšŒ ์‹œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ฟผ๋ฆฌ๋ฅผ ์ „์†กํ•˜๊ธฐ ์ „ 1์ฐจ ์บ์‹œ๋ฅผ ๋จผ์ € ํ™•์ธํ•˜๋ฏ€๋กœ ์—ฌ๋Ÿฌ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋™์ผํ•œ ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ฐธ์กฐํ•  ๊ฒฝ์šฐ ์บ์‹œ๋œ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜์—ฌ ์ฟผ๋ฆฌ ์‹คํ–‰์„ ์ƒ๋žตํ•œ๋‹ค.

์ฆ‰ ์‹ค์ œ ์ถ”๊ฐ€ ์ฟผ๋ฆฌ๋Š” ์ฐธ์กฐํ•˜๋Š” ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ์˜ ๊ณ ์œ ํ•œ ๊ฐœ์ˆ˜๋งŒํผ๋งŒ ์‹คํ–‰๋˜๊ณ  ์ตœ์•…์˜ ๊ฒฝ์šฐ N๋ฒˆ์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.


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

1) JPQL Fetch Join (๋ช…์‹œ์  ์กฐ์ธ)

JOIN FETCH๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด SQL ๋ ˆ๋ฒจ์—์„œ ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ•จ๊ป˜ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์–ด N+1 ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

@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

@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

์œ„์˜ ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ• ์™ธ์—๋„ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•˜์ง€ ์•Š๊ณ  ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ 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์˜ ์ „์ฒด ํŒจํ‚ค์ง€ ๊ฒฝ๋กœ๋ฅผ ๋ช…์‹œํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋…์„ฑ์ด ๋‹ค์†Œ ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ ์กฐํšŒ๋œ ๊ฒฐ๊ณผ๋Š” ์˜์† ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ’์„ ์ˆ˜์ •ํ•˜๋”๋ผ๋„ JPA์˜ ๋ณ€๊ฒฝ ๊ฐ์ง€(Dirty Checking)๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.

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