- 데이터베이스
- 스프링부트
- spring mvc
- 자바
- 1차원 배열
- jpa
- mysql
- sql
- @transactional
- spring
- 스프링
- springboot
- select
- nginx
- Docker
- 문자열
- hibernate
- PYTHON
- static
- 프로그래머스
- java
- DI
- ORM
- string
- AWS
- SSL
- spring security 6
- Django
- spring boot
- join
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
개발하는 자몽
[Spring Data JPA] 하이버네이트 Batch Size 본문
상황
1:N 관계가 얽혀있는 엔티티를 대상으로 Spring Data JPA를 이용해서 Pagination 조회를 하니 말로만 듣던 N+1 상황이 발생했다.
처음에는 일반적인 해결 방법인 Fetch Join을 사용하려고 했다. 더불어 중복을 방지하기 위해
하이버네이트 Batch Size
하이버네이트에서 제공하는
엔티티를 가져오는 것은 데이터베이스에서 데이터를 가져와 애플리케이션에서 객체(Object)로 변환하는 것을 의미한다. 하이버네이트는 데이터를 그룹(batches)으로 가져와 이런 동작을 최적화할 수 있다. 이는 데이터베이스 호출 수를 줄이고, 요청 처리 효율성을 증가시킨다.
클래스 레벨에서 적용하기
엔티티 클래스에
@Entity
@BatchSize(size=100)
public class MyEntity {
...
}
컬렉션 레벨에서 적용하기
엔티티의 컬렉션 필드에
@OneToMany
@BatchSize(size=50)
private Set<MyCollection> myCollections = new HashSet<>();
스프링 부트에서 batch_size 설정하기
spring.jpa.properties.hibernate.jdbc.batch_size=50
하이버네이트에서 이러한 쓰기 작업을 최적화하는 것은 오직 전역
해결
나는 읽기 작업 외에 쓰기(save/saveAll) 작업에도 적용되길 원했기 때문에
번외. Batch Insert와 MySQL
Notice 여기서 부터는 1:N 관계 Pagination의 N+1 문제와는 관련이 없다.
참고로 Batch Insert를 원하는데, MySQL을 사용하고 있고 PK 생성 전략을
MySQL에서 IDENTITY 전략은
이를 해결하는 방법 중 하나는 PK 생성 전략을
Batch Insert와 JDBC Template 사용하기
JDBC Template을 사용하여 Batch Insert를 하려면 MySQL JDBC에
spring:
datasource:
url: jdbc:mysql://{MYSQL_DB_URL}:{PORT}/{SCHEMA}?rewriteBatchedStatements=true
이제
@Repository
@RequiredArgsConstructor
public class MyEntityJdbcRepository {
private final JdbcTemplate jdbcTemplate;
public void saveAll(List<MyEntity> myEntities, Long myEntityLastId) {
String sql = "INSERT INTO my_entity (my_entity_id, ..., created_at, modified_at) VALUES (?, ..., now(), now())";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
MyEntity myEntity = myEntities.get(i);
// 파라미터 설정
ps.setLong(1, myEntityLastId + i); // pk 설정
ps.setString(2, myEntity.getXxx());
...
ps.setString(n, myEntity.getXxx());
}
@Override
public int getBatchSize() {
return myEntities.size(); // 삽입할 컬렉션 크기 만큼 배치 사이즈 설정
}
});
}
}
이제 insert/save 로직을 수행하는 곳에서 해당 메소드를 호출하여 사용하면 된다.
잘 적용이 되었는지 콘솔창에서 확인하고 싶다면 아래 설정을 추가하자.
spring:
datasource:
url: jdbc:mysql://{MYSQL_DB_URL}:{PORT}/{SCHEMA}?rewriteBatchedStatements=true&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=500
profileSQL=true : Driver에서 전송하는 쿼리를 출력logger=Slf4JLogger : Driver에서 쿼리 출력 시 사용할 Logger 설정maxQuerySizeToLog=... : 출력할 쿼리 길이 설정
Batch Insert 이후 또다른 문제...
Batch Insert를 사용한
jdbcTemplate.batchUpdate() 는 레코드를 바로 삽입하고AUTO_INCREMENT 값을 증가시킨다.- 하이버네이트의
save() 는 JDBC가 이미 사용한 최신AUTO_INCREMENT 값을 인지하지 못한다. - MySQL은 내부적으로
AUTO_INCREMENT 카운터를 유지하려고 한다.
그래서 JDBC를 사용한 Batch Insert 이후에 MySQL의
@Repository
@RequiredArgsConstructor
public class MyEntityJdbcRepository {
private final JdbcTemplate jdbcTemplate;
public void saveAll(List<MyEntity> myEntities, Long myEntityLastId) {
...
}
// 데이터베이스 id의 가장 마지막 값을 조회
public Long getMaxId() {
String sql = "SELECT COALESCE(MAX(my_entity_id), 0) FROM my_entity";
return jdbcTemplate.queryForObject(sql, Long.class);
}
// 다음 ID를 `value`로 시작하도록 테이블 변경
public void resetAutoIncrement(Long value) {
String sql = "ALTER TABLE my_entity AUTO_INCREMENT = ?";
jdbcTemplate.update(sql, value);
}
}
실제 사용 코드이다. Batch Insert 이후 ID 값을 업데이트 한다(단일 insert/save 하는 코드 쪽은 건드리지 않았다).
@Transactional
public void initData() {
...
myEntityJdbcRepository.saveAll(myEntities, 1L);
Long maxId = myEntityJdbcRepository.getMaxId();
myEntityJdbcRepository.resetAutoIncrement(maxId + 1);
}
이후 정상적으로 실행됐다!
🔖참고
'Java & Kotlin > Spring' 카테고리의 다른 글
[Spring Security] OAuth2 카카오 로그인 구현 with JWT (0) | 2024.12.29 |
---|---|
[Spring] @Value과 static 변수 (0) | 2024.12.16 |
[JPA Error] No EntityManager with actual transaction available for current thread - cannot reliably process 'flush' call (0) | 2024.10.23 |
[JPA] 임베디드 타입(@Embeddable, @Embedded)에 관하여 (0) | 2024.08.23 |
[Spring Boot / Error] Required request body is missing (0) | 2024.07.13 |