-
[Spring] 테스트 코드에서의 @Transactional 사용Spring 2023. 5. 30. 22:48
@Transactional
사이드 프로젝트를 진행하며 테스트 코드를 짜던 중
테스트 코드에서 @Transactional를 사용하는 과정에서 문제가 생겨 글을 작성하게 되었다.
❗️ 문제
ScheduleServiceTest.java (일부)
@SpringBootTest @Transactional(readOnly = true) class ScheduleServiceTest { @Autowired DatabaseCleanUp databaseCleanUp; @Autowired ScheduleService scheduleService; @BeforeEach @Transactional void beforeEach() { SignUpDto signUpDto1 = SignUpDto.builder() .username("member1") .password("12345678") .nickname("닉네임1") .address("서울시 광진구") .phone("010-1234-5678") .build(); savedMemberDto1 = memberService.signUp(signUpDto1); } @AfterEach @Transactional void afterEach() { databaseCleanUp.truncateAllEntity(); } @Test public void findByScheduleId() { } }
DatabaseCleanUp.java (일부)
@Transactional public void truncateAllEntity() { entityManager.flush(); entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate(); for (String tableName : tableNames) { entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); } entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate(); }
ScheduleServiceTest에서 findByScheduleId라는 메서드를 실행하게 되면
크게 세 가지의 각각 다른 트랜잭션이 생성된다.
1. @BeforeEach로 인해 생성된 트랜잭션 ➡️ @Transactional(readOnly = false)
2. findByScheduleId() 메서드로 인해 생성된 트랜잭션 ➡️ @Transactional(readOnly = true)
3. @AfterEach로 인해 생성된 트랜잭션 (databaseCleanUp.truncateAllEntity()) ➡️ @Transactional(readOnly = false)
2번의 findByScheduleId()는 데이터 변경이 없는 메소드이므로 @Transactional의 readOnly 옵션을 true로 두었고,
3번의 truncateAllEntity()는 모든 테이블의 데이터를 삭제해주는 메소드이므로 @Transactional의 readOnly 옵션을 false로 두었다.
그리고 findByScheduleId()를 실행했는데
.
.
.
이게 어떤 상황일까...?
beforeEach()를 거쳐 생긴 영속성 컨텍스트의 변경사항이 바로 DB에 반영되지 않고
afterEach()에서 호출한 databaseCleanUp.truncateAllEntity(); 안의 em.flush(); 가 호출되어 DB에 반영되는 것이다.
이 과정에서 transactional이 read-only가 true 설정되어 있어 저 오류가 발생하는 것 같은데
findByScheduleId() 메서드에 로그를 찍어보면 메서드가 실행도 안 되는 듯 하다.
✅ 해결
- 애초에 DatabaseCleanUp의 truncateAllEntity() 메소드를 통해 데이터들을 삭제할 수 있다.
➡️ Rollback을 위한 @Transactional 사용 안 해도 됨 (테스트 코드에서는 최대한 사용 안 하는 게 좋다) - findByScheduleId(), findByTeamId(), removeSchedule()에서 ScheduleMember 조회 시도하면서 LazyInitializationException 발생 ➡️ @Transactional 붙여주면 해결!
- findByScheduleId(): isEqualTo
- findByTeamId(): contains
- removeSchedule(): doesNotContain
: @Transactional 을 사용하면 Session이 Transaction이 종료되기까지 열려있다. 따라서 Lazy Loading을 사용할 수 있게 된다.
💻 Code
@SpringBootTest @Slf4j class ScheduleServiceTest { @Autowired DatabaseCleanUp databaseCleanUp; @Autowired MemberRepository memberRepository; @Autowired MemberService memberService; @Autowired TeamService teamService; @Autowired ScheduleService scheduleService; private MemberDto savedMemberDto1; private MemberDto savedMemberDto2; private TeamDto savedTeamDto1; private TeamDto savedTeamDto2; private ScheduleDto savedScheduleDto1; private ScheduleDto savedScheduleDto2; private ScheduleDto savedScheduleDto3; @BeforeEach void beforeEach() { // Member 회원가입 SignUpDto signUpDto1 = SignUpDto.builder() .username("member1") .password("12345678") .nickname("닉네임1") .address("서울시 광진구") .phone("010-1234-5678") .build(); SignUpDto signUpDto2 = SignUpDto.builder() .username("member2") .password("12345678") .nickname("닉네임2") .address("서울시 광진구") .phone("010-1234-5678") .build(); savedMemberDto1 = memberService.signUp(signUpDto1); savedMemberDto2 = memberService.signUp(signUpDto2); // teamA TeamDto teamA = TeamDto.builder() .name("teamA").build(); savedTeamDto1 = teamService.createTeamWithMember(teamA, savedMemberDto1); savedTeamDto2 = teamService.addMember(savedTeamDto1.getId(), savedMemberDto2.getUsername()); // teamB TeamDto teamB = TeamDto.builder() .name("teamB").build(); TeamDto savedTeamB = teamService.createTeamWithMember(teamB, savedMemberDto1); ScheduleDto scheduleDto1 = ScheduleDto.builder() .title("산책") .scheduleTime(LocalDateTime.now()) .build(); ScheduleDto scheduleDto2 = ScheduleDto.builder() .title("목욕") .scheduleTime(LocalDateTime.now()) .build(); ScheduleDto scheduleDto3 = ScheduleDto.builder() .title("애견카페") .scheduleTime(LocalDateTime.now()) .build(); savedScheduleDto1 = scheduleService.addSchedule(scheduleDto1, savedTeamDto2.getId(), new ArrayList<>()); savedScheduleDto2 = scheduleService.addSchedule(scheduleDto2, savedTeamDto2.getId(), new ArrayList<>()); savedScheduleDto3 = scheduleService.addSchedule(scheduleDto3, savedTeamB.getId(), new ArrayList<>()); } @AfterEach void afterEach() { databaseCleanUp.truncateAllEntity(); } @Test @Transactional public void findByScheduleId() { ScheduleDto findScheduleDto = scheduleService.findByScheduleId(savedScheduleDto1.getId()).get(); assertThat(findScheduleDto).usingRecursiveComparison().isEqualTo(savedScheduleDto1); } @Test // @Transactional(readOnly = true) public void test() { log.info("@Transactional(readOnly = true)"); } }
마지막 test method는 주석 해제했을 때 왜 안 되는지 아직도 미스테리...
이 부분은 조금 더 뜯어보고 따로 포스팅 해야겠다
📋 참고 자료
728x90'Spring' 카테고리의 다른 글
[Spring] Spring Security + JWT 로그인 구현하기 - 1 (4) 2023.06.14 [Spring] JWT(Json Web Token)란? | 구조, 암호화 방법, 장단점 (0) 2023.06.14 [Spring] 네이버 도서 검색 API 활용하기 (0) 2023.04.22 [Spring] Spring boot가 자동 등록하는 HandlerMapping과 HandlerAdapter (0) 2023.03.14 [Spring] Cannot resolve taglib with uri http://java.sun.com/jsp/jstl/core (0) 2023.03.09 - 애초에 DatabaseCleanUp의 truncateAllEntity() 메소드를 통해 데이터들을 삭제할 수 있다.