[Test] h2 db (PostgreSQL모드)와 PostgreSQL db로 테스트할 때의 차이점 (부제 : h2 db 테스트시 발생한 lock 오류 해결)

김태훈·2023년 9월 24일
0
post-thumbnail

PostgreSQL DB를 배포환경에서 사용중이다.
필자는 통합테스트를 로컬에서 실행할 때, h2 를 postgreSQL모드로 사용한다.
jdbc:h2:tcp://localhost/~/develope/test;MODE=PostgreSQL
하지만, 이 때 발생한 문제부터 알아보자.

1. 문제가 되는 테스트 코드

이 테스트 코드의 목적은 회원정보를 수정해야 하는 대분류의 케이스의 일부이다.
사용자가 비밀번호를 잊어버렸을 때, 가입한 이메일정보로 인증 코드를 보내서,
해당 인증 코드와 함께, 비밀번호를 바꿀 수 있게하는 동작을 통합테스트로 실행하였다.

@SpringBootTest
class MemberIntegrationTest {

    // 인증 메일 관련 주입
    @Autowired
    MailService mailService;

    @MockBean
    JavaMailSender mailSender;

    @Autowired
    AuthCodeRepository authCodeRepository;

    NonSocialMember nonSocialMember = NonSocialMember.createNonSocialMember(new MemberSaveDto(
            "userst",
            LoginType.NON_SOCIAL,
            "ownest112@gmail.com",
            "a1234567!"
    ));

    @Nested
    @DisplayName("회원정보 수정")
    class updateUser{

        @BeforeEach
        void saveUser(){
            memberRepository.save(nonSocialMember);
        }

        @Test
        @DisplayName("비밀번호 수정")
        void updatePassword(){
            MimeMessage mimeMessage = mock(MimeMessage.class);
            // 때때로 메소드 내에서 mocking할 줄 알자.
            when(mailSender.createMimeMessage()).thenReturn(mimeMessage);
            doNothing().when(mailSender).send(any(MimeMessage.class));
            GenCodeResponse genCodeResponse = mailService.sendEmailLink(nonSocialMember.getUserEmail());

            PasswordUpdateDto updatePasswordDto = PasswordUpdateDto.builder()
                    .email(nonSocialMember.getUserEmail()).code(genCodeResponse.getCode()).userPw("a7654321!")
                    .build();
            IsSuccessResponseDto isSuccessResponseDto = memberRepository.findAndChangePassword(updatePasswordDto);
            assertThat(isSuccessResponseDto.getMessage()).isEqualTo("비밀번호가 수정되었습니다.");

        }
    }
}

문제가 없어보였다.
하지만 문제는

org.springframework.dao.QueryTimeoutException: PreparedStatementCallback; SQL [update member.auth as auth set password = ? from member.info as memberInfo where memberInfo.email = ?]; Timeout trying to lock table {0}; SQL statement:
update member.auth as auth set password = ? from member.info as memberInfo where memberInfo.email = ? [50200-214]`

다음과 같은 오류가 뜬다. 즉 lock table이 되었다는 것이다.

실제로 db에 lock이 걸릴 수도 있겠다고 생각했다.
두가지 메서드 때문인데
mailService.sendEmailLink()memberRepository.findAndChangePassword() 메서드이다.

@Transactional
    public GenCodeResponse sendEmailLink(String beVerifiedEmail){
        LocalDateTime dueDate = LocalDateTime.now().plusMinutes(5); //유효시간 5분

        String generatedCode = makeRandomString();
        authCodeRepository.invalidateExistedEmailCode(beVerifiedEmail);
        authCodeRepository.insertEmailCodeForVerification(dueDate, beVerifiedEmail, generatedCode);
        sendEmailForUpdatingPassword(beVerifiedEmail,generatedCode);

        return new GenCodeResponse(generatedCode);
    }

insertEmailCodeForVerification메서드에서

public void insertEmailCodeForVerification(LocalDateTime dueDate, String beVerifiedEmail,String code){

        //----------------- member.info 테이블 insert -----------------//
        String sql = "insert into member.auth_code(email,code,due_date) values(?,?,?)";
        KeyHolder userKeyHolder = new GeneratedKeyHolder();
        try {
            jdbcTemplate.update(con -> {
                PreparedStatement psmt = con.prepareStatement(sql, new String[]{"id"});
                psmt.setString(1, beVerifiedEmail);
                psmt.setString(2, code);
                psmt.setTimestamp(3, Timestamp.valueOf(dueDate));
                return psmt;
            }, userKeyHolder);
        }catch(DataAccessException e){
            log.error("error in insert= {}"+e.getMessage());
        }
    }

이렇게 auth 테이블을 물고있다.
근데, memberRepository.findAndChangePassword()에서도 같은 테이블을 참조한다.

2) memberRepository.findAndChangePassword()

public IsSuccessResponseDto findAndChangePassword(PasswordUpdateDto passwordUpdateDto) {
        String sql = "update member.auth as auth set password = ? from member.info as memberInfo where memberInfo.email = ?";
        int updatedRow = jdbcTemplate.update(sql, passwordEncoder.encode(passwordUpdateDto.getUserPw()), passwordUpdateDto.getEmail());
        if (updatedRow == 0) throw new UpdateFailException("비밀번호가 수정되지 않았습니다.");
        return new IsSuccessResponseDto(true, "비밀번호가 수정되었습니다.");
}

이런식으로 auth 테이블을 같이 묶어버렸다.
그래서 lock 문제가 생기게 된다.

추측이지만 내가 생각하기로는 메서드들을 순차적으로 실행하는데, 1)의 db transaction이 끝나지 않은 상태에서 또 2)의 transaction이 실행하면서 생기는 lock문제였다.

2. 해결점

궁금해서 local 에 postgreSQL을 직접 띄워서 다시 해봤다.
놀랍게도 됐다.
흠..?
그렇다면 추측컨데 lock time이 h2가 더 짧았기 때문이 아닐까 생각했다.
그래서 h2의 db locktime을 늘려보았다.
jdbc:h2:tcp://localhost/~/develope/test;MODE=PostgreSQL;LOCK_TIMEOUT=5000
이러니까 성공했다!!

profile
기록하고, 공유합시다

0개의 댓글