brunch

You can make anything
by writing

C.S.Lewis

by 최진영 May 19. 2022

테스트 격리 라이브러리 개발기

@SpringBootAcceptanceTest

 우아한 테크 코스 레벨 2를 진행하면서 스프링을 사용한 미션이 진행되었는데요. 미션을 진행하면서 스프링에 대한 부분들도 물론 어려웠지만 가장 어렵다고 생각했던 것은 스프링에서 테스트하기였습니다.

 프레임워크를 사용하기 때문에 프레임워크라는 환경에 종속적인 테스트가 진행되었기 때문에 이전 단위 테스트만 했을 때와는 조금 더 여러 가지를 고려했어야 했어요. 그중 가장 신경 써야 했던 부분은 "테스트 격리"입니다.


 각각의 테스트가 서로 영향이 가지 않게 테스트를 격리시켜야 하는데 DB를 사용하는 테스트 환경인 경우 DB에 데이터가 들어가서 다른 테스트에 영향이 갈 수 있는데요. 일단 테스트 격리 라이브러리에 대해서 이야기하기 전에 어떤 테스트 방법이 있었는지 알아보고, 해결하기 위해서 만든 라이브러리 개발 과정을 이야기할까 해요.




테스트 격리 방법


1. @Transactional

 가장 처음으로 테스트에 @Transactional 어노테이션을 달면 됩니다.

 @SpringBootTest라는 통합 테스트 환경에서는 트랜잭션이 모두 커밋이 되기 때문에 따로 롤백이 되지 않는데요. 이 때문에 데이터가 모두 커밋이 되어 한번에 모든 테스트를 돌리게 될 경우 서로의 테스트가 영향이 가게 됩니다. 

@Transactional 을 통한 테스트 격리

 따라서 위와 같이 @Transactional을 달아서 테스트마다 롤백시켜 DB에 커밋이 되지 않게끔 하여 격리를 할 수 있습니다. @SpringBootTest에는 @Transactional이 붙어있지 않지만 @JdbcTest라는 슬라이싱 테스트에는 이미 @Transactional을 달아주었기 때문에 따로 붙여주지 않아도 돼요.


2. 컨텍스트 초기화

 @SpringBootTest 같은 경우 기본적으로 webEnvironment가 Mock으로 되어있기 때문에 위와 같이 @Transcational로 롤백을 시킬 수 있는데요. 문제는 지금부터 사용할 RANDOM_PORT와 같은 실제 서버를 띄우는 테스트 환경에 적용이 되지 않는다는 점이에요.

 랜덤 포트 혹은 정해진 포트로 실제로 서버를 띄우는 환경으로 테스트를 하는 경우 트랜잭션 커밋까지 전부다 진행된 상태를 만들다 보니 롤백을 시킬 수 없습니다.


 따라서 각각의 테스트를 격리시키기 위해서 아예 스프링 컨텍스트를 모든 테스트를 실행할 때 새로 띄우는 방법이 있어요.

매 테스트마다 컨택스트를 새로 띄움

 다만 이렇게 될 경우 컨택스트가 올라가는 시간이 오래 걸리기 때문에 테스트가 여러 개라면 그만큼 시간이 더 오래 걸리는 단점이 있습니다.


3. 데이터 삭제 메서드 추가

 단순히 모든 데이터를 삭제하는 쿼리를 하나 두고 테스트가 진행될 때마다 데이터를 삭제하는 방법입니다.

deleteAll()

 다만 이렇게 될 경우 테스트만을 위한 쿼리 메서드가 추가가 되는 단점이 있습니다.


4. DB 초기화

 DB를 초기화하는 sql파일을 만들어 @Sql 어노테이션을 통해서 실행시키는 방법입니다.

 이런 테이블을 초기화하는 쿼리 파일을 하나 생성하고, 아래와 같이 실행시키면 컨텍스트를 매 테스트마다 초기화하지 않아도 되고, 테스트만을 위한 메서드가 프로덕션 코드에 추가되지도 않습니다.

@Sql을 통해서 스키마 파일을 실행시킴


더 좋은 방법은 없을까?


 RANDOM_PORT와 같은 트랜잭션 롤백을 시킬 수 없는 환경에서 스프링 내에서 간단하게 테스트 격리를 제공하는 방법을 찾아봤지만 사실 별다른 방법을 찾을 수는 없었어요.


 테스트를 위한 프로덕션 메서드를 만드는 것도 싫었고, 그렇다고 DB 초기화 방법도 싫었어요. 한번 만들어두고 수정이 있지 않는다면 상관없지만, 테이블이 추가, 삭제되면 해당 파일을 수정하기도 귀찮고 fk가 걸린 순서를 항상 생각해주어야 했거든요.


 그래서... 만들어왔습니다



@SpringBootAcceptanceTest


 어떻게 하면 이런 일련의 과정을 단순화할 수 있을까를 가장 먼저 생각한 것 같아요.

 지금까지 단순하게 사용했던 @SpringBootTest처럼 어노테이션만 하나 달면 원하는 테스트를 진행할 수 있게 하도록 하는 것이 제1목 표였죠.

 그러던 와중 @SpringBootTest 어노테이션을 유심히 살펴보게 되었는데요,

@SpringBootTest 어노테이션 구조

 이때 SpringExtension을 보고 Junit의 Extension기능을 확장하여 사용한다면 제가 원하는 기능을 만들 수 있다는 것을 떠올릴 수 있었어요.

 테스트가 진행되기 전 원하는 세팅을 하고, 테스트가 끝나면 다른 테스트에 영향이 가지 않게 DB를 초기화하는 로직을 만드는 방향으로요.


Extension

 저희가 원하는 동작은 "테스트가 진행되고 나서 truncate 쿼리들을 만들어 실행한다"에요. 이를 위해서 Junit5에서 Extension을 통해서 위의 행위 자체를 확장하여 사용하려고 했어요.


 Extension을 만드는 방법에는 여러 가지가 있지만 저는 @AfterEach와 같이 테스트 동작들이 실행되고 나서 초기화되는 동작이 필요하기 때문에 "Lifecycle Callbacks"와 관련된 Extension을 사용하려고 해요. 아래와 같은 인터페이스들을 재정의함으로써 사용할 수 있는 거죠.


BeforeAllCallback, AfterAllCallback : 지정한 위치의 모든 테스트 메서드가 실행되기 전, 후

BeforeEachCallback, AfterEachCallback : 각 테스트 메서드가 실행되기 전, 후

BeforeTestExecutionCallback, AfterTestExecutionCallback : 테스트 메서드 직전, 직후


 여기서 지금 당장 필요한 기능은 "AfterEachCallback"가 되겠네요. 상속받게 되면 아래와 같이 테스트가 실행되고 난 후의 실행 로직을 만들 수 있습니다.

Extension 초기 세팅

 그럼 이제 모든 테이블을 truncate 하는 로직을 개발하면 되겠네요 :)


DatabaseCleanerExtension

 기본적으로 동작하는 로직은 "모든 테이블을 검색해서 truncate 하고 auto increment를 1로 변경한다"에요.

 그렇게 하기 위해서는 현재 테이블을 모두 받아와야 했고, 이를 위해서 context에서 JdbcTemplate을 받아와서 사용했어요.

SpringExtention을 통해 컨텍스트를 가져와 JdbcTemplate 사용


 사실 그 이후의 과정은 간단해요.

 테이블 정보를 가져오기 위해서 connection을 가져오고, connection을 통해서 테이블 정보를 가져오고 테이블 네임을 받아와서 아래와 같이 쿼리를 만들어 실행해주죠.

테이블 이름을 가져와 원하는 쿼리를 만들어 실행

 여기서 한 가지 추가된 점은 아까도 말했다시피 fk의 순서에 따라 truncate 동작이 문제가 발생할 수 있기 때문에 전체 쿼리를 동작시키기 전 fk 옵션을 끄고, 모든 쿼리를 날린 후 fk 옵션을 다시 켜주도록 했어요.



 다만 문제가 되었던 부분은 부끄럽게도 Connection 부분이 었는데요. JdbcTemplate에서 Connection을 바로 가져와서 사용하기 때문에 Connection에 대한 관리가 분리되어 try-resource를 통해서 바로 close 해주어야 하는데 JdbcTemplate이 Connection을 관리해준다라고 생각해서 Connection이 부족해서 테스트가 터지는 해프닝이 있었습니다 ㅎㅎ...

resource 관리는 항상 철저하게 close 하게....


 당연히 이 동작이 전체가 잘 돌아가는지 테스트도 짜야겠죠. (테스트는 단순해서 생략할게요)

 그럼 테스트도 잘 동작하겠다. 다음으로 테스트를 실행하기 전 항상 시켜야 하는 Port 설정을 할게요.


RestAssuredPortSetUpExtension

 저희가 미션 내에서 사용하는 RestAssured의 경우 사용하는 port를 지정해주어야 하기 때문에 테스트가 진행되기 전 항상 port를 지금 돌아가고 있는 로컬 테스트 환경의 port로 변경해주어야 해요.

 그래서 똑같이 Context를 가져오고, Context에서 port 정보를 가져오고 RestAssured의 port를 해당 port로 변경해주는 Extension을 만들어주었어요.

 단, BeforeEachCallback이라는 테스트가 실행되기 이전에 실행시키는 Extension을 사용했어요.

port 설정 Extension


@SpringBootAcceptanceTest

 마지막으로 작업하는 작업은 저희가 처음으로 목적으로 삼았던 "하나의 어노테이션만 달아도 될 수 있게 하고 싶다"에요.

 그래서 어노테이션 하나를 만들고, 지금까지 만든 Extension과 @SpringBootTest를 적용해주면 완료됩니다!



Jitpack


 위의 어노테이션을 만드는 과정은 사실 그렇게 어렵지는 않았는데 Jitpack에 Spring 의존성이 달린 프로젝트를 올리는 게 조금 이슈가 컸던 것 같아요 :(

 아무래도 해보지 않았던 영역이고 docs를 꼼꼼히 살펴보지 않은 제 잘못이 크지 않았나 싶습니다 ㅎㅎ....


 각설하고 Jitpack에 대해서 간단히 이야기하자면, 자기가 만든 라이브러리를 jar 형태로 배포하여 다른 프로젝트에서 사용할 수 있도록 라이브러리화하는 방법이에요. 저희가 dependency로 받아서 사용하는 다른 라이브러리처럼 저희가 만든 라이브러리를 그렇게 쓰는 거죠.


plugins

 가장 먼저 plugin 세팅이에요. 여기서 3일 썼는데요....

 보통 프로젝트를 생성하면 "org.springframework.boot" plugin이 추가가 되는데 해당 플러그인을 지우지 않으면 bootjar 때문에 정상적으로 jitpack에 배포되지 않습니다...

 "maven-publish"의 경우 더 아래에서 이야기할게요.


dependencies

 배포 시 환경에서 무조건 사용되어야 할 dependency들은 이렇게 전부 명시해주어야 합니다. api로 사용하게 될 경우 해당 라이브러리들을 전부 같이 배포해요. 다만 배포한 라이브러리를 사용한 프로젝트에서 라이브러리를 사용하고 있다면 해당 버전에 따라갑니다.


maven-publish

 maven-publish를 위한 부분이에요.

 jitpack docs(https://jitpack.io/docs/BUILDING/)를 보면 gradle을 통해서 배포 시 "maven" 혹은 "maven-publish" 플러그인이 활성화되어있어야 한다라고 되어있기 때문에 이를 사용하기 위해서 명시한 publishing 동작이라고 볼 수 있어요.


 릴리즈 태그 달고 jitpack에 배포하는 과정은 docs나 다른 곳에도 나와있고 과정이 단순해서 따로 작성하지는 않겠습니다.


결과 및 회고


https://github.com/jinyoungchoi95/spring-boot-acceptance-test

 모든 과정이 끝나고 어노테이션만으로도 통합 테스트 환경에서 편하게 테스트 격리를 할 수 있는 저만의 라이브러리를 만들었어요. 아래와 같이 라이브러리만 간단하게 추가한다면?

라이브러리 사용법
어노테이션 하나만!

 쉽고 빠르게 통합 테스트 환경에서 테스트 격리를 맛볼 수 있습니다.


 아직도 사실 많은 이슈가 남아있어요.


h2 2점대 이상에서는 호환이 되지 않음(다른 info 테이블 생성으로 인함)

h2 sql 문법을 사용하고 있기 때문에 혹시나 다른 테스트 환경일 때는 쿼리가 동작하지 않음(connection 종류마다 쿼리 변경 필요)

RandomPort를 강제하고 있기 때문에 DEFINED_PORT 상황에서도 사용할 수 있게끔 변경해야 함


 이런 부분들은 차근차근 해결해나가려고 합니다 :)


 사실 최근 있었던 개발 중 가장 재미있었던 경험인 것 같아요. 새로운 것을 만든다라기보다는 기존에 존재하는 것을 배운다라는 방향이 컸기 때문에 뭔가 만들어보고 싶다는 욕구가 많았어요.

 물론 5일 동안 다른 것들을 손에 못 잡고 하루 종일 라이브러리 만드는데 힘이 빠지긴 했지만 나름 결과물과 만들었던 과정에서 배웠던 것들이 남아있어서 뜻깊은 프로젝트가 될 것 같습니다.



참고 링크

A Guide to JUnit 5 Extensions

Guide to publishing libraries

JitPack / 쉽게 라이브러리 배포하기 / Spring boot 프로젝트 배포시 확인할 것


작가의 이전글 2021년 취준 개발기 회고
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari