@ MybatisTest 트랜잭션 동작하지 않는다?

Mugeon Kim·2024년 2월 20일
0
post-thumbnail

서론


  • Mybatis를 사용한다. 도중에 테스트 코드를 작성하면서 (@MybatisTest) 트랜잭션이 동작하지 않는 문제점을 발견을 했다.
  • 테스트를 위해서 스프링부트에서 지원하는 @MybatisTest를 작성하고 write 작업을 작성을 하였습니다. 테스트를 통과하고 데이터베이스를 확인해봤는데, 롤백을 기대를 하였는데 데이터가 들어가 있었다.

본론


이슈

  • 데이터베이스에는 단위테스트를 생성하고 데이터가 쌓이고 있었다. 여기에서 나는 트랜잭션이 문제가 있다고 생각을 하였다.

  • 그러면 코드를 보면서 문제를 살펴보겠습니다.

@MybatisTest
@Transactional(value = "transactionManager")
@Import({DataSourceConfig.class, LadderDAO.class})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class LadderDAOTest
  • 일단 dao 부분을 검증하는 코드이다. @MybatisTest를 살펴보면 트랜잭션이 선언이 되어져 있다. 하지만 DataSourceConfig에서 DB와 트랜잭션을 선언을 하였기 때문에 추가로 설정을 하였다.

  • 처음에는 MybatisTest의 문제라고 생각하여 @SpringbootTest를 하였지만 똑같은 결과가 발생을 하였다.

원인, 해결

  • 일단 많은 삽질을 하였지만 결국은 스토리지 엔진의 문제였습니다. 우리가 흔히 알고있는 MySQL과 MariaDB는 비슷하고 거의 같다고 알고있다. MySQL의 5.3버전 이전에는 MyISAM을 기본적인 스토리지 엔진을 사용을 하였지만 이후부터 InnoDB을 기본적인 엔진으로 선택을 하였다.
  • 이 중에서 가장 큰 차이는 트랜잭션의 지원 여부라고 생각한다. 문제는 내가 테스트를 하고 있었던 테이블은 MyISAM을 사용하고 있어서 트랜잭션이 정상적으로 동작을 하지 못하고 있었다.

mysql, mariadb은 백업의 기능이 없다. 그래서 테스트를 진행하기 이전에 Copy table, Insert Into를 통하여 테스트 테이블을 만들고 개인적으로 수행한다. 그런데 이때 Copy table을 통해서 생성하면 MyiSam으로 생성이 되는 경우가 있어서 문제가 발생을 하였다.

CREATE TABLE new_table_name LIKE old_table_name;
INSERT INTO new_table_name SELECT * FROM old_table_name;
ALTER TABLE new_table_name ADD PRIMARY KEY (column_name);

혹시 MySQL에 대해서 궁금하면 이전에 정리한 내용을 보면 좋을거 같다.
https://velog.io/@geon_km/MySQL-8.0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98


show table status
where name = '테이블 이름';
  • 다음 쿼리를 실행을 하면 스토리지 엔진을 확인할 수 있다. 그런데 궁금한 부분은 스토리지 엔진에 MyISAM과 InnoDB가 불규칙적으로 되어져 있었다.

  • 이 문제는 처음에 설정의 문제라고 생각을 했지만 문제는 테이블별로 내가 저장소 엔진 설정을 하지 않았다는 점이었다.

  • 그러면 테스트 코드에 대해서 살펴보자
@Test
public void testGameLeaderboard() throws Exception {
    //given
    final String gameCode = "game1";
    final String eventCode = "event1";
    final String roundCode = "round1";
    final String trackCode = "track1";
    final String carCode = "car1";
    final String playerName = "Player1";
    final String country = "Country1";
    final String countryCode = "C1";
    final String lapTime = "1:00.000";
    final int totalLaps = 10;
    final int totalTime = 600000;

    LadderVO gameInfo = LadderVO.builder()
            .game_code(gameCode)
            .event_code(eventCode)
            .round_code(roundCode)
            .track_code(trackCode)
            .car_code(carCode)
            .nickname(playerName)
            .country(country)
            .country_code(countryCode)
            .lap_time(lapTime)
            .total_time(String.valueOf(totalTime))
            .build();

    //when
    ladderDAO.insertLadderInfo(gameInfo);

    List<LadderVO> leaderboard = ladderDAO.getLadder();
    List<LadderVO> bestLeaderboard = ladderDAO.getBestLadder();
    assertAll(
            ()->assertThat(leaderboard).extracting(LadderVO::getNickname)
                    .contains(playerName),
            ()-> assertThat(leaderboard).extracting(LadderVO::getGame_code)
                    .contains(gameCode),
            ()-> assertThat(leaderboard).extracting(LadderVO::getEvent_code)
                    .contains(eventCode),
            ()-> assertThat(leaderboard).extracting(LadderVO::getRound_code)
                    .contains(roundCode),
            ()-> assertThat(leaderboard).extracting(LadderVO::getTrack_code)
                    .contains(trackCode)

    );

    assertAll(
            ()->assertThat(bestLeaderboard).extracting(LadderVO::getNickname)
                    .contains(playerName),
            ()-> assertThat(bestLeaderboard).extracting(LadderVO::getGame_code)
                    .contains(gameCode),
            ()-> assertThat(bestLeaderboard).extracting(LadderVO::getEvent_code)
                    .contains(eventCode),
            ()-> assertThat(bestLeaderboard).extracting(LadderVO::getRound_code)
                    .contains(roundCode),
            ()-> assertThat(bestLeaderboard).extracting(LadderVO::getTrack_code)
                    .contains(trackCode)
    );
}

@MybatisTest

  • 각 layer별로 테스트의 전략을 고민을 하였다. dao는 결국 db와 데이터 정합성을 측정이 가장 큰 목적이라고 생각하여 @MybatisTest를 통하여 순수한 mybatis의 테스트를 수행하기 위해 사용을 했다.

의존성

  • 위의 테스트를 진행하기 위해서는 의존성을 먼저 build.gradle에 추가를 한다.
dependencies {
    testImplementation("org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.2")
}

DAO

@Repository
public class MemberDao {

    private final SqlSession sqlSession;

    public CityDao(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    public Member selectMemberById(long id) {
        return this.sqlSession.selectOne("selectMemberById", id);
    }

}
  • sqlsession을 통해 스프링 트랜잭션 설정에 따라 자동으로 커밋 또는 롤백을 수행하고 쓰레드에 안전한게 스프링 빈으로 주입이 될 수 있게 만든다.

테스트

@MybatisTest
@Import(MemberDao.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class MemberDaoTest {

    @Autowired
    private MemberDao memberDao;

    @Test
    public void selectCityByIdTest() {
        Member member = cityDao.selectMemberById(1);
        assertThat(member.getName()).isEqualTo("김무건");
        assertThat(member.getState()).isEqualTo("공부중");
        assertThat(member.getCountry()).isEqualTo("한국");
    }

}
  • import를 통해서 dao에 대한 정보를 얻고 만약에 datasource로 추가적인 설정을 하였다면 그 부분도 추가하면 된다.

참고


https://jehuipark.github.io/note/transaction-do-not-work-issue

profile
빠르게 실패하고 자세하게 학습하기

1개의 댓글

comment-user-thumbnail
2024년 5월 28일

좋은 내용 잘 보고 갑니다!

답글 달기