-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BE] JPA의 saveAll, deleteAll를 bulk query로 개선 #273
Conversation
Test Results127 tests 126 ✅ 7s ⏱️ Results for commit 32d53b6. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1000만 건의 데이터라니 대박이네요!
상세한 설명 덕에 코드를 이해하는데 수월하였어요. 감사합니다!
@Modifying | ||
@Transactional | ||
@Query("DELETE FROM Schedule s WHERE s.attendee = :attendee") | ||
void deleteByAttendee(Attendee attendee); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deleteAll()의 개선 사항도 궁금해지는데요!
기존 코드 대비 성능이 어떤 식으로 향상 된 것인지 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 궁금합니다🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deleteAll()
도 saveAll()
과 마찬가지로 내부 구현을 살펴보면 반복문 안에서 delete()
를 호출하는 것을 알 수 있어요. 또한 단순히 deleteBy()
를 해도 쿼리가 N번 호출됨과 예상하지 못한 추가적인 SELECT 조회 쿼리
가 발생하는 것을 확인할 수 있었습니다.
때문에 saveAll() 과는 다른 방식으로 접근해야 함을 깨달을 수 있었는데요.
애초에 JPA가 자체적으로 Bulk 삭제를 지원하지 않을뿐더러, 삭제를 시도하면 영속성 컨텍스트에 삭제 대상과 동시에 연관된 모든 엔티티들을 영속화하려는 시도를 해요.
현재 상황에서는 Schedule
과 AvailableDate
에 추가 조회 쿼리가 발생하고 있는데
JPA는 삭제 전에 데이터베이스에 삭제 대상 엔티티가 존재하는지, 삭제시 참조 무결성을 위배하지 않는지 확인하는 작업을 필수로 거쳐요. 이는 delete()
내부 구현에서도 쉽게 확인할 수 있습니다.
그런데 삭제할 엔티티의 개수가 많다면 메모리에 심각한 부하를 주게되고 Out Of Memory Error
가 발생할 수 있어요. 이는 현재에도 Spring data jpa의 꽤나 유명한 이슈에요.
이를 효율적으로 해결하는 방법은 직접 @Query
를 작성해서 데이터베이스에 바로 쿼리를 호출하는건데요, 직접 SQL을 작성하는 것과 다름이 없으므로 애초에 우리가 기대했던 동작을 수행할 수 있게됩니다.
그런데 이는 영속성 컨텍스트를 무시한다고도 볼 수 있어요. 그래서 반드시 @Modifying
어노테이션을 명시해야 하고, 해당 작업으로 발생하는 데이터베이스와 영속성 컨텍스트간 불일치는 @Modifying 이 제공하는 옵션
으로 해결할 수 있어요. 현재는 사이드 이펙트가 발생하지 않는다고 판단해서 따로 옵션을 명시하지는 않았습니다.
https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.modifying-queries
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
id 생성 전략이 AutoIncrement(Identity) 일 경우, Hibernate는 Jdbc 레벨에서 Batch Processing을 비활성화는걸 새로 알게 되었네요. PR 본문이 상세하게 적혀있어 이해하는데 도움이 되었어요. 배치를 통해 훨씬 빠른 데이터 삽입이 가능하겠네요 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pr에 내용을 상세하게 적어주셔서 어떤 개선을 했는지 명확하게 이해할 수 있었어요.
궁금한 점과 사소한 코멘트 남겼습니다.
고생 많으셨어요~~!! 👏👏
availableDate = availableDateRepository.save(AvailableDateFixture.TODAY.create(meeting)); | ||
} | ||
|
||
@DisplayName("Schedule 리스트를 Batch Insert 한다.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 내용을 보면 schedules
가 제대로 저장되는지 확인하는 테스트인 것 같아요. 그러나 해당 메서드의 @DisplayName
은 'Batch Insert를 한다'는 내용이 있어서 insert 쿼리가 1개 나가는지 테스트한다고 생각했어요. @Display
내용을 테스트 내용에 맞춰보면 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
설정한 Batch Size 만큼 네트워크 통신을 하는지 테스트하는 건 불가능한 영역이라고 생각했어요.
또한 결국에 우리가 알고 싶은 부분은 insert 쿼리가 몇 번 나가는지
가 아니라, 우리가 예상한 쿼리가 잘 호출되는지
라고 생각합니다.
그런데 직접 작성한 쿼리가 올바르게 동작하는지는 테스트가 필요한 영역이라고 생각하고,
이에 대한 테스트를 작성했어요.
JDBC로 작업을 처리할 경우에 실제 호출되는 쿼리가 보이지 않을텐데
DB URL에 &profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=999999
을 명시하면 확인할 수 있어요 😄
배키가 혹시 제안하신 방법을 알고계신다면 가르쳐주시면 감사하겠습니다. 😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
정한 Batch Size만큼 네트워크 통신을 하는지 테스트하는 건 불가능한 영역이라고 생각했어요
네 저도 그렇게 생각합니다. 그래서 테스트 내용(코드)을 바꾸는 것이 아니라, @DisplayName
의 내용("Schedule 리스트를 Batch Insert 한다."
) 바꾸는 것을 말씀드렸던 거고요..! @DisplayName
의 내용을 보고 'batch 처리 하는 것을 테스트하는구나'라고 생각했거든요.
문구에 대한 사소한 내용이니, 그냥 넘어가도 좋을 것 같습니다😊
@Modifying | ||
@Transactional | ||
@Query("DELETE FROM Schedule s WHERE s.attendee = :attendee") | ||
void deleteByAttendee(Attendee attendee); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 궁금합니다🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨어요 재즈!
안 그래도 DB에 데이터가 많아지니 일정 등록이 10초가 넘어가길래😅 설정해야겠다고 생각했는데 빠르게 작업해 주셨네요👍
032ea96
to
32d53b6
Compare
관련 이슈
작업 내용
saveAll()
,deleteAll()
호출 시 엔티티 개수만큼 쿼리가 호출되는 문제를 개선했습니다.엔티티 수 만큼 쿼리가 호출되었던 이유는 다음과 같습니다.
saveAll()
과deleteAll()
메서드는 JPA에서 다수의 엔티티를 일괄적으로 처리할 수 있도록 제공하는 기능인데요.saveAll()
내부 구현을 살펴보면 하나의 트랜잭션 안에서 엔티티 수 만큼 반복문을 돌며save()
를 호출합니다.즉 매번 새로운 트랜잭션을 생성하는
save()
와 달리하나의 트랜잭션 안에서 처리한다는 차이만 존재할 뿐, 엔티티 수 만큼 쿼리가 호출되는 것에는 변함이 없습니다.
이를 해결하는 방법에는 두 가지 방법이 존재합니다.
hibernate.jdbc.batch_size
옵션을 통한 Batch UpdateHibernate도 Batch Processing을 위한
hibernate.jdbc.batch_size
옵션을 제공하는데요,하지만 id 생성 전략이
AutoIncrement(Identity)
일 경우, Batch Processing 및 Hibernate 쓰기 지연 철학과 충돌이 발생하고Hibernate는 Jdbc 레벨에서 Batch Processing을 비활성화 시킵니다.데이터베이스에 Id 생성 책임을 위임하는 Identity 전략과 서로 이해관계가 맞지 때문입니다.
배치 처리를 위해서는 영속성 컨텍스트에 엔티티를 모아서 처리하는 작업이 필요한데,
영속화를 위해서는 ID(PK)가 필수적으로 필요하기 때문에 개별 엔티티들을 영속화 하는 시점에
em.flush()
가 호출되게 됩니다.따라서 Identity 전략을 사용할 경우 JPA 레벨에서 Batch 처리하는 방법은 존재하지 않습니다.
Sequence 전략은 MySQL이 지원하지 않고있고, Table 전략은 현명한 판단이 아니기 때문에 더 Low 레벨인 Jdbc로 직접 구현하는 방법을 선택했습니다.
특이 사항
rewriteBatchedStatements=true
배치 사이즈의 경우 예약 생성하는 과정에서 가능한 날짜들을 삽입하는 경우 30
스케쥴 생성 과정에서 삽입하는 경우 500으로 설정하였습니다.
배치 사이즈를 설정할 때에는
JVM 메모리
와DB Packet Size Limit
두 가지를 고려해야 하는데요,너무 작게 설정하면 네트워크 통신 비용이 증가됨에 따라 성능이 하락하고, 너무 크게한다면 어플리케이션과 데이터베이스에 무리를 줄 수 있습니다.
Batch Processing의 철학을 고려해보면 현재보다 사이즈를 크게 설정해도 무리가 없을 것으로 예상하지만,
아직 실제 환경에서의 부하 데이터가 없고, OLTP 환경임을 고려했을 때 조금 더 방어적으로 설정하는 것이 안전하다고 판단했습니다.
MySQL Packet Size Limit
는 명령어로 쉽게 확인할 수 있고, 현재 기본 값은67108864B
,64MB
입니다.따라서 하나의 레코드 사이즈를 계산할 수 있으면 한 번에 몇 개 데이터까지 전송할 수 있는지 대략적으로 파악할 수 있습니다.
1000만개가 들어있는 Schedule 테이블의 data length는
711,983,104B
인 것을 확인했고 계산해봤을 때 하나의 레코드당 대략71B
.67108864B/71B 를 하면
945,195
라는 수치가 나오니 약 94만개 까지는 한 번에 전송할 수 있다는 결론을 얻을 수 있습니다.레퍼런스