사이드 프로젝트/AgileHub

간헐적으로 JUnit5 테스트가 깨지는 문제

EVO. 2024. 4. 1. 01:10

문제 사항

테스트코드가 40개를 넘어가면서 갑자기 그 이후로 간헐적으로 깨짐이 발생했다. 

처음 생겼던 에러는 다음과 같았다.

org.hibernate.exception.JDBCConnectionException: Unable to release JDBC Connection [The database has been closed [90098-224]] [n/a]

 

 

문제해결 시도

해당 에러를 검색해보니 원인은 다양했다.

 

1. H2 데이터베이스 설정 확인

 

H2 데이터베이스가 테스트 하나 실행하고 닫지 않도록 DB_CLOSE_DELAY=-1; 을 추가했다.

테스트가 독립적으로 실행하니깐 H2에 필요로 하는 커넥션이 많아진것으로 예상이 되어 HikariCP maximum pool size을 100개로 늘렸다.

  datasource:
    url: jdbc:h2:~/agile-hub;Mode=MySQL;DB_CLOSE_DELAY=-1;
    username: sa
    password:
    driver-class-name: org.h2.Driver
    hikari:
      maximum-pool-size: 100
      minimum-idle: 5
      idle-timeout: 300000
      max-lifetime: 1800000
      connection-timeout: 30000

 

결과: 여전히 간헐적으로 테스트코드가 실패한다. 

 

2. 동시성 문제

 

테스트코드는 동시성문제가 생길 수도 있다.

테스트코드안에 메서드마다 @Transactional을 적절하게 구성하면 될줄 알았지만 여전히 실패해 테스트가 오래걸리더라도 동시 실행 제한을 할 수 있는 방법을 찾아보니 Gradle 내에서 테스트 프로세서를 줄이는 방법이 존재했다.

 

maxParallelForks = 1

 

결과적으로 실패는 여전히 간헐적으로 발생했다.

 

3. 오류 메시지를 다시 좀 더 살펴보니 다음과 같은 에러 메시지도 존재했다.

 

해당 에러메시지를 쳐보니 대충 청크파일이 깨졌다와 데이터베이스 파일의 크기를 조정하려고 할 때 내부적으로 일관성 검사를 실패했다. 등등 알 수 없는 말이 많았다. 하지만 그래도 여기서 힌트를 얻을 수 있었는데 파일 이라는 키워드 였다. 파일? 나는 메모리 기반으로 테스트를 돌리고 있는데 이게 무슨 소리지 하고 인메모리 H2 설정파일을 검색하여 다시 봤다.

https://www.baeldung.com/spring-boot-h2-database 

 

`spring.datasource.url=jdbc:h2:mem:testdb`인데 기존에 나는  테스트 설정파일을 아래와 같이 설정했다.

 

`jdbc:h2:~/` 는 파일 기반 H2 데이터베이스를 사용했었던 것이다. 아직 잘 모르겠지만 일단 인메모리 기반 H2를 쓰기로 했으니 위로 교체하니 이제 정상적으로 테스트가 통과되었다.

 

 

그래서 왜 파일은 안되고 인메모리 기반 H2로는 테스트가 원할하게 되었는가

이미 공식문서 상에서도 test 상에서만 쓰는 용도로 인메모리 기반 H2를 사용한다. 

 

MVStore에 대해 더 알아보자 

MVStore

이 MVStore은 H2 서브시스템의 기본 저장소로 사용된다. JDBC나 SQL 사용하지 않고도 심지어 애플리케이션에서도 사용이 가능하다.

실제로 검색하면 나온다.

 

 

그래서 다시 아까처럼 H2 파일 기반으로 바꾸고 의심가는 곳에 다트를 찍어봤다. 그다음 테스트 케이스를 실행시켜보자.

역시나 MVStore을 사용하고 있다.

 

아직 잘 모르겠다. 하지만 일단 문서를 보면 동시에 쓰고 읽는 것을 지원하는 걸 보니 간헐적으로 실패하는 이유가 분명히 있을 것 같다.

 

오류 메세지를 다시 보면 RandomAccessStore에서 예외가 시작되었으니 거기에도 도트를 표시해놓겠다.

항상 이 메서드를 거치는 데 이 메서드는 스토어 파일의 크기를 축소할 수 있는지를 평가하고, 축소가능한 경우 실제로 파일 크기를 줄이는 역할을 한다. 아직도 뭔소리인지 모르겠는데 어설션 체크를 할 때 getFileLengthInUse()measureFileLengthInUse() 를 비교한다. 그 둘의 코드를 보도록 하자.

 

둘의 코드는 여전히 어렵다 하지만 GPT가 있으니 뭐하는 건지 짐작을 해보자. 

 

getFileLengthInUse 메소드

이 메소드는 freeSpace.getLastFree()를 호출하여 파일의 사용 중인 길이를 반환합니다. 이 방식은 내부적으로 관리되는 freeSpace 객체의 최근 상태를 기반으로 하며, 이는 아마도 할당되지 않은 공간의 경계를 나타냅니다. 이 방식은 파일의 실제 사용 공간보다는 할당된 공간의 마지막 포인트를 반환하는 것으로 보입니다.

measureFileLengthInUse 메소드

반면, measureFileLengthInUse 메소드는 데이터베이스 내의 모든 청크를 반복하여 실제로 할당된 공간의 크기를 측정합니다. isAllocated() 메소드를 통해 할당된 청크만을 고려하고, 해당 청크의 위치(c.block)와 길이(c.len)를 사용하여 파일 내에서 사용 중인 최대 오프셋을 계산합니다. 그 후, 계산된 최대 오프셋에 BLOCK_SIZE를 곱하여 실제 사용 중인 파일 길이를 도출합니다.

 

계산 방식의 차이가 있는데 특히 다중 스레드 환경에서 getFileLengthInUse는 마지막으로 할당된 공간의 위치를 기준으로 길이를 계산하는 반면 measureFileLengthInUse는 실제로 사용량을 측정하는 시점 사이에 데이터베이스 파일의 상태가 변할 수 있다. 

 

정리하면 파일 기반으로 H2 데이터베이스를 사용하면 데이터 접근 시 데스크 I/O가 필요하며 동시성 문제가 발생할 확률이 높은 것이다. (위 처럼 계산 결과가 달라질 수가 있다. I/O 작업은 꽤 시간이 걸리기 때문에) 

인메모리는 디스크 I/O에 비해 RAM을 통해 접근하기 때문에 훨씬 빠르고 동시성 관리도 더욱 간결하다. (지연이 없다고 봐도 무방하다) 실패 가능성이 0%로 없다고는 할 수 없지만 확실히 위의 어설션 오류가 발생할 확률은 현저히 줄어든다. 

 

결론: 기본적으로 JUnit 테스트 할때는 단일 스레드에서 순차적으로 실행이 되지만, 테스트 환경 내에서의 비동기 작업, 외부 시스템 상호작용, 데이터베이스 상태 변화등에 의해서 동시성 문제는 피하기 힘들어 보인다. 따라서 테스트코드에서는 인메모리 H2 그냥 쓰면 된다..