N+1 문제란?

의도치 않게 여러개의 Select문이 짧은 시간내에 실행되는 현상

연관관계가 설정된 Entity를 조회할 경우에 조회한 데이터 개수(N)만큼 연관관계의 조회 쿼리가 추가로 발생하는 현상

 

 

예시

출처 : https://programmer93.tistory.com/83

FetchType.EAGER

TEAM과 USER 테이블이 위와 같이 1:N 관계를 가질 때

 

@Entity
public class User {
    @Id
    @GeneratedValue
    private long id;
    private String firstName;
    private String lastName;

    @ManyToOne(fetch = FetchType.EAGER)		// 즉시 로딩
    @JoinColumn(name = "team_id", nullable = false)
    private Team team;
}
@Entity
public class Team {
    @Id
    @GeneratedValue
    private long id;
    private String name;

    @OneToMany(fetch = FetchType.EAGER)
    private List<User> users = new ArrayList<>();
}

@ManyToOne 어노테이션을 활용하여 연관관계 매핑 시, fetch 속성을 FetchType.EAGER (즉시 로딩)으로 설정한 경우

JpaRepository에서 findAll 호출하게 되면

 

Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?

위와 같이 TEAM을 전체 조회하는 쿼리가 발생하고 그 TEAM에 속한 USER를 조회하는 쿼리들이 파생되어 발생

TEAM이 여러개가 된다면?  조회되는 모든 TEAM에 대해 연관된 USER를 하나하나 조회하는 쿼리가 발생

불필요한 쿼리 증가

 

 

FetchType.LAZY

FetchType.EAGER를 FetchType.LAZY로 변경하게 되면

Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_

N+1 문제가 발생하지 않는 점 확인 가능

 

But

Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?

USER를 탐색할 때 N+1문제가 발생

EAGER와 LAZY는 N+1문제가 발생되는 시점만 다름

 

 

 

Why?

 

우선 FetcyType.EAGER 일 때의 동작 과정을 살펴보면

 

1. findAll() 호출 시

select t from Team t 라는 jpql 실행, 해당 구문을 분석한 select * from team 이라는 SQL 생성되어 실행

(Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_)

2. DB의 결과를 받아 team의 Entity 인스턴스 생성

3. team과 연결되어 있는 user도 로딩 필요 >> 영속성 컨텍스트에서 연관된 user가 존재하는 확인

4. 없다면 2에서 만들어진 인스턴스 개수에 맞게 select * from user where team_id=? 라는 SQL생성 후 실행

 

FetchType.LAZY 라면

3, 4번 과정이 전체 객체 조회 시 발생하는 것이 아닌 user 객체를 사용하는 시점에서 발생

** user 객체를 활용하고 싶다 >> user도 로딩이 필요 >> 3, 4번 과정 반복 **

 

 

해결 방법

 

Fetch Join

jpql을 사용해 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 함께 가져오는 방법

(SQL의 Join구문)

 

@Query 어노테이션을 활용하여 직접 jpql 작성 필요

    @Query("select t from Team t join fetch t.users")

 

findAll() 호출 시

Hibernate: select team0_.id as id1_0_0_, user2_.id as id1_2_1_, team0_.name as name2_0_0_, user2_.first_name as first_na2_2_1_, user2_.last_name as last_nam3_2_1_, user2_.team_id as team_id4_2_1_, users1_.team_id as team_id1_1_0__, users1_.users_id as users_id2_1_0__ from team team0_ inner join team_users users1_ on team0_.id=users1_.team_id inner join user user2_ on users1_.users_id=user2_.id

jpql에서 join fetch 구문은 Inner Join 구문으로 변경되어 실행

 

 

EntityGraph

Fetch Join을 jpql이 아닌 어노테이션으로 활용하는 방법

 

      @Override
      @EntityGraph(attributePaths = {"team"})
      List<Member> findAll();

findAll() 메소드를 Ovveride 하고 @EntityGraph 어노테이션의 attributePaths 속성의 값을 join하고 싶은 객체로 설정

 

Fetch Join의 경우 따로 Outer Join을 명시하지 않으면 Inner Join 실행

EntityGraph의 경우 기본적으로 Left Outer Join 실행

아우터 조인이기 때문에 필요 이상의 컬럼이 조회될 가능성 존재

 

 

Batch Size

N+1 문제가 발생했을 경우에 select * from user where team_id in(?, ?, ?) 방식으로 N+1 문제가 발생하는 바법

 

application.yml 설정

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 1000

 

application.properties 설정

spring.jpa.properties.hibernate.default_batch_fetch_size=1000

 

Entity 설정

@OneToMany(fetch = FetchType.EAGER)
@BatchSize(size = 1000)

 

원하는 방법대로 골라서 설정하면 

Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_
Hibernate: select users0_.team_id as team_id1_1_1_, users0_.users_id as users_id2_1_1_, user1_.id as id1_2_0_, user1_.first_name as first_na2_2_0_, user1_.last_name as last_nam3_2_0_, user1_.team_id as team_id4_2_0_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id in (?, ?, ?, ?)

위와 같이 in 을 사용한 쿼리 발생

 

 

 

실무에서?

우선 연관관계애 대한 설정이 필요하다면 FetchType을 LAZY로 사용하고 최적화 필요한 부분에 대해서 Fetch Join 사용

기본적인 Batch Size의 값을 1000 이하로 설정 (대부분 DB에서 In절의 최대 개수가 1000개이기 때문)

 

'끄적 > BE' 카테고리의 다른 글

Servlet  (0) 2023.01.18
JPA vs MyBatis  (0) 2023.01.11
Spring / Spring Boot  (0) 2023.01.09
REST API  (0) 2023.01.09
HTTP  (0) 2023.01.09

JPA 핵심 : Persistence 클래스

 

EntityManager에서 제공하는 API 사용

 

ex)

@Repository
@RequiredArgsConstructor
public class OrderRepository {

    private final EntityManager em;

    public void save(Order order){
        em.persist(order);
    }

    public Order findOne(Long id){
        return em.find(Order.class, id);
    }

    public List<Order> findAll() {
        return em.createQuery("select o from Order o", Order.class)
                .getResultList();
    }
    
    public List<Order> findAllWithItem() {
        return em.createQuery(
                "select distinct o from Order o" +
                        " join fetch o.member m" +
                        " join fetch o.delivery d" +
                        " join fetch o.orderItems oi" +
                        " join fetch oi.item i", Order.class)
                .getResultList();
    }
}

 

위의 OrderRepository는 Spring Data Jpa가 제공하는 JpaRepository를 상속하지 않고 

@Repository만 활용하여 EntityManager가 제공하는 API를 활용하여 CRUD 구현

 

 

Hibernate ??

JPA의 구현체 중 하나

 

 

Hibernate의 SessionFactory, Session, Transaction 을 살펴보면, 

JPA 인터페이스인 EntityManagerFactory, EntityManager, EntityTransaction을 상속받아 구현되어 있음

 

 

 

 

 

Spring Data Jpa

Spring에서 제공하는 모듈 중 하나

개발자가 JPA를 더 쉽고 편하게 사용할 수 있도록 도와주는 라이브러리

JPA를 추상화 시킨 Repository 인터페이스를 제공하여 개발자가 JPA를 더 편하게 사용할 수 있게 하는 모듈

사용자가 Repository 인터페이스에 정해진 규칙대로 메소드 입력 시,

Spring이 알아서 해당 메소드 이름에 적합한 쿼리를 날리는 구현체를 만들어 Bean 으로 등록

 

Spring Data Jpa 사용하지 않는다면, 클래스에 @Repository 어노테이션 작성, EntityManager의 API를 직접 호출해야 함

 

Spring Data Jpa 사용 시, JpaRepository 인터페이스를 상속받아 메소드 명으로만 CRUD 구현 가능

 

 

JpaRepository 메소드 작성 예시

public interface AccountRepository extends JpaRepository<Account, Long> {

    boolean existsByUsername(String username);

    Optional<Account> findById(Long id);

    Optional<Account> findByUsername(String username);

    Optional<Account> findByUsernameAndPassword(String username, String password);
}

 

Repository 인터페이스에 정해진 규칙대로 메소드 입력 시, 해당 메소드에 적합한 JPQL 생성해 처리

 

** 상세한 메소드 명명규칙 참고

https://docs.spring.io/spring-data/jpa/docs/1.10.1.RELEASE/reference/html/#jpa.sample-app.finders.strategies

 

Spring Data JPA - Reference Documentation

Example 11. Repository definitions using Domain Classes with mixed Annotations interface JpaPersonRepository extends Repository { … } interface MongoDBPersonRepository extends Repository { … } @Entity @Document public class Person { … } This example

docs.spring.io

https://docs.spring.io/spring-data/jpa/docs/2.4.3/reference/html/#jpa.query-methods.query-creation

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) public interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") v

docs.spring.io

 

 

 

 

------------------------------------------------------------------------------------------------------------------------------

 

요약하자면

Jpa 활용하기 위해서는 Persistence 의 API (EntityManager), Hibernate와 같은 구현체 활용

 

Spring Data Jpa는 Jpa를 사용하기 쉽게 미리 구현된 인터페이스 (JpaRepository)를 제공

JpaRepository 상속받은 클래스는 메소드 명으로 CRUD 구현 가능

 

'끄적 > BE' 카테고리의 다른 글

Persistence Context (영속성 컨텍스트)  (0) 2023.01.01
Spring MVC  (0) 2022.12.29
JPA  (0) 2022.12.28
Token을 사용한 로그인 인증  (0) 2022.12.28
JWT  (0) 2022.12.28

** spring data jpa 에 관한 설명은 아래 링크 **

https://rainover0824.tistory.com/38

 

JPA, Spring Data Jpa

JPA 핵심 : Persistence 클래스 EntityManager에서 제공하는 API 사용 ex) @Repository @RequiredArgsConstructor public class OrderRepository { private final EntityManager em; public void save(Order order){ em.persist(order); } public Order findOne(

rainover0824.tistory.com

 

 

JPA란?

Java Persistence API 

 

자바 진영에서 ORM 기술 표준으로 사용되는 인터페이스의 모음

어플리케이션과 JDBC 사이에서 동작

 

 

 

 

 

 

ORM?

자바의 객체와 RDB를 매핑

DB의 특정 테이블이 자바 객체로 매핑되어 SQL을 하나하나 작성하지 않고 객체로 구현 가능

어플리케이션의 객체를 RDB 테이블에 자동으로 영속화 해주는 기술

 

 

장점

  • SQL문이 아닌 Method를 통해 DB 조작 가능, 개발자가 객체 모델을 이요하여 로직 구성하는데 집중 가능
  • Query와 같이 필요한 선언문, 할당 등의 부수적 코드 필요 X
  • 객체지향적인 코드 작성 가능 >> 생산성 증가
  • 매핑 정보가 Class로 명시되어있기 때문에 ERD 의존도 낮고 유지보수, 리팩토링에 유리
  • Ex) DB를 변경하는 경우 ORM 사용한다면 쿼리 수정할 필요 X

 

단점

  • 프로젝트 규모가 크고 복잡한 설계일 경우 속도 저하, 일관성을 무너뜨리는 문제가 생길 수 있음
  • 복잡하고 무거운 Query는 속도를 위해 별도 튜닝이 필요, 따라서 결국 SQL을 사용해야 할 수 도 있음

 

 

 

JPA 사용하는 이유?

JPA가 알아서 반복적인 CRUD SQL을 처리

매핑된 관계를 이용해서 SQL을 생성하고 실행

SQL 사용이 필요해지는 경우 Native SQL이라는 기능을 통해 직접 SQL작성도 가능

 

SQL이 아닌 객체 중심으로 개발할 수 있다는 것이 가장 큰 장점

생산성, 유지보수성 증대

 

 

JPA 동작과정

JPA는 어플리케이션과 JDBC 사이에서 동작

개발자가 JPA를 사용하면 JPA내부에서 JDBC API를 사용하여 SQL 호출, DB와 통신

개발자가 직접 JDBC API를 사용 X

 

 

JPA 저장

1. DB에 저장하고 싶은 데이터를 담고 있는 객체를 (ex. 회원 정보를 담고있는 member 객체 같은) JPA에 전달

2. JPA는 객체를 분석, Insert SQL 생성

3. JDBC API 사용하여 SQL을 DB에 전달 

 

 

JPA 조회

1. 조회하고자 하는 객체의 PK값을 JPA에 전달

2. JPA가 엔티티 매핑 정보(@Entity) 를 바탕으로 select SQL 생성

3. JDBC API 사용하여 SQL DB에 전달

4. DB로부터 결과를 전달받고, 전달받은 결과를 객체에 매핑

 

 

 

JPA 수정

 

JPA는 수정 메소드 제공 X

1. 위의 조회 기능을 사용해서 매핑된 객체에 조회된 정보를 담은 후, 매핑된 객체에서 변경하고자 하는 값을 변경

2. 커밋하게 되면 DB에 update SQL 전달

3. 변경된 값 DB 반영

 

** Spring 에서 사용하는 JPA는 JPA를 이용하는 spring-datat-jpa 프레임워크!!

JPA와는 다름

'끄적 > BE' 카테고리의 다른 글

Persistence Context (영속성 컨텍스트)  (0) 2023.01.01
Spring MVC  (0) 2022.12.29
JPA, Spring Data Jpa  (0) 2022.12.28
Token을 사용한 로그인 인증  (0) 2022.12.28
JWT  (0) 2022.12.28

+ Recent posts