인프런 커뮤니티 질문&답변

shanePark님의 프로필 이미지
shanePark

작성한 질문수

토비의 스프링 부트 - 이해와 원리

스프링 부트의 자동 구성과 테스트로 전환

테스트에서의 @Transactional 사용에 대해 질문이 있습니다.

해결된 질문

작성

·

6.7K

·

수정됨

30

 안녕하세요 토비 선생님!

강의 너무 재밌게 잘 듣고 있습니다. 이제 몇개 남지 않아서 많이 아쉽네요.

 

다름이 아니라 테스트 코드 작성시 `@Transactional` 어노테이션의 사용에 대해 질문이 있습니다.

 

저는 롤백테스트 작성을 위해 @Transactional 을 애용해왔는데요,,

얼마전 업무를 보다가 이상하게 테스트 코드는 잘만 통과를 하는데 같은 코드가 서버에 띄웠을때는 의도대로 동작을 안하더라고요. 한참을 씨름하다 알고보니 엔티티를 변경하고 JPA 변경감지로 변경하도록 의도한 코드인데 트랜잭션 경계 밖에서 변경을 하고 있었더라고요.

이후로는 찾아보니 테스트 코드에서 @Transactional을 사용하지 말라는 이야기가 많아 안쓰려고 노력을 해보는데 테스트 후 전부 롤백시키는게 만만치가 않더라고요. @AfterEach로 리포지터리를 다 불러와서 하나씩 돌려놓는것도 일이고..

개인적으로는 @Transactional 대신에 단순히 전체 테스트에 대해 DB 롤백을 해주는 어노테이션이 별개로 있었으면 더 좋지 않았을까 하는 아쉬움도 있더라고요.

  1. 스프링에서 굳이 서로 다른 용도의 기능을 하나의 어노테이션으로 공유하는 이유가 있을까요? 트랜잭션 경계라는 점에서는 공통점이 있다지만 각각의 기능으로 분리되었어도 되지 않았을까요?

  2. AfterEach 넣어서 리포지터리 하나하나 불러와 일일히 초기화 해 주는 대신 @Transactional 만큼 코드가 깔끔해지면서 테스트코드에 @Transactional을 쓰면 생기게 되는 문제를 해결하는 방법이 있을까요? 혹은 사실 알고보면 테스트에 @Transactional을 쓰는게 좋은건데 제가 잘못 오해하고 있었을까요?

 

요즘 테스트 코드를 작성할때마다 고민이 많았는데 제가 한동안 검색한 결과로는 쉽게 결론을 내기가 힘들었습니다.

더 나은 코드작성에 꼭 도움이 필요해서 실례를 무릅쓰고 질문글을 올려봅니다.

감사합니다.

 

답변 3

32

토비님의 프로필 이미지
토비
지식공유자

DB를 사용하는 테스트는 여러모로 어렵습니다. 내장형 DB가 아니라면 테스트 대상 애플리케이션 밖에서 동작하는데다, 데이터가 테스트 수행마다 변경이 되면 일관된 테스트 결과를 보장해줄 수 없기 때문이죠. 오래전에는 dbunit 같은 도구를 이용해서 테스트 수행 전후에 테스트용 db를 준비하는 것과 테스트 후에 이를 원래대로 돌려놓는 작업을 일일히 진행을 했어야 했습니다.

그런 시절에 등장한 @Transactional 롤백 테스트는 정말 혁신적이었고 db가 사용되는 테스트를 편리하게 작성할 수 있고, 각 테스트가 고립되어 수행되는 것을 보장해주기 때문에 많은 인기를 끌어왔습니다.

하지만 @Transactional 테스트는 테스트 수행 중에 단 한 개의 트랜잭션 경계만 사용이 되고, 그 경계를 테스트 메소드로 확장을 해도 문제가 없는 상황에서만 유효합니다. 말씀하신 케이스처럼 트랜잭션 설정을 제대로 하지 않은 코드도 테스트에서는 문제가 없는 것처럼 보입니다.

JPA의 detached 상태 오브젝트의 변경이 자동감지 되지 않는 코드가 @Transactional 테스트에서는 정상 동작하게 보이는 현상이나, @Transactional이 동일 클래스의 메소드 사이의 호출에서는 적용되지 않는(스프링의 기본 프록시AOP를 사용하는 경우라면) 문제, 또 JPA에서는 save한 오브젝트가 영속 컨텍스트에만 존재하고 db로 flush되지 않은 상태로 rollback되기 때문에 명시적으로 flush하지 않으면 실제 db 매핑에 문제가 있어도 검증하지 못한다는 문제 등을 들 수 있습니다.

이런 단점에도 불구하고 장점이 압도적으로 많기 때문에 저는 @Transactional 테스트를 적극적으로 권장합니다. 테스트용 DB까지 동작하는 단위 테스트(보기에 따라선 통합 테스트)를 작성할 수 있고, 심지어 병렬 테스트 수행도 가능해집니다. 테스트 코드 작성 속도가 빠르기 때문에 테스트를 더 적극적으로 활용할 가능성도 높아집니다.

대신 @Transactional 테스트에서 제대로 검증이 되지 않는 문제를 잘 인식하고 작성을 해야 합니다. 저도 가끔 테스트 하나에서 두 개 이상의 트랜잭션 경계가 참여하는 테스트를 작성하는 경우 강제로 커밋 테스트로 만들고 테스트를 작성합니다.

두 가지를 생각해볼 수 있습니다.

테스트를 웬만큼 잘 작성해도 애플리케이션 코드를 완벽하게 검증할 수는 없다는 사실을 인식해야 합니다. 그래서 개발자가 작성하는 테스트, 단위 테스트나 통합 테스트 외에 실제 환경과 유사하게 환경을 구성하고 진행하는 인수 테스트, e2e 테스트, 혹은 http api 테스트 같은 것을 추가로 진행해야 합니다.

코드에서 발생할 수 있는 전형적인 오류(경험하신 트랜잭션 경계 밖에서 detached 엔티티의 값 변경 같은 것들은, 코딩 가이드를 잘 작성해서 따르게 하고, 코드 리뷰에서 확인할 수 있도록 하고, 사용 가능하다면 각종 정적 분석 도구의 힘을 빌어서 어떤 작업을 수행하는 위치에 제한을 걸어주는 등을 통해서 검증이 되도록 해야 합니다.

단지 테스트에서 트랜잭션을 시작하지 않고 db 관련 작업이 수행되는 테스트를 한다고 해도 또, 여러가지 문제들은 남아있습니다.

대표적으로 테스트 경계가 바르게 설정되어있는가 검증의 문제인데요. 수행하는 작업 전체가 하나의 트랜잭션으로 잘 묶여있는지에 대한 검증은, 내부에서 여러번 db 작업을 수행하는 중간에 에러가 났을 경우 전체 작업이 다 롤백 되는지를 확인해야 합니다. 이건 @Transactional을 안 쓴다고 해결할 수 있는 문제가 아니죠. 게다가 적절한 시점에서 에러를 강제로 내게 하는 것은 또 매우 어려운 작업입니다. 경계 설정이 잘못 되어서 여러개의 트랜잭션으로 쪼개져 있어도 전체가 다 정상 수행되면 결과는 문제 없어보이겠죠. 이런 트랜잭션 경계 설정 오류의 문제는 실전에서 데이터가 깨지는 현상을 만나야지만 문제가 있다는 것을 알게 됩니다. 가장 힘든 작업이죠.

이런 걸 해결할 수 있는, 테스트를 수행하는 중에 경계 설정과 관련된 작업이 어디서 어떻게 시작되고 종료되는지 등을 테스트로 검증할 수 있으면 좋겠다 싶긴한데(어떻게든 만들려면 만들 수는 있겠습니다), 대부분의 경우 그 효과가 미미합니다. 그래서 다들 안 하는 것이겠죠.

테스트의 @Transactional 애노테이션이 애플리케이션에서 쓰는 것과 동일하다는게 처음엔 조금 혼란스럽기도 하지만, 테스트 메소드 단위로 트랜잭션 경계가 만들어진다고 생각하면 사실 자연스러운 결정이라고 봅니다. 오히려 롤백 테스트가 디폴트라는 게 코드에는 안 보인다는게 처음에 무슨 일이 일어나는지 잘 모르게 만들 수가 있겠지만요.

@Transactional 대신 tearDown 등에서 db를 클리어 하는 작업은 불가능한 건 아니지만 별로 추천하고 싶지 않습니다. 테스트 이전 상태가 모든 데이터가 다 비어있는 것으로 하기도 하지만, 어느 정도 초기 데이터 상태를 db에 넣고 하는 경우도 많은데, 데이터를 클리어하는 작업에서 이를 정확하게 원복한다는게 롤백 방식을 쓰지 않으면 매우 귀찮고 실수하기 쉽습니다. 초기 데이터가 달라지기라도 하면 모든 db 정리하는 코드를 또 다 고쳐야 하는데, 거기에 오류가 있으면 테스트가 다 깨지거나, 실패해야 할 다음 테스트가 성공하게 만들 수도 있겠죠. 그래서 아주 간단한 경우가 아니면 권장하지 않습니다. 아니면 테스트 하나 수행할 때마다 db 전체를 다 날리고 초기화 하는 작업을 하는 방법도 있긴한데, 애플리케이션이 커지면서 테스트가 매우 느려질테니 결국 테스트를 덜 만들거나 잘 하지 않게 될 겁니다. 단점이 더 많은 거죠.

속시원한 답을 못 드려서 죄송하긴 하지만 제가 아는 범위에선 이 정도 답변을 해드리는게 전부일 듯하네요.

shanePark님의 프로필 이미지
shanePark
질문자

많은 도움이 되었습니다. !!

제가 궁금했던 부분들을 정확하게 짚어주셨고, 제가 놓치고 있던 부분까지 알기 쉽게 설명해 주신 덕분에 더 좋은 고민을 해볼 수 있을 것 같습니다.

잘 참고하여 코드 개선과 더 많은 테스트 작성 및 고민을 해보겠습니다. 정말 감사합니다.

20

토비님의 프로필 이미지
토비
지식공유자

제가 틈날 때 김영한 님한테도 한번 꼬치꼬치 물어보겠습니다.

토비님의 프로필 이미지
토비
지식공유자

김영한 님한테 @Transactional 롤백 테스트 쓰는 대신에 커밋시키고 tearDown에서 복원하는 방법에 대해서 물어봤습니다.

답변은
"그러면 실용성이 너무 떨어지잖아요. 몇가지 조심하면 되는데 그것 때문에 오만가지 불편함을 감수하면서 초가삼간 다 태울 수 없으니..."

shanePark님의 프로필 이미지
shanePark
질문자

정말 감사합니다 !! 너무 기뻐서 제 수명이 늘어날 것 같습니다.

토비님도 만수무강하세요 ㅜㅜㅜ

5

토비님의 프로필 이미지
토비
지식공유자

제 유튜브 방송에서 이전에 드렸던 답변과 ChatGPT에게 같은 질문을 했을 때의 답변을 비교하며 설명하는 시간을 가졌습니다.

https://www.youtube.com/watch?v=-961J2c1YsM

shanePark님의 프로필 이미지
shanePark
질문자

어제 밤에 너무 재밌게 잘 봤어요! 제 질문이 바보같진 않았을까 노심초사하면서 봤네요..ㅎㅎ

감사합니다 토비선생님! 인프콘에 뵈러 가고 싶었는데 아쉽게도 탈락했네요 앞으로도 좋은 강의 기대하겠습니다 :)

shanePark님의 프로필 이미지
shanePark

작성한 질문수

질문하기