본문 바로가기
[개발] 프레임워크/Spring

JPA N+1 문제

by Devsong26 2023. 11. 17.

 

N + 1 문제는 처음 실행한 SQL의 결과 수만큼 추가로 SQL을 실행하는 것입니다.

조회 시점에 차이가 있을 뿐 지연로딩과 즉시로딩 모두 발생할 수 있습니다.

 

코드를 통해 확인을 해보겠습니다.

 

데이터베이스 스키마

CREATE TABLE User (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE Order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES User(id)
);

 

JPA Entity 클래스

@Entity
@Table(name = "User")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<Order> orders;
}

@Entity
@Table(name = "Order")
@Data
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private Date orderDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
}

 

외래키를 가진 연관관계의 주인인 order 클래스를 User 클래스에서 mappedBy로 지정합니다.

Order 클래스에서는 JoinColumn에 외래키를 명시합니다.

 

아래는 레파지토리와 테스트 코드입니다.

@Repository
public interface UserRepository extends JpaRepository<User, Long> {}


@ActiveProfiles("dev")
@SpringBootTest
@ExtendWith(SpringExtension.class)
class UserRepositoryTest {

    @Autowired
    UserRepository userRepository;

    @Test
    public void test(){
        List<User> users = userRepository.findAll();

        assertNotNull(users);
    }

}

 

아래는 콘솔 로그로 확인한 N + 1 문제입니다.

2023-11-18T16:41:59.714+09:00 DEBUG 17028 --- [    Test worker] org.hibernate.SQL                        : select u1_0.id,u1_0.name from user u1_0
Hibernate: select u1_0.id,u1_0.name from user u1_0
2023-11-18T16:41:59.806+09:00 DEBUG 17028 --- [    Test worker] org.hibernate.SQL                        : select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id=?
Hibernate: select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id=?
2023-11-18T16:41:59.815+09:00 DEBUG 17028 --- [    Test worker] org.hibernate.SQL                        : select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id=?
Hibernate: select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id=?
2023-11-18T16:41:59.817+09:00 DEBUG 17028 --- [    Test worker] org.hibernate.SQL                        : select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id=?
Hibernate: select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id=?
2023-11-18T16:41:59.820+09:00 DEBUG 17028 --- [    Test worker] org.hibernate.SQL                        : select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id=?
Hibernate: select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id=?

 


 

해결방법

페치 조인 사용 

N + 1 문제를 해결하는 가장 일반적인 방법입니다. 페치 조인은 SQL 조인을 사용해서 연관된 엔티티를 함께 조회하므로 N + 1 문제가 발생하지 않습니다.

만일 일대다 조인으로 인해 중복된 결과가 발생하면 DISTINCT 예약어를 사용해야 합니다.

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    @Override
    @Query("SELECT u FROM User u JOIN FETCH u.orders")
    List<User> findAll();

}

 

2023-11-18T16:54:07.196+09:00 DEBUG 1084 --- [    Test worker] org.hibernate.SQL                        : select u1_0.id,u1_0.name,o1_0.user_id,o1_0.id,o1_0.order_date from user u1_0 join `order` o1_0 on u1_0.id=o1_0.user_id
Hibernate: select u1_0.id,u1_0.name,o1_0.user_id,o1_0.id,o1_0.order_date from user u1_0 join `order` o1_0 on u1_0.id=o1_0.user_id

 

 

하이버네이트 @BatchSzie

BatchSize 어노테이션을 사용하면 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN 절을 사용해서 조회합니다.

총 데이터 개수가 4일 때 BatchSize를 2로 지정하면 즉시 로딩시 조회 쿼리가 2회 수행됩니다.

지연 로딩으로 설정 시 3번째 데이터를 사용하게 될 경우 조회 쿼리를 추가 실행합니다.

@Entity
@Table(name = "User")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @BatchSize(size = 2)
    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<Order> orders;
}

 

2023-11-18T16:58:48.187+09:00 DEBUG 16296 --- [    Test worker] org.hibernate.SQL                        : select u1_0.id,u1_0.name from user u1_0
Hibernate: select u1_0.id,u1_0.name from user u1_0
2023-11-18T16:58:48.311+09:00 DEBUG 16296 --- [    Test worker] org.hibernate.SQL                        : select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id in (?,?)
Hibernate: select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id in (?,?)
2023-11-18T16:58:48.326+09:00 DEBUG 16296 --- [    Test worker] org.hibernate.SQL                        : select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id in (?,?)
Hibernate: select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id in (?,?)

 

 

 

하이버네이트 @Fetch(FetchMode.SUBSELECT)

Fetch 어노테이션에 FetchMode를 SUBSELECT로 사용하면 연관된 데이터를 조회할 때 서브 쿼리를 사용해서 N + 1 문제를 해결합니다.

 

@Entity
@Table(name = "User")
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Fetch(FetchMode.SUBSELECT)
    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
    private List<Order> orders;
}

 

2023-11-18T17:02:51.266+09:00 DEBUG 14940 --- [    Test worker] org.hibernate.SQL                        : select u1_0.id,u1_0.name from user u1_0
Hibernate: select u1_0.id,u1_0.name from user u1_0
2023-11-18T17:02:51.349+09:00 DEBUG 14940 --- [    Test worker] org.hibernate.SQL                        : select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id in(select u1_0.id from user u1_0)
Hibernate: select o1_0.user_id,o1_0.id,o1_0.order_date from `order` o1_0 where o1_0.user_id in(select u1_0.id from user u1_0)

 

 


 

MyBatis를 사용해서 조인쿼리를 많이 사용하다 보니 Fetch Join이 가장 익숙합니다.

 

 


 

참고 도서

- 자바 ORM 표준 JPA 프로그래밍

'[개발] 프레임워크 > Spring' 카테고리의 다른 글

스프링 트랜잭션  (0) 2023.11.26
Feign Client  (2) 2023.11.25
@RestControllerAdvice  (0) 2023.11.16
[Spring F/W] Spring Batch  (0) 2023.10.23
WebFlux  (0) 2023.10.22