일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- 클린코드
- 인터프리터
- 객체지향
- clean code
- Java
- JIT
- 클린 코드
- 도메인 모델
- 캐싱
- 스프링
- JPA
- SRP
- 리팩토링
- 재사용성
- 쿼리 최적화
- 자바
- 캡슐화
- 스프링부트
- 객체지향의 사실과 오해
- REST API
- 객체
- string
- 협력
- Lombok
- 추상화
- 책임
- Refactor
- 캐시
- cache
- spring boot
- Today
- Total
GO SIWOO!
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (8) - JDBC Batch INSERT 본문
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (8) - JDBC Batch INSERT
gosiwoo 2023. 10. 18. 02:54[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (7) - Ehcache 3 사용
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (6) - Redis를 통한 Open API 결과 캐싱(Caching) [리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (5) - 응답 form & Global Exception [리팩토링 일지] 팀 프로젝트,
gosiwoo.tistory.com
📌대량의 데이터를 필요로 한 이유
요즈음 Real MySQL 8.0을 공부하며 Index 설정을 통한 쿼리 최적화를 이루고자 대량의 데이터가 필요했습니다. 우선 Index를 설정할 테이블은 Member, 회원 테이블로 다량의 데이터를 CommanLineRunner를 사용해 프로젝트 시작과 동시에 데이터를 집어넣기로 하였습니다.
📌JDBC Batch Insert 도입 전
1. for문 돌아가며 단건 save
@Component
public class InitData {
@Bean
CommandLineRunner initMemberData(MemberRepository memberRepository) {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
long beforeTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
Member createdMember = Member.builder()
.username(UUID.randomUUID().toString())
.password(UUID.randomUUID().toString())
.build();
memberRepository.save(createdMember);
}
long afterTime = System.currentTimeMillis();
long secDiffTime = afterTime - beforeTime;
System.out.println("(ms): "+ secDiffTime);
}
};
}
}
위와 같은 코드로 작성하였습니다. 해당 코드는 100만번의 INSERT를 실행하는 코드이며 실행 시간을 출력할 것입니다.
실행 결과는 다음과 같습니다.
1998965m/s, 33분이 넘게 걸렸다... 쿼리 최적화를 위해 코드를 수정하고 프로젝트를 다시 시작하기에 33분 씩이나 걸리는데 실제로 사용하기에는 무리가 있어 보입니다. 사실 500만건 이었는데 줄인거다...
이대로는 안된다. 바꿔야 합니다.
2. for문 돌아가며 List.add 후 saveAll
@Component
public class InitData {
@Bean
CommandLineRunner initMemberData(MemberRepository memberRepository) {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
long beforeTime = System.currentTimeMillis();
List<Member> memberList = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
Member createdMember = Member.builder()
.username(UUID.randomUUID().toString())
.password(UUID.randomUUID().toString())
.build();
memberList.add(createdMember);
}
memberRepository.saveAll(memberList);
long afterTime = System.currentTimeMillis();
long secDiffTime = afterTime - beforeTime;
System.out.println("(ms): "+ secDiffTime);
}
};
}
}
List에 Member 엔티티를 담고 100만건의 Member를 saveAll 메서드로 한번에 INSERT하였습니다.
360941m/s, 단건 save보다 약 6배 단축되었습니다. 그런데 이게 최선일까? 여전히 6분이라는 시간을 프로젝트 실행마다 기다려야 합니다. 더 개선해야 합니다.
📌JDBC Batch Insert 도입
1. Bulk Insert의 필요이유?
위에서 사용한 save()와 saveAll()은 트랜잭션을 생성하는 횟수의 차이만 있을 뿐 단건의 save()가 진행되는 것은 차이가 없습니다.
그렇다면 어떻게 해야할까? 답은 Bulk Insert입니다.
단건 삽입
INSERT INTO Members (username, password) VALUES ("username1", "password1");
INSERT INTO Members (username, password) VALUES ("username2", "password2");
INSERT INTO Members (username, password) VALUES ("username3", "password3");
Bulk 삽입
INSERT INTO Members (username, password)
values
("username1", "password1"),
("username2", "password2"),
("username3", "password3);
Bulk Insert는 위처럼 단건의 쿼리만 나갑니다. saveAll()이 하나의 트랜잭션으로 묶여 처리를 하더라도 100만건의 쿼리문을 수행하기보다 성능이 훨씬 좋습니다.
2. JDBC Batch Insert
MySQL을 사용하는 본 프로젝트에서는 IDENTITY 전략을 사용해 기본 키 생성을 하므로 Batch Insert를 사용할 수 없습니다. 기본 키의 설정을 DB에 위임하는 IDENTITY 전략은 AUTO_INCREMENT의 동작을 DB에서 관리하기에 DB의 접근해야 기본 키의 값이 결정됩니다. 즉, INSERT 쿼리를 실행하기 전까지는 PK를 알 수 없기에 Batch Insert를 사용불가능합니다.
JDBC Batch Insert를 사용해 Bulk Insert 수행합니다.
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/kotudy__refactor?&rewriteBatchedStatements=true
username: *
password: *
driver-class-name: com.mysql.cj.jdbc.Driver
application.yml 파일입니다. spring.datasource.url: rewriteBatchedStatements=true 파라미터를 추가해야 합니다.
해당 파라미터를 통해 INSERT 쿼리를 Bulk 수행을 할 수 있게 됩니다.
@Repository
@RequiredArgsConstructor
public class MemberBulkRepository {
private final JdbcTemplate jdbcTemplate;
@Transactional
public void saveAllMember(List<Member> products) {
String sql = "INSERT INTO members (username, password, role) " +
"VALUES (?, ?, ?)";
jdbcTemplate.batchUpdate(sql,
products,
products.size(),
(PreparedStatement ps, Member member) -> {
ps.setString(1, member.getUsername());
ps.setString(2, member.getPassword());
ps.setString(3, member.getRole().toString());
});
}
}
JDBC Bulk를 수행하는 Repository 클래스입니다. Spring JDBC를 사용합니다.
batchUpdate()를 사용해 매개변수로 받은 List를 한번의 INSERT 쿼리문으로 수행하도록 합니다.
@Component
public class InitData {
@Bean
CommandLineRunner initMemberData(MemberBulkRepository memberBulkRepository) {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
long beforeTime = System.currentTimeMillis();
List<Member> memberList = new ArrayList<>() {
{
for (int i = 0 ; i < 1000000; i++) {
Member createdMember =Member.builder()
.username(UUID.randomUUID().toString())
.password(UUID.randomUUID().toString())
.build();
this.add(createdMember);
}
}
};
memberBulkRepository.saveAllMember(memberList);
long afterTime = System.currentTimeMillis();
long secDiffTime = afterTime - beforeTime;
System.out.println("(ms): "+ secDiffTime);
}
};
}
}
초기 데이터를 INSERT하는 코드입니다. 위에서 사용했던 save(), saveAll()과 크게 다르지 않습니다.
28065m/s, 28초가 걸렸습니다. 앞서 실행했던 saveAll() 방법, 6분보다 약 12배의 개선을 이루어 내었습니다.
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/kotudy__refactor?&rewriteBatchedStatements=true&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=200
username: *
password: *
driver-class-name: com.mysql.cj.jdbc.Driver
profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=2000 옵션을 주어 JDBC 쿼리문을 출력해 몇번의 INSERT 쿼리문이 날라갔는지 확인하면 다음과 같습니다.
- profileSQL : Driver에서 전송하는 쿼리를 출력합니다.
- logger : Driver에서 쿼리 출력시 사용할 Logger를 설정합니다.
- maxQuerySizeToLog : 출력할 쿼리 길이 설정합니다.
쿼리문은 최대 2000자까지 나옵니다. 빨간 네무 보분을 보면 Bulk Insert가 수행된 것을 보실 수 있습니다.
다음 포스팅에서는 이렇게 삽입한 데이터를 바탕으로 조회 쿼리 최적화 과정에 대해서 포스팅하겠습니다.
📌포스팅 과정에서 참고한 글들
Spring JDBC를 사용하여 Batch Insert 수행하기 (tistory.com)
Spring JPA Save() vs SaveAll() vs Bulk Insert (velog.io)
Spring Data JPA의 saveAll() 사용시 주의점 — 김솔샤르의 인사이트 (tistory.com)
[Spring] 프록시 내부 호출 문제점 (tistory.com)
@Transactional 안에서 @Transactional 메서드 사용 - 인프런 | 질문 & 답변 (inflearn.com)
Generate Primary Keys Using JPA and Hibernate (thorben-janssen.com)
Batch Insert 성능 향상기 1편 - With JPA - Yun Blog | 기술 블로그 (cheese10yun.github.io)
📌Github
GitHub - siwookim97/kotudy-refactor: 📌한국어학습 웹 어플리케이션 백엔드 RESTful API 리팩토링 Repo
📌한국어학습 웹 어플리케이션 백엔드 RESTful API 리팩토링 Repo. Contribute to siwookim97/kotudy-refactor development by creating an account on GitHub.
github.com
'Develop > 팀 프로젝트, 나홀로 리팩토링' 카테고리의 다른 글
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (10) - OSIV 설정 (0) | 2023.10.25 |
---|---|
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (9) - 조회 쿼리 성능 개선 (1) | 2023.10.19 |
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (7) - Ehcache 3를 통한 Open API 호출 서비스 응답 속도 향상 (1) | 2023.09.15 |
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (6) - Redis를 통한 Open API 결과 캐싱(Caching) (0) | 2023.09.06 |
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (5) - 응답 form & Global Exception (0) | 2023.08.17 |