티스토리 뷰

자바와 스프링에 대한 기본 지식을 기르기 위해 토이 프로젝트를 시작했습니다.

 

토이 프로젝트로 배우는 자바 스프링 [0]. prologue

자바와 스프링에 대한 기본 지식을 기르기 위해 토이 프로젝트를 시작했습니다. 프론트 코드 : https://github.com/laboratory-kkoon9/connector_front GitHub - laboratory-kkoon9/connector_front Contribute to laboratory-kkoon9/co

kkoon9.tistory.com

프론트 코드 : https://github.com/laboratory-kkoon9/connector_front

 

GitHub - laboratory-kkoon9/connector_front

Contribute to laboratory-kkoon9/connector_front development by creating an account on GitHub.

github.com

백엔드 코드 : https://github.com/laboratory-kkoon9/connector_back

 

GitHub - laboratory-kkoon9/connector_back

Contribute to laboratory-kkoon9/connector_back development by creating an account on GitHub.

github.com

배경

사용자들의 모든 프로필을 조회하는 페이지 내 API를 개발 과정에서 생긴 문제입니다.

1. Profile - User

프로필 목록 조회 시 User 테이블 select 절이 프로필 개수만큼 실행됐었습니다.

Hibernate: 
    select
        profile0_.id as id1_2_,
        profile0_.bio as bio2_2_,
        profile0_.company as company3_2_,
        profile0_.location as location4_2_,
        profile0_.status as status5_2_,
        profile0_.user_id as user_id7_2_,
        profile0_.website as website6_2_ 
    from
        profiles profile0_
Hibernate: 
    select
        user0_.id as id1_4_0_,
        user0_.avatar as avatar2_4_0_,
        user0_.email as email3_4_0_,
        user0_.name as name4_4_0_,
        user0_.password as password5_4_0_ 
    from
        users user0_ 
    where
        user0_.id=?
Hibernate: 
    select
        user0_.id as id1_4_0_,
        user0_.avatar as avatar2_4_0_,
        user0_.email as email3_4_0_,
        user0_.name as name4_4_0_,
        user0_.password as password5_4_0_ 
    from
        users user0_ 
    where
        user0_.id=?
Hibernate: 
    select
        user0_.id as id1_4_0_,
        user0_.avatar as avatar2_4_0_,
        user0_.email as email3_4_0_,
        user0_.name as name4_4_0_,
        user0_.password as password5_4_0_ 
    from
        users user0_ 
    where
        user0_.id=?
Hibernate: 
    select
        user0_.id as id1_4_0_,
        user0_.avatar as avatar2_4_0_,
        user0_.email as email3_4_0_,
        user0_.name as name4_4_0_,
        user0_.password as password5_4_0_ 
    from
        users user0_ 
    where
        user0_.id=?

현재 저의 DB에는 4개의 프로필이 존재합니다.

2. Profile - Skill

프로필 목록 조회 시 Skill 테이블 select 절 역시 프로필 개수만큼 실행됐었습니다.

Hibernate: 
    select
        skills0_.profile_id as profile_3_3_0_,
        skills0_.id as id1_3_0_,
        skills0_.id as id1_3_1_,
        skills0_.name as name2_3_1_,
        skills0_.profile_id as profile_3_3_1_ 
    from
        skills skills0_ 
    where
        skills0_.profile_id=?
Hibernate: 
    select
        skills0_.profile_id as profile_3_3_0_,
        skills0_.id as id1_3_0_,
        skills0_.id as id1_3_1_,
        skills0_.name as name2_3_1_,
        skills0_.profile_id as profile_3_3_1_ 
    from
        skills skills0_ 
    where
        skills0_.profile_id=?
Hibernate: 
    select
        skills0_.profile_id as profile_3_3_0_,
        skills0_.id as id1_3_0_,
        skills0_.id as id1_3_1_,
        skills0_.name as name2_3_1_,
        skills0_.profile_id as profile_3_3_1_ 
    from
        skills skills0_ 
    where
        skills0_.profile_id=?
Hibernate: 
    select
        skills0_.profile_id as profile_3_3_0_,
        skills0_.id as id1_3_0_,
        skills0_.id as id1_3_1_,
        skills0_.name as name2_3_1_,
        skills0_.profile_id as profile_3_3_1_ 
    from
        skills skills0_ 
    where
        skills0_.profile_id=?

N+1이 무엇인지 공부해보고, 이에 대한 해결 방법에 대해서 다뤄보겠습니다.

N+1 문제 정의

chatGPT가 말하는 N+1 문제

JPA(Java Persistence API)에서 N+1 문제는 데이터베이스 조회 시 발생할 수 있는 성능 문제 중 하나입니다.

이 문제는 관계형 데이터베이스의 관계 매핑에서 발생하며, 일반적으로 One-to-Many 또는 Many-to-Many와 같은 연관 관계에서 주로 나타납니다.

N+1 문제는 다음과 같은 상황에서 발생합니다:

  1. 하나의 엔티티(부모 엔티티)가 여러 개의 연관된 엔티티(자식 엔티티)를 가지고 있습니다.
  2. 부모 엔티티를 조회할 때는 쿼리 한 번으로 N개의 부모 엔티티의 정보를 가져옵니다.
  3. 그런 다음 각 자식 엔티티에 대한 추가적인 쿼리를 실행하여 자식 엔티티의 데이터를 가져옵니다.

이런 식으로 쿼리가 N+1번 실행되기 때문에 "N+1 문제"라고 불립니다.

이는 데이터베이스 호출이 증가하고 성능이 저하될 수 있는 문제를 일으킬 수 있습니다.

김영한 JPA 책에서 말하는 N+1 문제

profile 엔티티가 10개이면 skilll를 조회하는 SQL도 10번 실행됩니다.

이처럼 처음 조회한 데이터 수만큼 다시 SQL을 사용해서 조회하는 것을 N+1 문제라고 합니다.

N+1이 발생하면 SQL이 상당히 많이 호출되므로 조회 성능에 치명적입니다.

즉시로딩에서의 N+1 문제

entityManager를 직접 사용한다면 즉시로딩으로 연관관계가 맺어진 자식 엔티티는 조인을 사용해서 한 번의 SQL로 가져올 수 있습니다.

하지만 스프링 데이터 JPA는 쿼리 메서드(ex findAll)를 사용하면 메서드 이름을 분석하여 JPQL을 실행합니다.

이 때 즉시 로딩과 지연 로딩에 대해서 전혀 신경 쓰지 않고 SQL을 생성합니다.

따라서 jpa는 profileRepository.findAll()에 대하여 다음과 같은 쿼리를 만들어냅니다.

select * from profile

위 SQL로 프로필 엔티티를 애플리케이션에 로딩합니다.

하지만 프로필 엔티티와 연관된 유저 엔티티는 즉시 로딩으로 설정되어 있으므로 JPA는 유저 엔티티를 가져오려고 다음 SQL을 추가로 실행합니다.

select * from user WHERE id=?

위 예시처럼 프로필이 4개라면 프로필을 가져오는 쿼리 1번과 4번의 쿼리가 추가로 실행되는 겁니다.

즉시로딩과 N+1 문제에서 꼭 알아가야 할 키워드 : JPQL, entityManager

JPQL과 entityManager의 차이점

EntityManager와 JPQL(Java Persistence Query Language)은 JPA의 일부분으로, 데이터베이스와 상호 작용하는 데 사용됩니다.

entityManager

EntityManager는 JPA에서 영속성 컨텍스트를 관리하고 엔티티 객체를 데이터베이스에 저장, 조회, 갱신, 삭제하는 데 사용되는 인터페이스입니다.

영속성 컨텍스트는 엔티티 인스턴스를 관리하고, 엔티티 객체의 생명주기를 추적하여 데이터베이스와 일관성을 유지하는 데 도움이 됩니다.

EntityManager는 엔티티 매니저 팩토리에서 생성되며, 트랜잭션을 관리하고 영속성 컨텍스트를 초기화하는 역할을 합니다.

JPQL (Java Persistence Query Language)

JPQL은 객체지향 쿼리 언어로, 엔티티 객체에 대한 쿼리를 작성하는 데 사용됩니다.

JPQL은 엔티티의 속성을 기반으로 쿼리를 작성하며, SQL과는 다르게 테이블이 아닌 엔티티와 관계에 초점을 맞춥니다.

JPQL은 데이터베이스 종속성을 줄이고 객체 지향적인 쿼리 작성을 가능하게 합니다.

결론

EntityManager은 JPA에서 영속성 컨텍스트를 관리하고 데이터베이스와의 트랜잭션을 처리하는 데 사용되는 인터페이스이며, JPQL을 실행하는 데 사용됩니다.

JPQL은 객체지향적인 쿼리 언어로, 엔티티와 관련된 데이터를 조회하고 조작하는 데 사용됩니다.

EntityManager을 통해 JPQL을 실행하여 영속성 컨텍스트와 데이터베이스 간의 상호 작용을 처리할 수 있습니다.

즉, EntityManager은 영속성 컨텍스트를 관리하고 데이터베이스 트랜잭션을 처리하는 주체이며, JPQL은 이 EntityManager를 통해 엔티티를 조회하고 조작하는 데 사용되는 쿼리 언어입니다.

지연로딩에서의 N+1 문제

지연로딩이라면 프로필을 findAll 했을 때 유저와 같은 상황은 발생하지 않습니다.

하지만 이후 비즈니스 로직에서 Skill 컬렉션을 사용할 때 발생하게 됩니다.

모든 회원에 대해 연관된 Skill 컬렉션을 순회하게 되면 N+1이 발생하게 됩니다.

해결 방법 [1]. application.yml 에 batch_size 설정

spring:
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        jdbc:
          time_zone: Asia/Seoul
        format_sql: true
        show_sql: true
        default_batch_fetch_size: 10

spring.jpa.properties.hibernate.default_batch_fetch_size에 값을 넣어주면 됩니다.

Hibernate: 
    select
        profile0_.id as id1_2_,
        profile0_.bio as bio2_2_,
        profile0_.company as company3_2_,
        profile0_.location as location4_2_,
        profile0_.status as status5_2_,
        profile0_.user_id as user_id7_2_,
        profile0_.website as website6_2_ 
    from
        profiles profile0_
Hibernate: 
    select
        user0_.id as id1_4_0_,
        user0_.avatar as avatar2_4_0_,
        user0_.email as email3_4_0_,
        user0_.name as name4_4_0_,
        user0_.password as password5_4_0_ 
    from
        users user0_ 
    where
        user0_.id in (
            ?, ?, ?, ?
        )
Hibernate: 
    select
        skills0_.profile_id as profile_3_3_1_,
        skills0_.id as id1_3_1_,
        skills0_.id as id1_3_0_,
        skills0_.name as name2_3_0_,
        skills0_.profile_id as profile_3_3_0_ 
    from
        skills skills0_ 
    where
        skills0_.profile_id in (
            ?, ?, ?, ?
        )

 

결론

application.yml의 설정값을 변경해주는 것 외에도 @BatchSize 어노테이션을 사용하거나 페치 조인을 사용하는 방법이 있지만, 저는 위 방법을 제일 좋아하기 때문에 해당 방법만 소개했습니다!

N+1 문제 외에도 삽입 혹은 수정할 때에도 여러 개의 쿼리가 발생되는게 고민이시라면 다음 포스팅을 추천드립니다.

 

JPA에서의 bulk insert, bulk update test

배경 jpa는 insert는 save로, update는 변경감지를 통해 업데이트가 진행됩니다. 문제는 이 작업들이 단 건으로 진행된다는 점이죠. saveAll이 있으나 내부를 살펴보면 반복문을 통해 save로 저장해주는

kkoon9.tistory.com

읽어주셔서 감사합니다!

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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
글 보관함