Notice
Recent Posts
Recent Comments
Link
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

che01 님의 블로그

JPA N+1 문제 완벽 해결하기 - EntityGraph를 활용한 성능 최적화 본문

Spring

JPA N+1 문제 완벽 해결하기 - EntityGraph를 활용한 성능 최적화

che01 2025. 6. 10. 19:41

JPA를 사용하면서 가장 자주 마주치는 성능 이슈 중 하나가 바로 N+1 문제입니다. 오늘은 N+1 문제가 무엇인지, 왜 발생하는지, 그리고 EntityGraph를 활용해서 어떻게 해결할 수 있는지 자세히 알아보겠습니다.

N+1 문제란?

N+1 문제는 JPA에서 연관관계가 있는 엔티티를 조회할 때 발생하는 성능 이슈입니다.

문제 발생 패턴:

  1. 첫 번째 쿼리로 N개의 메인 데이터를 조회 (1번의 쿼리)
  2. 각 메인 데이터의 연관된 엔티티를 가져오기 위해 추가로 N번의 쿼리 실행
  3. 총 1 + N번의 쿼리가 실행되어 "N+1 문제"라고 불림

실제 예시로 N+1 문제 이해하기

엔티티 구조

@Entity
public class Todo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String title;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    
    // getter, setter...
}

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private String email;
    
    // getter, setter...
}

N+1 문제 발생 상황

@Service
public class TodoService {
    @Autowired
    private TodoRepository todoRepository;
    
    public List<TodoDto> getAllTodos() {
        List<Todo> todos = todoRepository.findAll(); // 1번의 쿼리
        
        return todos.stream()
                .map(todo -> new TodoDto(
                    todo.getId(),
                    todo.getTitle(),
                    todo.getUser().getName() // 각 Todo마다 User 조회 (N번의 쿼리)
                ))
                .collect(Collectors.toList());
    }
}

실행되는 SQL 쿼리

-- 1. 모든 Todo 조회 (1번의 쿼리)
SELECT id, title, user_id FROM todo;

-- 2. 각 Todo의 User 조회 (N번의 쿼리)
SELECT id, name, email FROM user WHERE id = 1;
SELECT id, name, email FROM user WHERE id = 2;
SELECT id, name, email FROM user WHERE id = 3;
SELECT id, name, email FROM user WHERE id = 4;
SELECT id, name, email FROM user WHERE id = 5;
-- ... Todo 개수만큼 반복

문제점:

  • Todo가 100개라면 총 101번의 쿼리가 실행됨
  • 데이터베이스 부하 증가
  • 응답 시간 급격히 증가
  • 네트워크 오버헤드 발생

해결 방법 1: EntityGraph 활용

기본 EntityGraph 적용

@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
    
    @EntityGraph(attributePaths = "user")
    List<Todo> findAll();
    
    @EntityGraph(attributePaths = "user")
    Optional<Todo> findById(Long id);
}

실행되는 SQL 쿼리 (개선 후)

-- 하나의 조인 쿼리로 모든 데이터 조회
SELECT 
    t.id, t.title, t.user_id,
    u.id, u.name, u.email
FROM todo t 
LEFT JOIN user u ON t.user_id = u.id;

개선 효과:

  • 101번의 쿼리 → 1번의 쿼리로 감소
  • 성능 대폭 향상
  • 네트워크 오버헤드 최소화

복잡한 연관관계에서의 EntityGraph

@Entity
public class Todo {
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;
    
    @OneToMany(mappedBy = "todo", fetch = FetchType.LAZY)
    private List<Comment> comments;
}

@Entity
public class Comment {
    @ManyToOne(fetch = FetchType.LAZY)
    private User author;
}
@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
    
    // 중첩된 연관관계도 한 번에 조회
    @EntityGraph(attributePaths = {"user", "comments", "comments.author"})
    List<Todo> findAllWithDetails();
    
    // 필요에 따라 부분적으로만 조회
    @EntityGraph(attributePaths = {"user", "comments"})
    List<Todo> findAllWithUserAndComments();
}

해결 방법 2: Named EntityGraph

엔티티에 Named EntityGraph 정의

@Entity
@NamedEntityGraph(
    name = "Todo.withUser",
    attributeNodes = @NamedAttributeNode("user")
)
@NamedEntityGraph(
    name = "Todo.withUserAndComments",
    attributeNodes = {
        @NamedAttributeNode("user"),
        @NamedAttributeNode(value = "comments", subgraph = "comments.author")
    },
    subgraphs = @NamedSubgraph(
        name = "comments.author",
        attributeNodes = @NamedAttributeNode("author")
    )
)
public class Todo {
    // 엔티티 필드들...
}

Repository에서 Named EntityGraph 사용

@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
    
    @EntityGraph("Todo.withUser")
    List<Todo> findAll();
    
    @EntityGraph("Todo.withUserAndComments")
    List<Todo> findAllWithDetails();
}

JPQL Fetch Join과의 비교

JPQL Fetch Join 방식

@Repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
    
    @Query("SELECT t FROM Todo t JOIN FETCH t.user")
    List<Todo> findAllWithUser();
    
    @Query("SELECT DISTINCT t FROM Todo t " +
           "JOIN FETCH t.user " +
           "LEFT JOIN FETCH t.comments c " +
           "LEFT JOIN FETCH c.author")
    List<Todo> findAllWithDetails();
}

EntityGraph vs JPQL Fetch Join

구분 EntityGraph JPQL Fetch Join

가독성 선언적, 직관적 JPQL 쿼리 작성 필요
재사용성 Named EntityGraph로 재사용 가능 메서드별로 쿼리 작성
복잡도 간단한 어노테이션 복잡한 JPQL 작성
유연성 제한적 세밀한 제어 가능
성능 비슷함 비슷함

정리

N+1 문제는 JPA를 사용할 때 반드시 고려해야 할 성능 이슈입니다. EntityGraph를 적절히 활용하면:

  • 성능 향상: N+1번의 쿼리를 1번으로 줄일 수 있음
  • 개발 효율성: 선언적 방식으로 간단하게 적용 가능
  • 유지보수성: Named EntityGraph로 재사용성 향상

다만 모든 상황에서 EntityGraph가 최선은 아니므로, 데이터의 특성과 사용 패턴을 고려하여 적절한 전략을 선택하는 것이 중요합니다. 항상 실제 쿼리 로그를 확인하고 성능을 측정하여 최적화 효과를 검증해야 합니다.

'Spring' 카테고리의 다른 글

트랜잭션 격리수준 (Transaction Isolation Levels)  (0) 2025.07.01
Self-invocation Problem  (0) 2025.06.24
JPA 상속관계 매핑 전략 완전 정리  (2) 2025.06.09
JPA N:M 관계 완전 정리  (0) 2025.06.09
JPA 1:1 관계 완전 정리  (0) 2025.06.09