본문 바로가기
우아한테크코스 4기/레벨1

[레벨 1 돌아보기] dao 테스트 중 lock wait?

by 나는후니 2022. 4. 15.

레벨 1에서의 학습목표와는 조금 동떨어진 이야기지만.. Dao를 테스트 하는 과정에서 어떤 에러를 만났습니다.

com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction

에러를 확인해보니 락을 획득하기 위한 시간이 초과되어 트랜잭션을 다시 시작하라 라고 말합니다.

어떤 상황이기에 이 에러가 발생했고, 이 문제를 어떻게 해결하는지 적어보고자 합니다.

락 발생

db의 커밋 모드 설정을 수동 커밋 모드로 전환하면 특정 쿼리가 실행되는 순간 해당 세션에서 트랜잭션이 열립니다. 이를 트랜잭션 a라고 칭하겠습니다.

 

트랜잭션 a는 다른 트랜잭션으로 부터 격리하고 트랜잭션을 하나의 작업으로 마무리해야합니다.

만약 또 다른 트랜잭션 b트랜잭션 a에서 변경되는 row에 접근하려 한다면 트랜잭션 a트랜잭션 b로 부터 변경하고자 하는 row를 지켜야 합니다.

 

이 때 lock 개념이 등장합니다. 트랜잭션 a는 변경하고자 하는 row에 대한 lock을 쿼리를 던지는 순간 획득하고 트랜잭션이 종료될 때까지 반환하지 않습니다. commit혹은 rollback으로 트랜잭션이 종료되어야 lock을 반환하는 것이죠. 트랜잭션 a가 락을 갖고 있을 때 트랜잭션 b가 해당 row에 접근하려면 lock을 취득할 때까지 대기하게 됩니다. 만약 트랜잭션 a가 종료되지 않으면, 타임아웃에 걸려 트랜잭션 b는 위의 에러와 같은 에러를 발생시킵니다.

 

그렇다면 제 코드에서 왜 lock이 발생했는지 알아보겠습니다.

코드

@BeforeEach
void setUp() throws SQLException{
    connection = JdbcTemplate.getConnection(JdbcTestFixture.DEV_URL);
    connection.setAutoCommit(false);
    boardDao = new BoardDaoImpl(connection);
}

매 테스트를 실행하기 전 Connection의 commit 옵션을 수동 커밋으로 변경합니다.

그리고 여러개의 row를 한 번에 업데이트하는 테스트와 하나의 row만 업데이트하는 테스트 코드를 작성합니다.

@Test
@DisplayName("단건 업데이트 후 보드를 가져온다.")
void updateAndGet() {
    boardDao.updatePosition("h2", "white_pawn");
    Map<Position, Piece> initialBoard = BoardFactory.initialize();
    initialBoard.put(Position.valueOf("g2"), new Blank());
    Map<String, String> expected = ChessDto.of(new Board(initialBoard)).getBoard();
    assertThat(boardDao.getBoard()).isEqualTo(expected);
}

@Test
@DisplayName("배치 업데이트 후 보드를 가져온다.")
void updateBatchAndGet() throws SQLException {
    Map<String, String> map = new HashMap<>();

    map.put("g2", "white_pawn");
    map.put("h2", "white_pawn");
    boardDao.updateBatchPositions(map);

    Map<String, String> expected = ChessDto.of(new Board(BoardFactory.initialize())).getBoard();
    assertThat(boardDao.getBoard()).isEqualTo(expected);
}

이제 테스트 코드를 실행해보면 문제가 발생합니다.

이 문제가 왜 발생하는지 보기 위해 현재 진행 중인 트랜잭션을 살펴보았습니다.

위 트랜잭션 리스트를 보면 7601 트랜잭션LOCK WAIT라는 상태로 대기 중입니다. 즉 LOCK을 받지 못하고 기다리고 있는 것이죠.

 

왜그럴까요?

// 배치 update
Map<String, String> map = new HashMap<>();

map.put("g2", "white_pawn");
map.put("h2", "white_pawn"); // position이 h2인 row의 lock을 획득한다.
boardDao.updateBatchPositions(map);

// 단건 update
boardDao.updatePosition("h2", "white_pawn"); // position이 h2인 row의 lock이 배치 update trx에서 사용중이기 때문에 획득 불가

batch update 테스트 코드를 보면 트랜잭션을 다시 원래 상태로 돌리는 rollback 을 하지 않고 있습니다. 따라서 트랜잭션이 종료되지 않고 유지되며 lock 또한 반환하지 않고 있죠. lock이 다른 트랙잭션에 있는 row로 접근하려 하니 에러가 발생하게 된 것입니다.

해결

해결방법은 간단합니다. 트랜잭션을 종료하고 lock을 반환하면 됩니다.

@Test
@DisplayName("배치 업데이트 후 보드를 가져온다.")
void updateBatchAndGet() throws SQLException {
    Map<String, String> map = new HashMap<>();

    map.put("g2", "white_pawn");
    map.put("h2", "white_pawn");
    boardDao.updateBatchPositions(map);

    Map<String, String> expected = ChessDto.of(new Board(BoardFactory.initialize())).getBoard();
    assertThat(boardDao.getBoard()).isEqualTo(expected);
    connection.rollback();
}