Skip to content

Commit 68a9873

Browse files
[feat] 미니게임 결과 저장 Bulk Insert 전환으로 DB 커넥션 점유 시간 최소화 (#1096)
* feat: MiniGame 결과 대량 삽입을 위한 Repository 추가 - MiniGameResultBulkRepository 및 구현체(MiniGameResultBulkRepositoryImpl) 추가 - JdbcTemplate을 사용한 bulkInsert 메서드 구현 - BatchPreparedStatementSetter를 활용한 배치 삽입 로직 처리 - MiniGame 결과 대량 처리 성능 최적화 * feat: MiniGame 결과 벌크 저장 로직 추가 및 성능 최적화 - MiniGameResult 벌크 삽입을 위한 List 생성 및 처리 로직 추가 - MiniGameResultJpaRepository에 bulkInsert 메서드 연결 - 로깅 정보에 저장된 플레이어 수 추가 - 결과 저장 성능 개선 및 부하 감소 위해 배열 기반 처리 적용 * feat: MySQL 데이터베이스 Hikari 설정 추가 - hikari.data-source-properties.rewriteBatchedStatements 설정 추가 - 대량 삽입 시 성능 최적화 위한 옵션 활성화 * fix: MiniGame 결과 벌크 삽입 시 빈 리스트 처리 로직 추가 - 결과 데이터가 없는 경우 불필요한 DB 호출 방지를 위해 early return 로직 추가 - MiniGameResultBulkRepositoryImpl의 bulkInsert 메서드에 유효성 검증 로직 보완 * refactor: MiniGame 결과 저장 로직 성능 최적화 및 리팩토링 - Room 플레이어 데이터를 한 번에 조회하도록 findByRoomSessionAndPlayerNameIn 메서드 추가 - PlayerEntity 검색 시 Map 데이터 구조 활용으로 중복 호출 제거 - MiniGameResultSaveEventListener 코드 가독성 및 효율성 개선 * refactor: MiniGameResultSaveEventListener 중복 키 처리 로직 개선 - Collectors.toMap 사용 시 중복 키 발생 처리 로직 추가 - 기존 값 유지로 중복 키로 인한 오류 방지 - 코드를 안전하고 명시적으로 개선하여 가독성 향상
1 parent 2ca178f commit 68a9873

File tree

6 files changed

+89
-9
lines changed

6 files changed

+89
-9
lines changed

backend/src/main/java/coffeeshout/minigame/event/MiniGameResultSaveEventListener.java

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
import coffeeshout.room.infra.persistence.RoomEntity;
2020
import coffeeshout.room.infra.persistence.RoomJpaRepository;
2121
import jakarta.transaction.Transactional;
22+
import java.util.ArrayList;
23+
import java.util.List;
2224
import java.util.Map;
25+
import java.util.function.Function;
26+
import java.util.stream.Collectors;
2327
import lombok.RequiredArgsConstructor;
2428
import lombok.extern.slf4j.Slf4j;
2529
import org.springframework.context.event.EventListener;
@@ -60,24 +64,40 @@ public void handle(MiniGameFinishedEvent event) {
6064
final MiniGameResult result = miniGame.getResult();
6165
final Map<Player, MiniGameScore> scores = miniGame.getScores();
6266

67+
final List<String> playerNames = room.getPlayers().stream()
68+
.map(player -> player.getName().value())
69+
.toList();
70+
71+
final Map<String, PlayerEntity> playerEntityMap = playerJpaRepository
72+
.findByRoomSessionAndPlayerNameIn(roomEntity, playerNames)
73+
.stream()
74+
.collect(Collectors.toMap(
75+
PlayerEntity::getPlayerName,
76+
Function.identity(),
77+
(existing, replacement) -> existing
78+
));
79+
80+
final List<MiniGameResultEntity> resultEntities = new ArrayList<>();
81+
6382
for (Player player : room.getPlayers()) {
64-
final PlayerEntity playerEntity = playerJpaRepository.findByRoomSessionAndPlayerName(roomEntity,
65-
player.getName().value())
66-
.orElseThrow(() -> new IllegalArgumentException("플레이어가 존재하지 않습니다: " + player.getName().value()));
83+
final PlayerEntity playerEntity = playerEntityMap.get(player.getName().value());
84+
if (playerEntity == null) {
85+
throw new IllegalArgumentException("플레이어가 존재하지 않습니다: " + player.getName().value());
86+
}
6787

6888
final Integer rank = result.getPlayerRank(player);
6989
final Long score = scores.get(player).getValue();
7090

71-
final MiniGameResultEntity resultEntity = new MiniGameResultEntity(
91+
resultEntities.add(new MiniGameResultEntity(
7292
miniGameEntity,
7393
playerEntity,
7494
rank,
7595
score
76-
);
77-
78-
miniGameResultJpaRepository.save(resultEntity);
96+
));
7997
}
8098

81-
log.info("미니게임 결과 저장 완료: joinCode={}", event.joinCode());
99+
miniGameResultJpaRepository.bulkInsert(resultEntities);
100+
101+
log.info("미니게임 결과 벌크 저장 완료: joinCode={}, playerCount={}", event.joinCode(), resultEntities.size());
82102
}
83103
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package coffeeshout.minigame.infra.persistence;
2+
3+
import java.util.List;
4+
5+
public interface MiniGameResultBulkRepository {
6+
7+
void bulkInsert(List<MiniGameResultEntity> resultEntities);
8+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package coffeeshout.minigame.infra.persistence;
2+
3+
import java.sql.PreparedStatement;
4+
import java.sql.SQLException;
5+
import java.sql.Timestamp;
6+
import java.util.List;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
9+
import org.springframework.jdbc.core.JdbcTemplate;
10+
11+
@RequiredArgsConstructor
12+
public class MiniGameResultBulkRepositoryImpl implements MiniGameResultBulkRepository {
13+
14+
private final JdbcTemplate jdbcTemplate;
15+
16+
@Override
17+
public void bulkInsert(List<MiniGameResultEntity> resultEntities) {
18+
if (resultEntities.isEmpty()) {
19+
return;
20+
}
21+
22+
final String sql = """
23+
INSERT INTO mini_game_result (mini_game_play_id, player_id, player_rank, score, mini_game_type, created_at)
24+
VALUES (?, ?, ?, ?, ?, ?)
25+
""";
26+
27+
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
28+
@Override
29+
public void setValues(PreparedStatement ps, int i) throws SQLException {
30+
final MiniGameResultEntity entity = resultEntities.get(i);
31+
ps.setLong(1, entity.getMiniGamePlay().getId());
32+
ps.setLong(2, entity.getPlayer().getId());
33+
ps.setInt(3, entity.getRank());
34+
ps.setLong(4, entity.getScore());
35+
ps.setString(5, entity.getMiniGameType().name());
36+
ps.setTimestamp(6, Timestamp.valueOf(entity.getCreatedAt()));
37+
}
38+
39+
@Override
40+
public int getBatchSize() {
41+
return resultEntities.size();
42+
}
43+
});
44+
}
45+
}

backend/src/main/java/coffeeshout/minigame/infra/persistence/MiniGameResultJpaRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import org.springframework.data.repository.Repository;
44

5-
public interface MiniGameResultJpaRepository extends Repository<MiniGameResultEntity, Long> {
5+
public interface MiniGameResultJpaRepository extends Repository<MiniGameResultEntity, Long>,
6+
MiniGameResultBulkRepository {
67

78
MiniGameResultEntity save(MiniGameResultEntity miniGameResultEntity);
89
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package coffeeshout.room.infra.persistence;
22

3+
import java.util.List;
34
import java.util.Optional;
45
import org.springframework.data.repository.Repository;
56

67
public interface PlayerJpaRepository extends Repository<PlayerEntity, Long> {
78
PlayerEntity save(PlayerEntity playerEntity);
89

910
Optional<PlayerEntity> findByRoomSessionAndPlayerName(RoomEntity roomSession, String playerName);
11+
12+
List<PlayerEntity> findByRoomSessionAndPlayerNameIn(RoomEntity roomSession, List<String> playerNames);
1013
}

backend/src/main/resources/application.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ spring:
77
username: ${MYSQL_USERNAME}
88
password: ${MYSQL_PASSWORD}
99
driver-class-name: com.mysql.cj.jdbc.Driver
10+
hikari:
11+
data-source-properties:
12+
rewriteBatchedStatements: true
1013
lifecycle:
1114
timeout-per-shutdown-phase: 5m
1215
data:

0 commit comments

Comments
 (0)