Level 1 체스 미션 중, 반복 수행되는 DB 여러 행의 삽입을 최적화하기 위해 고민한 내용을 적어봤습니다.
1. Console 버전 체스 초기판 구성
private static final Map<Position, Piece> INITIAL_BOARD = new HashMap<>();
static {
setupOthersPieces(RANK_EIGHT, Color.BLACK);
setupPawns(RANK_SEVEN, Color.BLACK);
setupPawns(RANK_TWO, Color.WHITE);
setupOthersPieces(RANK_ONE, Color.WHITE);
}
초기 콘솔 버전은 단순 Map 자료구조에 좌표값과 좌표에 해당하는 기물만 저장하는 방식이었습니다.
따라서 static 블럭에서 모든 것이 해결 가능했죠.
하지만 게임 진행 상황을 DB에 연동하게 되면서, 상황이 달라졌습니다.
2. Map 자료구조를 DB에 그대로 구현
create table board
(
gameId int null,
position varchar(2) null,
piece varchar(2) null
);
이번 미션의 목적은 도메인의 응집도 향상과 이를 통한 장점을 경험하는 것이었다고 생각합니다.
View를 콘솔로만 사용하다가, Web View와 DB가 연동되었지만 도메인의 수정은 일부만 필요했던 것이죠.
그런데 한 가지 불편한 부분이 생겼습니다.
새로운 게임방이 생성될 때마다,
게임방별 좌표와 기물 정보를 저장하는 board 테이블에
초기값 32행을 INSERT 해줘야 하는 상황이 발생한 거죠.
3. 초기 접근
private void insertPiecesOnPositions(String gameId) {
final String sql = "insert into position values (?, ?, ?)";
final Map<String, String> initialBoard = BoardFactory.newBoardForDB();
for (Entry<String, String> pieceByPosition : initialBoard.entrySet()) {
try (final Connection connection = getConnection();
final PreparedStatement preparedStatement = connection.prepareStatement(sql)
) {
preparedStatement.setString(1, gameId);
preparedStatement.setString(2, pieceByPosition.getKey());
preparedStatement.setString(3, pieceByPosition.getValue());
preparedStatement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
가장 처음 시도한 방법은, Console 버전에서 사용한 Map 자료구조를 그대로 가져와서
전체를 순회하며 그것을 DB에 저장하는 방식입니다.
심지어 초기 코드에는 connection마저 매 순회마다 새로 생성하고 있군요 😱
덕분에 새로 방을 만들 때에는 화면에 기물이 그려지기까지 2.3초나 소요되었습니다.
4. 테이블 복제 시도
create table initialBoard
(
initialPosition varchar(2) null,
initialPiece varchar(2) null
);
INSERT INTO initialBoard (initialPosition, initialPiece)
VALUES ('A8', 'rb') , ('B8', 'nb') , ('C8', 'bb') , ('D8', 'qb')
, ('E8', 'kb') , ('F8', 'bb') , ('G8', 'nb') , ('H8', 'rb')
, ('A7', 'pb') , ('B7', 'pb') , ('C7', 'pb') , ('D7', 'pb')
, ('E7', 'pb') , ('F7', 'pb') , ('G7', 'pb') , ('H7', 'pb')
, ('A2', 'pw') , ('B2', 'pw') , ('C2', 'pw') , ('D2', 'pw')
, ('E2', 'pw') , ('F2', 'pw') , ('G2', 'pw') , ('H2', 'pw')
, ('A1', 'rw') , ('B1', 'nw') , ('C1', 'bw') , ('D1', 'qw')
, ('E1', 'kw') , ('F1', 'bw') , ('G1', 'nw') , ('H1', 'rw');
콘솔 체스미션에서는 Map 자료구조에 담아둔 초기 체스판 원본을 미리 만들어두고,
요청에 따라 그것을 복제해서 사용하는 방식이었습니다.
이 방식을 그대로 DB에서 사용해보고 싶었습니다.
도커 엔트리포인트의 DB초기화 설정으로 위 쿼리를 설정했습니다.
원본 테이블을 만들어 둔 것입니다.
그리고 다른 테이블의 값을 그대로 복제하기 위한 쿼리를 찾아봤고 아래와 같이 Java 코드가 개선될 수 있었습니다.
private void insertPiecesOnPositions(String gameId) {
final String sql = "insert into board(gameId, position, piece) select ?, initialPosition, initialPiece from initialBoard";
try (final Connection connection = getConnection();
final PreparedStatement preparedStatement = connection.prepareStatement(sql)
) {
preparedStatement.setString(1, gameId);
preparedStatement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
insert into board(gameId, position, piece)
select ?, initialPosition, initialPiece
from initialBoard
initialBoard의 initialPosition, initialPiece 행 모두 그대로 가져오고,
매번 변경되는 gameId 컬럼만 동적으로 전달해줌으로써, 단 한번의 쿼리만 전달하도록 개선되었습니다.
커넥션 비용을 제외하더라도, 순수 DB 성능적으로만 접근해도
여러개의 요청을 하나로 묶어서 전달하는 것이 성능 개선을 가져다 줄 것으로 기대되었지만,
Java 코드까지 간결해지는 부수적인 이득도 있었습니다.
2.3초에 달하던 방 생성 응답시간도 0.5초대로 개선되었습니다.
2022.04.24 추가
이 때만 해도 레벨 2에서도 체스를 할 줄 몰랐었죠 하하하
Spring으로 전환하고 JDBC Template을 사용하는 것으로 변경한 이후
응답시간 0.15초로 개선되었습니다 😂
5. 여러행 Insert 처리에 대한 성능 최적화
여기까지 진행하고 나서 또다른 궁금증이 생겼습니다.
여러 행을 INSERT해야 할 때,
한 번의 요청에 많은 값을 전달하는 경우 어느정도 성능 향상이 있을까 였죠.
찾아본 결과 정확히 어느 정도다 라고 단언할 수는 없지만,
분명한 성능 향상이 있을 거라는 점 정도는 확인할 수 있었습니다.
If you are inserting many rows from the same client at the same time,
use INSERT statements with multiple VALUES lists to insert several rows at a time.
This is considerably faster (many times faster in some cases) than
using separate single-row INSERT statements.
bulk insert 에 대해서는 이 악물고 일단 무시했습니다..
할 게 너무 많네요 하하... 다음 기회에 알아보는 걸로..
6. 간단한 테스트
인텔리제이에 내장되어 있는 DataGrip 기능을 이용해 INSERT를 시도해봤습니다.
32행 정도의 일괄 삽입은 1행 삽입과 성능 차이가 거의 없음을 알 수 있었습니다.
여러 행 삽입이 필요한 경우, 가능하다면 한 번의 쿼리 전달로 처리하는 것이
성능을 위해 더 나은 선택이 될 것 같습니다.
'우아한테크코스 4기' 카테고리의 다른 글
Spring 의존성 주입 방법 중 생성자 주입을 사용해야 하는 이유 (0) | 2022.04.22 |
---|---|
Level 1을 정리하는 레벨 인터뷰 후기 (2) | 2022.04.22 |
🤔 도메인(domain)은 무슨 뜻일까?! (2) | 2022.03.25 |
객체의 행동으로 표현되는 책임과 역할 (객체지향의 사실과 오해) (2) | 2022.03.25 |
💻 코딩을 지탱하는 기술을 읽었습니당 ! (4) | 2022.03.08 |