마음만 바쁜 사람
article thumbnail

웹 애플리케이션을 구현하는 우테코 미션 진행 중 전체 테스트 실행 시 특정 Dao 테스트에서 실패하는 경우가 발생했는데,

해당 테스트 코드와 실행 결과를 보면 다음과 같다.

@Test
@Transactional
void findWinners() {
    gameLogDao.insert(3, "달리", 4);
    gameLogDao.insert(3, "디노", 4);
    gameLogDao.insert(3, "저문", 2);

    winnersDao.insert(3, "달리");
    winnersDao.insert(3, "디노");

    assertThat(winnersDao.find(3))
    .isEqualTo(List.of(new Car(new Name("달리"), 4), new Car(new Name("디노"), 4)));
}

 

브리가 나타났다.

내가 원헀던 동작은 디노, 달리, 저문 이 세 명의 참가자 중 디노와 달리가 공동 우승하여 이 둘을 올바르게 반환해 주는지 확인하는 것이었는데, 실제 테스트 결과에는 디노와 달리 말고도 브리라는 참가자를 같이 반환하고 있다. 넣어주지도 않은 브리는 어디서 나온걸까?

 

 

다른 테스트 코드를 찾아 보던 중 근원지를 찾았다.

Post 기능이 잘 동작하는지 확인하는 컨트롤러 테스트인데, Insert 한 데이터가 다른 Select 테스트에서 검출되는 것을 막기 위해 Transactional 어노테이션을 붙여 주었다. (테스트에서는 @Transactionl을 통해 메서드가 끝나면 이전 상태로 롤백 해주기 때문!)

 

하지만 현재 해당 테스트 메서드의 결과가 롤백되지 않았고, 이 때문에 findWinner 테스트에서 브리,토미,브라운 중 1위를 한 사람이 같이 반환되고 있는 상황임을 파악할 수 있었다.

이번엔 컨트롤러 테스트에서 브라운이 우승해 함께 반환되고 있다.

 

그렇다는 건 결국 현재 컨트롤러 테스트의 @Transational 설정이 적용되지 않는다는 소리인데.. 이유가 무엇일까?

 

결론부터 말하자면 테스트 메서드와 해당 메서드에서 호출한 컨트롤러 메서드가 서로 다른 스레드에서 실행되기 때문이다.

트랜잭션이 적용되지 않은 컨트롤러 테스트의 코드를 다시 한 번 살펴보자.

@DisplayName("이름과 실행 횟수 POST")
@Test
@Transactional
void postInput() {
    GameRequestDto gameRequestDto = new GameRequestDto("브리,토미,브라운", 10);
    RestAssured.given().log().all()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .body(gameRequestDto)
            .when().post("/plays")
            .then().log().all()
            .statusCode(HttpStatus.OK.value());
}

해당 코드는 컨트롤러에서 "/plays"로 매핑된 Post 메서드를 호출하고, 에러가 발생하는지를 검증한다.

진행 흐름으로 봤을 때는 Post 함수를 호출해 테이블에 데이터를 삽입하고, 함수가 끝날 때 다시 데이터가 삭제되는 롤백 과정을 거쳐야 하는 것이 정상이지만 결과는 그렇지 않았다.

 

테스트 메서드와 호출되는 컨트롤러 메서드에 현재 사용 스레드를 출력하는 방식으로 비교해 보자.

@DisplayName("이름과 실행 횟수 POST")
@Test
@Transactional
void postInput() {
    GameRequestDto gameRequestDto = new GameRequestDto("브리,토미,브라운", 10);

    // 테스트 함수는 어떤 스레드에서 진행되는지 확인
    System.out.println("Test.postInput Thread: "+Thread.currentThread());

    RestAssured.given().log().all()
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .body(gameRequestDto)
            .when().post("/plays")
            .then().log().all()
            .statusCode(HttpStatus.OK.value());
}

 

@PostMapping
public ResponseEntity<GameResponseDto> postInput(@RequestBody GameRequestDto gameRequestDto) {
    GameResponseDto gameResponseDto = racingGameService.play(gameRequestDto);

    // 컨트롤러의 Post 함수는 어떤 스레드에서 진행되는지 확인
    System.out.println("Controller.postInput Thread: "+Thread.currentThread());

    return ResponseEntity.ok()
            .body(gameResponseDto);
}

Thread.currentThread() 메서드를 출력하면 함수가 실행되는 스레드의 정보를 파악할 수 있다. (간단히 스레드 이름만 보려면 .getName을 추가해 주면 된다.)

 

결과는 과연.?

테스트 함수와 컨트롤러 함수가 실행된 스레드가 서로 다르다. (리스트의 첫 번째 값이 thread name)

테스트 메서드가 실행되는 스레드는 main, 컨트롤러 메서드가 실행되는 스레드는 http-nio-auto-1-exec-1 로 다른 것을 확인할 수 있다.

 

그럼 해당 테스트 코드의 동작 과정을 명확히 파악할 수 있다.

1. 테스트 메서드가 스레드 A 에서 실행된다.

2. 테스트 메서드 내의 코드에서 컨트롤러의 Post 메서드를 호출한다.

3. 호출을 받은 컨트롤러 메서드는 스레드 B에서 실행된다.

4. 테스트 메서드가 완료되면 롤백을 수행한다.

5. 하지만 트랜잭션의 범위는 스레드 A 내로 한정되므로 스레드 B에는 아무런 영향을 끼치지 못한다.

 

테이블에 데이터를 삽입하는 기능은 컨트롤러의 함수, 즉 스레드 B에서 진행되기 때문에 아무리 테스트 코드에 트랜잭션을 적용해도 테이블에는 변화가 없는 것이었다. (테스트 코드의 트랜잭션은 올바르게 작동하고 있었다..!)

 

따라서 현재 컨트롤러 테스트에서는 해당 클래스에 AfterEach 등을 써서 테이블을 초기화 시켜주는 방법 이외에는 삽입한 데이터를 롤백할 수 있는 방법이 없다. 

 

아니면 컨트롤러 테스트 이외의 테스트 클래스들에 추가적인 어노테이션을 적어주어 DB를 초기화 시켜줄 수도 있다.

원하지 않은 결과가 발생했던 findWinner의 클래스 구조를 보면 다음과 같다.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Transactional
public class WinnersDaoTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    private WinnersDao winnersDao;
    private GameLogDao gameLogDao;

    @BeforeEach
    void setUp() {
        winnersDao = new WinnersDao(jdbcTemplate);
        gameLogDao = new GameLogDao(jdbcTemplate);
    }
}

 

트랜잭션을 적용해도 이미 테이블에 데이터가 삽입되어 있기 때문에 의미가 없다. 테이블을 초기화 시켜주거나 새로운 DB를 띄워야 한다.

해결하기 위한 방법은 여러가지가 있다.

 

1. @BeforeEach, @AfterAll 등을 사용해 테이블을 정리한다.

2. @AutoConfigureTestDatabase 를 사용해 테이블을 변경한다.

3. 그냥 @SpringBootTest 대신 @JdbcTest를 쓴다.

 

@JdbcTest를 사용해도 되는 이유는 간단하다. 해당 Dao 테스트에서는 오직 DataSource와 JdbcTemplate만 필요한 테스트이므로 굳이 @SpringBootTest를 사용할 필요가 없다. 그리고 이 두 어노테이션의 선언부를 보면 다음과 같다.

@SpringBootTest의 선언부
@JdbcTest의 선언부

보다시피 @JdbcTest에는 @Transactional과 @AutoConfigureTestDatabase 어노테이션이 모두 적용되어 있다.

따라서 굳이 @SpringBootTest에다 @Transactional과 @AutoConfigureTestDatabase를 붙이는 것 보다 @JdbcTest를 사용하는 것이 더 깔끔해 보인다. (물론 JdbcTest를 사용할 수 있는 테스트일 경우!)

 

 

profile

마음만 바쁜 사람

@훌루훌루

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!