- 도커
- with jdk
- kotlin
- restcontroller
- openapispec
- QueryDSL
- OneToMany
- restdocs
- springboot
- oraclejdk
- 폰트어썸
- 전략패턴
- arc browser
- Openjdk
- java_to_kotlin
- remix icon
- fontawesome
- 무료 아이콘 폰트
- EntityGraph
- 옵저버패턴
- Observer Pattern
- Spring Cloud
- Docker
- 라즈베리파이 클러스터
- 리믹스아이콘
- Spring
- 디자인패턴
- 라즈베리파이
- java
- spring cloud contract
- Today
- Total
< Dev-Kidult />
JPA OneToMany EAGER 문제 및 해결방안 본문
먼저 모든 코드는 Github에 정리되어 있으며, 함께 보시면 이해하기 쉬우실 것 같습니다.
문제
회사에서 이번 프로젝트를 진행하면서 JPA를 좀 더 깊게 사용을 하게 되었는데
그중 하나의 도메인에 OneToMany로 엮어있는 필드가 3개가 있었습니다.
테스트하면서는 양이 적어 문제가 되는지 몰랐지만
현재 스테이지서버에서 데이터가 약 900건 정도 쌓이게 되었고
화면으로 던져주기전에 저 OneToMany로 엮어있는 필드 3개를 활용하여 데이터를 가공하여 넘겨주는 상황이었습니다.
테스트를 진행하시는 분들이 너무 느리다 하여 파악을 진행하던 중에 fetchType을 eager로 해두어도 sql 셀렉이 900 * 3으로 2700건이 나오는 상황이었습니다.
문제 해결
(진행은 코틀린으로 하였습니다)
먼저 예제용으로 user와 그리고 여러 개의 game character를 만들었습니다. (DB도 도메인 보고 맞게 만들어주시면 됩니다.)
@Table
@Entity
data class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
@Column
var username: String? = null,
@Column
var password: String? = null,
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = [CascadeType.ALL], orphanRemoval = true)
var characters: MutableSet<GameCharacter>? = null
) {
fun addCharacter(character: GameCharacter) {
if (characters == null) {
characters = mutableSetOf()
}
this.characters?.add(character)
character.updateUser(this)
}
}
@Table
@Entity
data class GameCharacter(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
@ManyToOne
@JoinColumn(name = "user_id")
var user: User? = null,
@Column
var name: String? = null,
@Column
var gameLevel: Long? = null
) {
fun updateUser(user: User) {
this.user = user
}
}
그리고 바로 이어서 DataJpaTest 클래스를 만들어서 진행했습니다.
@Test
internal fun `user 를 여러개 조회하면 N1쿼리가 발생한다`() {
val extractCharacterName = extractCharacterName(userRepository.findAll())
then(extractCharacterName.size).isEqualTo(10)
}
먼저 평범하게 findAll 함수를 호출하여 진행하였더니 fetch type을 eager로 지정했음에도 위와 같이 총 11건의 쿼리가 발생하는 것을 볼 수 있습니다.
이를 해결하기 위해 @EntityGraph라는 어노테이션을 쓰게 됩니다.
@EntityGraph(attributePaths = ["characters"])
@Query("select u from User u")
fun findAllEntityGraph(): MutableList<User>
attributePaths에 array로 outer join 할 필드를 써주시면 되는데, DB칼럼명이 아닌 도메인 안에 필드명을 써주시면 됩니다.
실제로 해당 어노테이션 적용을 하여 테스트를 진행하니
@Test
internal fun `entityGraph 어노테이션을 쓰면 N1쿼리가 안생김`() {
val extractCharacterName = extractCharacterName(userRepository.findAllEntityGraph())
then(extractCharacterName.size).isEqualTo(10)
}
select
user0_.id as id1_1_0_,
characters1_.id as id1_0_1_,
user0_.password as password2_1_0_,
user0_.username as username3_1_0_,
characters1_.game_level as game_lev2_0_1_,
characters1_.name as name3_0_1_,
characters1_.user_id as user_id4_0_1_,
characters1_.user_id as user_id4_0_0__,
characters1_.id as id1_0_0__
from
user user0_
left outer join game_character characters1_ on
user0_.id = characters1_.user_id
해당 단일 쿼리가 날아가 해결이 됩니다.
추가로 List가 아닌 Set으로 해주셔야 카테시안 곱으로 인한 중복데이터가 안들어갑니다
데이터가 적을 때는 체감하지 못하던 문제였는데 데이터가 쌓이고 페이징을 적용하지 못하는 상황에서 문제를 마주하니 이러한 새로운 사실 및 공부를 하니 다행이라는 생각이 들었습니다.
이 글, 이 블로그를 보시는 분들도 문제 해결에 도움이 됐으면 좋겠습니다.
'개발 > Back-end' 카테고리의 다른 글
Spring boot JPA Specification + Spring filter 파라미터로 쉽게 조회하기 (0) | 2022.04.21 |
---|---|
`Spring cloud contract` 를 배워보자 (0) | 2020.08.14 |
디자인 패턴 - 옵저버패턴(Observer pattern) with jdk (0) | 2019.07.11 |
디자인 패턴 - 전략패턴(Strategy pattern) (0) | 2019.06.29 |
Open JDK와 Oracle JDK의 차이점 (0) | 2018.10.10 |