Replies: 1 comment
-
리비 발제 의도1년 정도 전에 네임드 개발자들이 뜨겁게 토론했던 주제인 롤백 테스트 구현 방법에 대한 토의를 해보고 싶다.
첫번째인 명시적 cleanUp은 테스트 메서드가 종료되기 직전에 명시적으로 데이터베이스를 cleanUp하는 구현을 말한다.
두번째인 @transactional 애너테이션을 이용한 롤백 테스트 구현은 @transactional 애너테이션에 테스트 코드에서 사용될 경우 트랜잭션이 종료되면 자동으로 롤백을 하는 것을 이용한 구현이다.
테스트 코드에 여러분들이 편리함과 안전성 속에서 어느 쪽에 가치를 더 두는지, 각각의 트레이드 오프 지점을 어떻게 저울질 했는지 궁금하여 발제한다. 리비 의견: @transactional을 이용하여 롤백 테스트를 구현한다.cleanUp 코드는 영속성 계층과 맞닿아 있을 뿐더러 필요에 의해서는 Native Query도 적극적으로 활용하여야 한다. (프로젝트 팀의 정책에 따라 다르겠지만 본인은 대부분의 경우에서 cleanUp을 위해 Low level의 코드를 작성해야 했던 적이 많다.) 본인이 @transactional을 사용하여 롤백 테스트를 구현하는 첫번째 이유는 잘 추상화된 스프링의 기능을 제공하지 않을 필요가 없기 때문이다. 둘째로 롤백을 수동으로하는 cleanUp 코드는 다음과 같은 한계를 지닌다.
수동 롤백은 부분적 데이터 롤백과 추상화하지 못한 코드를 유지보수 하게 만드는 단점이 있다. 에버 의견: 명시적 cleanUp을 통해 롤백 테스트를 구현한다.테스트의 롤백을 위해서 트랜잭션을 열지 않고 롤백만 할 수 있는 애너테이션 기능이 있다면 적극 사용할 것이다. 다만 테스트의 롤백을 위해서 새로운 트랜잭션을 열어야 하는 첫째로 테스트 코드의 트랜잭션 생명주기와 프로덕션 코드의 트랜잭션 생명주기가 상이함으로 여러 문제가 발생할 수 있다. 이러한 문제는 컴파일 타임에 발견하기 힘들어 굉장히 치명적인 오류로 이어질 수 있다. 이러한 문제를 발견하기 힘든 이유는 @transactional을 사용하고 싶지 않은 두번째 이유는 학습 비용이다. @transactional과 같이 위험할 수 있는 추상화는 그 상세에 대한 공부가 반드시 선행되어야 하는데, 팀 전체가 이를 숙지하고 사용하도록 가이드하는 것이 매우 비용이 높다고 생각한다. 제리 의견: 명시적 cleanUp을 통해 롤백 테스트를 구현한다.테스트의 롤백을 위해서 @transactional을 사용했다가 문제를 만났던 적이 실제로 여러번 있었다. 그 떄마다 애를 먹었었는데 그런 경험때문에 앞으로는 @transactional을 사용하지 않으려고 한다. 물론 학습한 상태에서 @transactional을 사용하면 편리한 부분이 있겠지만 모든 팀원들이 가이드를 잘 지켜주리라고 믿는 것은 naive한 판단 아닐까 싶다. 추가적으로 cleanUp코드를 작성하는 것이 그렇게 공수인지 의문이다. 전체 데이터를 롤백하는 코드를 생각해보면 한번 작성해놓고 수정할 일이 없다. 안전하고 공수도 크지 않다고 생각하기에 @transactional 애너테이션을 사용하지 않고 싶다. 콜리 의견: @transactional을 이용하여 롤백 테스트를 구현한다.원래 트랜잭셔널을 사용하지 말자는 주의였는데 이번에 자료 조사를 하면서 트랜잭션을 사용해도 괜찮다쪽으로 바뀌었다.
테스트 코드의 @transactional은 거짓 양성의 위험이 존재한다.제리: 실제 코드에는 트랜잭션이 누락되고 테스트 코드에 트랜잭션이 포함되는 경우를 생각해보자. 테스트 코드는 프로덕션에 없는 트랜잭션을 생성하고 그 속에서 검증을 진행한다. 실제 코드와 동작이 달라질 수 있는 부분이다. 실제로 나는 테스트 코드에서 트랜잭셔널을 작성하고 프로덕션에 누락하여 LazyInitializationException을 만난 적이 있다. 이러한 예외는 발견하기 힘들며 협업 자원을 갉아먹는 요소이다. 에버: 제리가 설명한 상황이 굉장히 까다롭고 다루기 어려운 상황이라는 것에 동의한다. 특히 런타임에서야 발견되는 예외라는 지점이 해당 문제를 까다롭게 만든다. 반드시 예방되어야 하는 지점인데 @transactional 사용에 동의하는 크루들은 이러한 위험성에 공감을 하지 못하는 것인지 궁금하다. 리비: 치명적인 예외 상황이고 반드시 예방되어야 한다는 것에 동의한다. 다만 예방하는 방법이 @transactional을 사용하지 않는 것이라면 그것은 과한 처리라고 생각. 지금 당장 떠올려봤을 때는 팀원들에게 서비스 메서드에 @transactional을 빼먹지 않도록 체크리스트나 그라운드룰을 운용하는 방식이 생각나는 것 같다. 팀원들이 필요한 애너테이션을 빠뜨리지 않도록 합의하는 비용은 싸다. 하지만 새로운 cleanUp코드를 유지보수하는 비용은 비싸다. 조금만 주의하면 저렴한 비용으로 높은 퀄리티를 누릴 수 있는데, 엔진오일 갈아야한다고 자동차를 새로 사는 것은 아닌지 고민해봐야한다고 생각한다. 콜리: 재민님이 리비와 비슷한 표현을 사용했었다. cleanUp코드의 비용을 빗대어 수동 cleanUp 코드는 유지보수 비용이 매우 높다.콜리: @transactional을 사용하지 않고 작성된 cleanUp 코드는 작성이 까다롭고 유지보수하기 어렵다. 특히 현업에서는 도메인이 매우 많은데 외래키 제약 조건을 의식하며 롤백하는 일이 가장 힘들다. 매 테스트 트랜잭션마다 전체 DB를 롤백할 수도 있지만 이는 트랜잭션 롤백보다 훨씬 많은 오버헤드가 있다. 데이터를 생성하고 삭제하는 일이� 매 테스트마다 실행된다면 테스트 서버의 부담이 높아지고 시드데이터를 유연하게 활용하는 @transactional 테스트보다 성능이 좋지 못하다. 아까 빈대잡기 위해 초가삼간 태운다는 표현이 등장했는데 나는 이 말이 현재 @transactional에 딱 맞는 상황이라고 생각한다. 반대쪽 의견은 어떤지? 제리: 우선 현재 팀에서는 모든 데이터를 삭제하는 롤백 테스트를 작성하고 있기에 유지보수 비용이 높지 않다. 시드 데이터를 활용할만한 규모의 프로젝트를 진행해보지 않아서 잘 모르겠는데 시드 데이터를 활용하기 시작한다면 확실히 번거로울 것 같기는 하다. 다만 그건 그러한 상황을 만났을 때의 얘기고 현재의 우리 상황에서 가장 적절한 것은 수동 cleanUp이라고 생각되지 않는가? 에버: 우리팀 역시 모든 데이터를 삭제하는 롤백 테스트를 작성하고 있기에 코드적인 측면에서 유지보수 비용이 높지 않다. 성능이 좋지 않지 않냐 라고 물어본다면 그것은 그럴 수도 있겠다라고 답할 것 같다. 다만 앞서 이야기했던 위험성과 후술할 위험성이 반드시 안전한 방법으로 예방되어야 함을 고려했을 때에는 이정도 오버헤드는 필요한 지출이라고 생각한다. 테스트 코드의 @transactional, 또 다른 거짓 양성 위험들에버: 다음의 상황들은 또 다른 @transactional의 거짓 양성들이다. 트랜잭션 전파 속성을 조절한 테스트 롤백 실패
제리: 또 다른 거짓양성도 있다. 유명한 update 문제이다. JPA의 변경 감지는 트랜잭션이 커밋되는 지점에서 쿼리문이 수행된다. 그런데 테스트 코드에서 호출된 update서비스 메서드가 종료되어도 테스트 메서드의 트랜잭션은 이어지고 있을 수 있다. 변경 감지가 반영되어야 하는 시점에 반영되지 않는 것이다. 이처럼 @transactional은 데이터를 깔끔하게 롤백시키는 것처럼 보이지만 위와 같은 위험지점들을 일일이 고려했을 때 의미가 있다. 실상은 그렇게 간편하지 않을 수도 있는 것이다. 콜리: 트랜잭셔널 전파 속성 관련해서 먼저 이야기 하겠다. 토비님이 향로님의 글에 반박한 사례가 있어서 이를 소개한다. 전파 속성이 REQUIRED, SUPPORT외의 것이 사용되어 테스트 @transactional에서 관리하지 못하는 트랜잭션이 생성된다면 테스트 메서드에서 커밋 후 테스트에서 생성한 데이터를 정리하면 된다. 사실 그리고 REQUIRES_NEW의 경우 배치나 분리 상황이 필요한 멀티 트랜잭션 전략이 필요한 경우에 아주 가끔 사용되기 때문에 흔히 경험하게 되는 사례는 아니다. EventListener 역시 일반적인 상황 처럼 보이지는 않는다. 이벤트를 던지는 코드에 대해 테스트 하면서 그 이벤트를 구독해서 동작하는 쪽의 작업까지 테스트 해야 하는가? Pub/Sub 모델을 사용했다면 이벤트를 바행하는 코드와 이벤트를 받아 쓰는 쪽의 결합도를 낮추어 테스트 하는 것이 오히려 올바르다고 보여진다. 리비: 변경 감지 update는 예외적으로 유의해야 하는 부분이다. em.flush를 명시적으로 호출하는 것이 해결책이 될 수 있겠다. 관리해야 하는 가이드가 너무 많아지는 것 아니냐고 물을 수도 있겠지만 이 정도는 개인적으로 핸들링하기 까다롭지 않다고 생각한다. 콜리: 애초에 위의 거짓 양성 위험들은 스프링 레퍼런스에서 3가지 주의점으로 제시하고 있다.
토론 회고리비: 처음에는 @transactional을 사용하자 주의였지만 토론을 진행하게 되면서 @transactional을 사용하지 말자는 쪽으로 의견이 바뀌었다. 오늘 소개된 @transactional의 거짓 양성 중 새로 알게 된 것들이 많았는데 그래서 생각이 바뀐 것 같다. 다만 규모가 큰 프로젝트의 cleanUp코드를 유지보수하다 보면 다시 의견이 바뀔지도 모르겠다. 여담으로 한 주 쉬고 진행된 토론이었는데 다들 정리를 잘해와서 놀랐다. 특히 콜리가 정리를 열심히 해오고 모르는 맥락을 한번씩 짚어주어 고마웠다. 다음 토론 부터는 나도 좀 빡세게 준비해야 할 듯 ㅋ; 콜리: cleanUp코드를 써오던 상황에서 @transactional은 잘 추상화된 고마운 존재처럼 느껴진다. 오늘 각각의 장단점을 저울질 해보면서 @transactional을 사용하자는 나의 의견에 조금 더 무게를 실을 수 있었던 것 같다. 물론 단점이 있겠지만 적어도 나의 저울질에서는 실보다 득이 큰 것이 @transactional을 사용하는 것인 듯 하다. 제리: 룰을 내가 숙지하는 것은 쉬우나 다른 팀원들 모두에게 숙지시키는 것은 어려울 수 있다. @transactional을 위해서 모두가 지켜야하는 가이드가 추가된다면 그것의 비용은 만만치 않으며 치명적인 휴먼에러로 이어질 수 있기에 안전하게 대처되어야 한다고 생각한다. @transactional의 장점이라고 이야기 하는 에버: 트랜잭셔널과 롤백에 대한 이해가 깊지 않았는데 오늘 토론으로 여러 맥락을 배울 수 있어서 좋았다. 많은 지식이 없었던 나의 입장에서는 모르고 위험한 코드보다는 확실히 제어할 수 있는 내가 작성한 코드가 더 안전해보인다. 우선은 내가 제어할 수 있는 코드로 작성을 하고 머리가 조금 굵어진다면 귀찮음을 피하기 위해 @transactional을 사용해볼 것 같다. |
Beta Was this translation helpful? Give feedback.
-
참고: 향로님 블로그
Beta Was this translation helpful? Give feedback.
All reactions