블로그 이미지

suddiyo


꾸준히 기록하고 성장하는 백엔드 개발자 💻
Today
-
Yesterday
-
Total
-

ABOUT ME

-

  • [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()를 실행했는데

    .

    .

    .

    Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

     

    이게 어떤 상황일까...?

     

    beforeEach()를 거쳐 생긴 영속성 컨텍스트의 변경사항이 바로 DB에 반영되지 않고

    afterEach()에서 호출한 databaseCleanUp.truncateAllEntity(); 안의 em.flush();  가 호출되어 DB에 반영되는 것이다.

    이 과정에서 transactional이 read-only가 true 설정되어 있어 저 오류가 발생하는 것 같은데

    findByScheduleId() 메서드에 로그를 찍어보면 메서드가 실행도 안 되는 듯 하다.

     

     

    ✅ 해결

    1. 애초에 DatabaseCleanUp의 truncateAllEntity() 메소드를 통해 데이터들을 삭제할 수 있다.
      ➡️ Rollback을 위한 @Transactional 사용 안 해도 됨 (테스트 코드에서는 최대한 사용 안 하는 게 좋다)
    2. findByScheduleId(), findByTeamId(), removeSchedule()에서 ScheduleMember 조회 시도하면서 LazyInitializationException 발생 ➡️ @Transactional 붙여주면 해결!
      • findByScheduleId(): isEqualTo
      • findByTeamId(): contains
      • removeSchedule(): doesNotContain

    chatGPT야 고마워

    : @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는 주석 해제했을 때 왜 안 되는지 아직도 미스테리...

    이 부분은 조금 더 뜯어보고 따로 포스팅 해야겠다


    📋 참고 자료

     

    스프링 테스트 케이스에서의 @Transactional 유의점

    스프링 테스트 케이스에서 @Transactional 사용 시 유의점

    velog.io

     

    JPA의 LazyInitializationException

    JPA를 사용하는 경우 잊을 만하면 만나는 LazyInitializationException 을 해결 하는 방법 정리

    velog.io

     

    728x90

    댓글

Designed by Tistory.