[SPRING] ๐Ÿ…ฝ JPA์˜ N+1๋ฌธ์ œ ํ•ด๊ฒฐํ•˜๊ธฐ - JPQL & @EntityGraph

๋ฆผ๋ฏผ์ง€ยท2025๋…„ 4์›” 20์ผ

Today I Learn

๋ชฉ๋ก ๋ณด๊ธฐ
47/62

โšก๏ธ N+1 ๋ฌธ์ œ

N+1 ๋ฌธ์ œ๋Š” ๋ฌด์—‡์ด๊ณ , ์™œ ๋ฐœ์ƒํ• ๊นŒ??

๐Ÿ” ์ •์˜

: N+1 ๋ฌธ์ œ๋Š” 1๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ N๊ฐœ์˜ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•œ ํ›„, ๊ฐ ์—”ํ‹ฐํ‹ฐ์˜ ์—ฐ๊ด€๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ N๋ฒˆ ์ถ”๊ฐ€ ์ฟผ๋ฆฌ๋กœ ์กฐํšŒํ•˜๋Š” ์ƒํ™ฉ!
โžก๏ธ LAZY๋กœ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋งŒ๋“ค์—ˆ์„ ๋•Œ ๋ฐœ์ƒํ•จ!
(์ด๋ ‡๊ฒŒ ๋งํ•˜๋ฉด ์ดํ•ด๊ฐ€ ์ž˜ ๊ฐ€์ง€ ์•Š์œผ๋‹ˆ,,, ์˜ˆ์‹œ๋กœ ์•Œ์•„๋ณด์ž)

SELECT * FROM todos;
-- Todo๋ฅผ ๋จผ์ € ๊ฐ€์ ธ์˜ค๊ณ (1๋ฒˆ)~ ๊ทธ๋‹ค์Œ์— ๊ทธ์— ๋งž๋Š” ์œ ์ € ๋˜ ์ฐพ์•„์˜ค๊ธฐ(N๋ฒˆ) => N+1
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
...

SQL๋กœ ๋ณด๋ฉด ์กฐ๊ธˆ ๋” ์ดํ•ด๊ฐ€ ๊ฐ„๋‹ค~!

โœ… ์™œ ์ง€์—ฐ ๋กœ๋”ฉ(LAZY)์—์„œ๋งŒ N+1 ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ• ๊นŒ?

๐Ÿ”ธ LAZY ๋กœ๋”ฉ์ด๋ž€?
์Šคํ”„๋ง์„ ์‹คํ–‰ํ•˜์ž๋งˆ์ž ์—ฐ๊ด€๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ฐ”๋กœ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๊ณ ,
์‹ค์ œ๋กœ ๋‚ด๊ฐ€ "์ด๊ฑฐ ์ด์ œ ์ ‘๊ทผํ•ด!" ๋ผ๊ณ  ํ• ๋•Œ DB์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ์‹(๊ทธ๋•Œ๊ทธ๋•Œ ์กฐ๋‹ฌ)

์•„๋ž˜์˜ ์ฝ”๋“œ์ฒ˜๋Ÿผ ์„ค์ •๋˜์–ด์žˆ์„ ๋•Œ ์‹คํ–‰์‹œ์— ๊ฐ€์ ธ์˜ค๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ,
todo.getUser()(์œ ์ € ๊ฐ€์ ธ์™€!)๋ฅผ ์ฒ˜์Œ ํ˜ธ์ถœํ•  ๋•Œ!!! ๋น„๋กœ์†Œ User ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋œ๋‹ค

@ManyToOne(fetch = FetchType.LAZY)
private User user;

โ˜‘๏ธ ์˜ˆ์‹œ๋กœ ์•Œ์•„๋ณด๊ธฐ

์•„๋ž˜์˜ ์ฝ”๋“œ๋Š” Todo๋ฅผ ์กฐํšŒํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค.

public Page<TodoResponse> getTodos(int page, int size) {
        Pageable pageable = PageRequest.of(page - 1, size);

        Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);

        return todos.map(todo -> new TodoResponse(
                todo.getId(),
                todo.getTitle(),
                todo.getContents(),
                todo.getWeather(),
                new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()),
                todo.getCreatedAt(),
                todo.getModifiedAt()
        ));
    }

new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()), ์—ฌ๊ธฐ ์ฝ”๋“œ๋ฅผ ์ž์„ธํžˆ ๋ณด์ž
user๋„ ํ•จ๊ป˜ ๊ฐ€์ ธ์˜ค๊ณ  ์žˆ๋Š” ๋ชจ์Šต์„ ๋ณผ ์ˆ˜ ์žˆ๋”ฐ!

๊ทธ๋Ÿฌ๋ฉด Todo ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์˜ฌ๋–„ user๋Š” ์กฐํšŒ๋˜์ง€ ์•Š์€ ์ƒํƒœ์˜€๊ธฐ ๋•Œ๋ฌธ์—(User๋Š” ์•„์ง ๋กœ๋”ฉx), map() ์•ˆ์—์„œ ์ ‘๊ทผํ•  ๋•Œ๋งˆ๋‹ค N๊ฐœ์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋‚ ์•„๊ฐ€๊ฒŒ ๋œ๋‹ค,,,๐Ÿฅน

์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ JPQL์„ ํ™œ์šฉํ•˜๋ฉด ๋œ๋‹ค!!!
๋‘๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค โžก๏ธ @Query & @EntityGraph


๐Ÿ’“ JPQL ํ™œ์šฉํ•˜๊ธฐ!

JPQL์ด ๋ญ˜๊นŒ?

์•ฝ๊ฐ„ JPQ + SQL๊ตฌ๋ฌธ์œผ๋กœ ์ƒ๊ฐํ•˜๋ฉด ํŽธํ•  ๊ฒƒ ๊ฐ™๋‹ค!

๐Ÿ“Œ JPQL (Java Persistence Query Language)
JPA์—์„œ SQL์ฒ˜๋Ÿผ ์“ฐ๋Š” "๊ฐ์ฒด ์ง€ํ–ฅ ์ฟผ๋ฆฌ ์–ธ์–ด"

์—ฌ๊ธฐ์„œ SQL๊ณผ ์•ฝ๊ฐ„ ๋‹ค๋ฅธ์ ์€,
SQL โ†’ ํ…Œ์ด๋ธ” ๊ธฐ์ค€์œผ๋กœ ์งˆ์˜
JPQL โ†’ ์—”ํ‹ฐํ‹ฐ ๊ธฐ์ค€์œผ๋กœ ์งˆ์˜

์ž ์ด์ œ @Query & @EntityGraph๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐœ์„ ํ•ด๋ณด์ž!

๐Ÿ’ก @Query

MVC ๊ตฌ์กฐ์—์„œ Repository์—์„œ DB๋ฅผ ์กฐํšŒํ• ๋•Œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ,
์˜ˆ์‹œ ์ฝ”๋“œ๋กœ ์•Œ์•„๋ณด์ž

@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")

Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

@Query("SELECT t FROM Todo t " +
       "LEFT JOIN FETCH t.user " + --> ์—ฌ๊ธฐ
       "WHERE t.id = :todoId")
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);

์—ฌ๊ธฐ์„œ LEFT JOIN FETCH t.user๋Š” ๋ฐ”๋กœ N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ์„ ์œ„ํ•œ fetch join!
โžก๏ธ ์ฆ‰, Todo๋ฅผ ์กฐํšŒํ•˜๋ฉด์„œ ๊ฐ Todo์— ์—ฐ๊ฒฐ๋œ User๋„ ํ•œ ๋ฒˆ์— ๊ฐ™์ด ๋กœ๋”ฉ!!!

sql ๊ด€์ ์—์„œ๋Š” ๋‘ ํ…Œ์ด๋ธ”์„ ์กฐ์ธ์‹œ์ผœ์„œ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ๊ณผ ๋น„์Šทํ•˜๋‹ค! => ํ•œ๋ฒˆ์— ์กฐํšŒ

SELECT t.*, u.* 
FROM todos t
JOIN users u ON t.user_id = u.id;

SELECT t FROM Todo t๋Š” ์‹ค์ œ๋กœ๋Š” todos ํ…Œ์ด๋ธ”์ด์ง€๋งŒ,
JPQL์—์„œ๋Š” Todo๋ผ๋Š” ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ฟผ๋ฆฌ ์งœ๋Š” ๊ฒƒ!

๐Ÿ“Œ @EntityGraph๋กœ ๊ฐœ์„ ํ•ด๋ณด๊ธฐ

๐Ÿ“Œ @EntityGraph๋ž€?
LAZY์„ค์ •๋œ ์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๊ฐ™์ด ๋กœ๋”ฉ(Eager์ฒ˜๋Ÿผ!)ํ•˜๊ณ  ์‹ถ์„ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” JPA์˜ ์–ด๋…ธํ…Œ์ด์…˜
๐Ÿ‘‰ JPQL์˜ fetch join๊ณผ ๋˜‘๊ฐ™์€ ๊ธฐ๋Šฅ์„ ์„ ์–ธ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์คŒ!

์•„๊นŒ ์ฝ”๋“œ๋ฅผ JPA์˜ @EntityGraph๋กœ ๊ฐœ์„ ํ•ด๋ณด์ž!

    @EntityGraph(attributePaths = "user")
    Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

    @EntityGraph(attributePaths = {"user"})
    Optional<Todo> findById(@Param("todoId") Long todoId);

์•„๊นŒ ๊ธธ์—ˆ๋˜ ์ฝ”๋“œ๋ฅผ ์ด๋ ‡๊ฒŒ ์งง๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค!

๋ฌผ๋ก  ๋‘๊ฐœ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค

โšก๏ธ JPQL + @EntityGraph ์กฐํ•ฉ
๋งŒ์•ฝ ์กฐ๊ฑด์ด ๋ณต์žกํ•ด์„œ(๋‚ด๋ฆผ์ฐจ์ˆœ, ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์กฐ๊ฑด๋“ค,,) @Query๋ฅผ ์จ์•ผ ํ•œ๋‹ค๋ฉด, ์ด๋ ‡๊ฒŒ๋„ ๊ฐ€๋Šฅ!

@EntityGraph(attributePaths = {"user"})
@Query("SELECT t FROM Todo t ORDER BY t.modifiedAt DESC")
List<Todo> findAllWithUserOrderByModifiedAt();

์ด ๊ฒฝ์šฐ์—๋„ @EntityGraph๊ฐ€ ์ž‘๋™ํ•ด์„œ t.user๋ฅผ ๊ฐ™์ด fetch!

๐Ÿ›’ ์ •๋ฆฌ

์ƒํ™ฉ์— ๋งž์ถฐ์„œ, ์–ด๋–ค ๋ฐฉ์‹์ด ๋” ์˜ฌ๋ฐ”๋ฅผ์ง€ ๋ณด๊ณ  ์„ ํƒํ•˜๋ฉด ๋œ๋‹ค.

๋น„๊ตJPQL โ†’ 'fetch join'JPA โ†’ '@EntityGraph'
์„ ์–ธ ๋ฐฉ์‹์ฟผ๋ฆฌ ์ง์ ‘ ์ž‘์„ฑ์–ด๋…ธํ…Œ์ด์…˜
์œ ์ง€๋ณด์ˆ˜๊ธธ์–ด์งˆ ์ˆ˜ ์žˆ์Œ๊น”๋”ํ•˜๊ณ  ์žฌ์‚ฌ์šฉ ์‰ฌ์›€
๊ธฐ๋Šฅ์™„์ „ํ•œ ์ œ์–ด ๊ฐ€๋Šฅ์„ ์–ธ์ , ๊ฐ„๋‹จํ•จ
์žฅ์ ๋ณต์žกํ•œ ์กฐ๊ฑด ๊ฐ€๋Šฅ์ฝ”๋“œ ๊ฐ„๊ฒฐ

0๊ฐœ์˜ ๋Œ“๊ธ€