Today I Learned
Spring 플러스 주차 개인 과제
#대용량 데이터 삽입 및 조회 성능 테스트
이번에 구현한 기능은 대용량 데이터를 삽입하고, 그 데이터를 기반으로 검색 API의 성능을 테스트하는 작업이었다.
목표
500만 건의 테스트 유저 데이터를 삽입
검색 API의 응답 시간 측정 및 인덱스 최적화
삽입/조회 성능 테스트 결과 기록
구현
1. 삽입 코드 설계
// 고유 닉네임 생성 메서드
private String createUniqueNickname() {
String nickname = "user_" + UUID.randomUUID().toString().substring(0, 8);
// 중복인 경우 재생성
while (!nicknameSet.add(nickname)) {
nickname = "user_" + UUID.randomUUID().toString().substring(0, 8);
}
return nickname;
}
처음에는 UUID를 활용해서 랜덤한 닉네임을 생성했는데, 예상보다 생성시간이 길어 약 15분 정도 소요됐다.
조회 성능만 고려했었는데, 생성시간도 생각보다 오래 걸렸다.
닉네임 길이가 너무 길어 `substring()`으로 자릿수를 줄였고, 혹시 이로 인해 중복 검증 루프를 과도하게 반복해서 시간이 늘어지는 건 아닌지 테스트해봤지만, UUID 길이에 따른 유의미한 성능 차이는 없었다.
public void insertUsers() throws SQLException {
int bulk = 5000000;
String sql = "INSERT INTO test_users (nickname) VALUES (?)";
// 데이터베이스 연결과 SQL 실행을 위한 객체 생성
try (Connection connection = dataSource.getConnection(); // DB 연결
PreparedStatement ps = connection.prepareStatement(sql)) { // SQL 준비
connection.setAutoCommit(false); // 자동 커밋 off
for (int i = 1; i <= bulk; i++) {
String nickname = createUniqueNickname();
ps.setString(1, nickname); // SQL의 (?)에 닉네임 값을 설정
ps.addBatch(); // 배치에 추가 (실행 X)
// 1000건마다 DB에 커밋
if (i % 1000 == 0) {
ps.executeBatch(); // 배치 실행
connection.commit(); // 트랜잭션 커밋
ps.clearBatch(); // 배치 초기화
}
}
}
}
이후 JDBC의 배치 기능을 활용해 1000건마다 `executeBatch()`와 `commit()`을 수행하도록 구성하고,
1000, 2000, 5000, 10000, 50000으로 배치 크기를 조절하면서 삽입 속도에 어떤 영향을 주는지도 테스트해봤지만,
이또한 큰 차이는 없었다.
for (int i = 1; i <= bulk; i++) {
// 인덱스 기반 랜덤 닉네임 생성
String nickname = "user_" + i + "_"+ UUID.randomUUID().toString().substring(0, 4);
ps.setString(1, nickname); // SQL의 (?)에 닉네임 값을 설정
ps.addBatch(); // 배치에 추가 (실행 X)
마지막으로 `Set`에 500만 건을 저장하며 중복 검증하는 구조가 병목일 수 있다는 판단으로, 인덱스 기반으로 고유성을 확보하고, UUID 일부를 접미에 붙여 랜덤성을 유지하는 방식으로 변경해서 테스트해봤으나, 이 역시 유의미한 차이는 없었다.
아쉽게도 리팩토링을 통해 유의미한 성능 향상은 없었고,
실무에서도 500만 건씩 데이터를 처리할 일은 드물겠지만,
삽입 구조에 대한 다양한 시도와 실험을 통해 성능 병목의 원인들에 대해 생각해볼 수 있는 시간이었다.
2. 검색 API 성능 테스트
검색 API는 닉네임이 정확히 일치하는 경우에만 유저를 반환하도록 구현했다.
@Transactional(readOnly = true)
public List<TestUser> findByNickname(String nickname) {
long start = System.currentTimeMillis();
List<TestUser> result = testUserRepository.findByNickname(nickname);
long end = System.currentTimeMillis();
System.out.println("조회 시간(ms): " + (end - start));
return result;
}
첫 조회 결과를 확인하고 이후 응답 속도를 개선하기 위한 과정을 진행해야하는데, 예상과 달리 너무 조회 속도가 빨라서 당황해버렸다.
몇 번을 다시 시작해봐도 4~6ms가 나오는 것이다.
테이블 정의를 다시 살펴보니 `nickname` 컬럼에 UNIQUE 제약이 걸려 있었다.
습관처럼 넣어둔 `unique = true` 설정이 사실상 인덱스 역할을 하면서,
정확히 일치하는 조건으로 조회할 때, 인덱스가 있으면 단 몇 ms 안에 결과를 반환할 수 있다는 걸 의도치 않게 알게 된 것이다.
결과적으로 성능 개선을 위한 한 가지 방법을 배우게 되었다.
#마치며
여러모로 배운 점이 많은 과제였다.가장 크게 느낀 점은 왜 성능이 중요한 지 체감할 수 있었다는 것이다.