티스토리 뷰
자바와 스프링에 대한 기본 지식을 기르기 위해 토이 프로젝트를 시작했습니다.
프론트 코드 : https://github.com/laboratory-kkoon9/connector_front
백엔드 코드 : https://github.com/laboratory-kkoon9/connector_back
배경
사용자들의 모든 프로필을 조회하는 페이지 내 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 문제는 다음과 같은 상황에서 발생합니다:
- 하나의 엔티티(부모 엔티티)가 여러 개의 연관된 엔티티(자식 엔티티)를 가지고 있습니다.
- 부모 엔티티를 조회할 때는 쿼리 한 번으로 N개의 부모 엔티티의 정보를 가져옵니다.
- 그런 다음 각 자식 엔티티에 대한 추가적인 쿼리를 실행하여 자식 엔티티의 데이터를 가져옵니다.
이런 식으로 쿼리가 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 문제 외에도 삽입 혹은 수정할 때에도 여러 개의 쿼리가 발생되는게 고민이시라면 다음 포스팅을 추천드립니다.
읽어주셔서 감사합니다!
'개발 노트 > 토이 프로젝트로 배우는 스프링+자바' 카테고리의 다른 글
토이 프로젝트로 배우는 자바 스프링 [5]. 엔티티의 생성시각을 자동으로 저장하기 (JPA Auditing) (0) | 2024.04.23 |
---|---|
토이 프로젝트로 배우는 자바 스프링 [4]. WebSecurityConfigurerAdapter deprecated (1) | 2024.01.28 |
토이 프로젝트로 배우는 자바 스프링 [3]. API path와 HTTP Method로 권한 분리하기 (0) | 2024.01.23 |
토이 프로젝트로 배우는 자바 스프링 [2]. 연관관계 추가 및 삭제 (0) | 2024.01.16 |
토이 프로젝트로 배우는 자바 스프링 [0]. prologue (1) | 2023.11.27 |
- Total
- Today
- Yesterday
- BAEKJOON
- 프로그래머스
- 코테
- Effective Java
- C++
- Spring
- Algorithm
- 클린 아키텍처
- kotest
- BOJ
- AWS
- 객체지향
- MSA
- 이팩티브 자바
- 디자인 패턴
- Java
- kkoon9
- 디자인패턴
- 정규표현식
- node.js
- 백준
- 테라폼
- programmers
- 알고리즘
- JPA
- Kotlin
- Spring Boot
- 이펙티브 자바
- 클린 코드
- Olympiad
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |