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 |