글 작성자: 개발섭

JPA 관련해서 게시물 찾다가 위와 같은 블로그를 발견했어서 무슨 문제인지를 파악해보고 싶어서 해결기와 관련한 글을 작성해보았습니다

https://ttl-blog.tistory.com/523?category=906284

 

[JPA] 살려주세요

문제 상황을 간단하게 나타내 보도록 하겠습니다.. TestEntity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class TestEntity { @Id @GeneratedValue(strategy = Generat..

ttl-blog.tistory.com

TL; DR!

블로그의 질문 사항의 핵심은 롤백 상황을 테스트 환경에서 테스트 가능하도록 만드는것이었다. 블로그에서는 @Transactional를 포함한 통합테스트 환경에서 “롤백” 되는 상황을 만들어버려서 문제라고 생각했다. 즉, 테스트 환경 자체에서 아예 롤백되지 않는 상태를 만들어놓고 롤백 메소드를 실행시키면 롤백되는 상황 체크할 수 있고, 애초에 롤백되는 상황을 가정한거니 테스트 독립성, 정합성 문제도 어느정도는 해결된다고 생각한다. 롤백 테스트를 해보고 싶다면, 테스트 메소드에 포함되어있는 @Transactional을 제외 시켜버리고 진행하면 된다.

코드

아래의 TestEntity 객체는 String type의 name과 Long타입 Id만 존재하는 매우 단순한 객체이다.

testService의 test는 값을 변경하고 uncheckedException을 발생 시키는 함수이다.

블로그의 존재하는 테스트 코드에서 약간은 변형되어있는 테스트 코드이다.

@Test
    public void rollBackTest() throws Exception {

        TestEntity test = new TestEntity();
        test.setName("바보");
        TestEntity save = testRepository.save(test);
        //'바보' 라는 TestEntity가 저장

        assertThrows(IllegalStateException.class, ()-> testService.test());
        //'바보' 를 '멍청이'로 바꾸려는데 RunTimeException이 발생 여기서 이미 롤백해야하는 상황이지만...

        TestEntity test1 = testRepository.findById(save.getId()).get();
        assertEquals("바보", test1.getName());
        System.out.println(test1.getName());  //'멍청이' 가 출력...ㅠ
    }

상황 설명

블로그의 소개된 테스트 상황은 메소드 자체에 UnCheckedException을 터트린뒤에 이 변경 내역이 롤백 환경이 만들어지고 그 변경 내역이 원래 상태로 돌아가는 것을 테스트 코드화 시켜서 확인해보고 싶었던것 같다. ( 본인이 아니라서 정확하게 의도를 파악한건지는 모르겠다만... 😅)

친한 동료분과 대화를 통해서 어느정도 실마리를 찾았는데... SpringBootTest에서 @Transactional를 메소드든 클래스 단위로 사용하게 된다면, 자동으로 롤백이된다. (DB 테스트간 동일 상황을 맞춰야하니까)

결국 이것 역시 전파옵션과 비슷한 사례라고 보는데, 블로그 분의 해법중 하나인 @Transactional(propagation=Propagation.REQUIRES_NEW) 를 실제 메소드에서 옵션을 걸어서 사용하는 방식을 택해서 성공시켰던 것을 보았다.

테스트를 위해 만들어놓은 메소드에서는 이 롤백되는 경우에 대한, 검증은 어떤식으로 해야할까?

해결방법

블로그분의 해법에서 크게 벗어나지는 않았으나, 결국 가장 큰 문제인 테스트를 하기위해서 과연 트랜젝션 옵션을 주는게 맞는가? 라는 대전제를 피해가기는 어려웠다.

결국 테스트 단위의 트랜잭션 때문에 메소드에서 강제로 예외가 발생해도 메소드 트랜잭션 자체가 테스트 트랜잭션으로 편입되니까.. 문제 해결이 안된다고 파악했다.

내 생각에는 아예 반대로 생각해봤다. 어차피 롤백되는 상황이라면 테스트에서 트랜잭션을 통한 롤백을 넣지 않아도 되는게 아닌지를 실험을 해보았다.

@Test
public void rollBackTest() throws Exception {

        TestEntity test = new TestEntity();
        test.setName("바보");
        TestEntity save = testRepository.save(test);

        assertThrows(IllegalStateException.class, ()-> testService.test());

        TestEntity test1 = testRepository.findById(save.getId()).get();
        assertEquals("바보", test1.getName());
        System.out.println(test1.getName()); 
    }

이 경우는 트랜잭션이 바로 test에서만 생기니.. 그 값이 롤백이 되버리고, 값이 변경이 되더라도 메소드 트랜잭션에 의해서 이미 값자체는 롤백이 되었으니 이전 초기값과 일치하는 상황이 생겨서 롤백되는 것 역시 테스트코드로 확인할 수 있었다.

 

👆 뭐 사실 이런 문제도 생각해볼 수는 있다.

 

트랜잭션 자체를 없에는것이 정합성에 문제가 있을수도 있지 않느냐? 라는 문제 역시 존재 할 수 있다고 생각한다. 왜냐하면, 결국 테스트는 동일한 상황에 동일한 값을 내뱉을거라고 구현하는건데, 테스트 DB 상의 Transacational을 없에버리는 바람에 초기 DB가 섞이는 이상한 상황이 생길 수 도 있을 거라고 생각한다.

이게 트랜잭션 롤백상황을 가정하고 테스트를 해보는 게 핵심이다.

즉, 트랜잭션 롤백이 될것을 가정하고 테스트를 하기 때문에, 애초에 원래 값으로 돌아가는걸 상정하고 테스트를 하는게 아닌가? 그러면 굳이 테스트 단위의 롤백이 필요하지는 않을것 같다는 게 나의 생각이다.

결국 테스트 단위의 롤백 역시, 테스트 이전과 형태를 동일하게 만들려고 목적을 가지고 있으니까 테스트 단위의 동일한 행동을 하는 롤백 행동을 테스트 하는 것 자체가 이미 테스트 롤백 상황과 동일하다고 생각했다.

추가

통합테스트에 @Transactional을 포함시켜서 DB 롤백테스트를 시도하셨었는데, 나는 더 나아가서 @DataJpaTest인 상황에서 롤백 테스트 하는 경우 방법이 있을지 확인했다.

@DataJpaTest 는 안에 @Transactional이 포함되어있고, 심지어 모든 테스트 메소드에 @Trasactional이 걸리는 상황이 생긴다. 이런 경우에는 전파 옵션 자체를 꺼버리는게 나는 좋다고 판단했다

@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void rollBackTest() throws Exception {

    TestEntity test = new TestEntity();
    test.setName("바보");
    TestEntity save = testRepository.save(test);


	assertThrows(IllegalStateException.class, ()-> testService.test());
    

    TestEntity test1 = testRepository.findById(save.getId()).get();
	assertEquals("바보", test1.getName());
    
}

NEVER, NOT_SUPPORT, SUPPORT 3개 모두 가능하다. 현재 트랜잭션을 없에고 실제 메소드에 관한 트랜잭션을 넣는 옵션이라면 모두 가능했었다. 그래도 이왕이면 NOT_SUPPORT가 좋지 않을까라는 동료분의 의견을 받아서 처리했었다.

여기서 더 나아가 @Transactional(readOnly=true)도 롤백되는 상황이 생기지 않을까 했었는데... readOnly 옵션의 경우 역시 테스트 메소드 자체의 롤백을 통해서 만들어둔 테스트 코드가 붉어졌고.. 아무리 읽기전용이여도 롤백상황에는 그 트랜잭션이 작동되는 것을 확인했었다.

사담

우연하게 찾은 게시물이 꽤 많은 도움을 줬는데, 이것 저것 찾아보게 하는 원동력이 되는 듯하다. 이런식으로 한번 정리를 깔끔하게 한 뒤에, 그분에게 답장도 남겨보고.. 나만의 정리를 추가해서 나만의 해석을 만들어 보는 것도 좋아보인다.

가끔 블로그나 StackoverFlow와 같은 질의응답게시물을 통해서 문제점 파악 → 왜 문제가 일어났는지 원리 체크 → 해결방안 제시와 같은 방식으로 공부를 해보는 것도 어떤 면에서는 Deep Dive하게 공부하는 방법이 되는 것 같아서 좋았다.

마지막으로 역시 이 가장 큰 원동력이 되었던 블로그 주인님에게 가장큰 감사의 말씀올린다.