PostgreSQL DB를 배포환경에서 사용중이다.
필자는 통합테스트를 로컬에서 실행할 때, h2 를 postgreSQL모드로 사용한다.jdbc:h2:tcp://localhost/~/develope/test;MODE=PostgreSQL
하지만, 이 때 발생한 문제부터 알아보자.
이 테스트 코드의 목적은 회원정보를 수정해야 하는 대분류의 케이스의 일부이다.
사용자가 비밀번호를 잊어버렸을 때, 가입한 이메일정보로 인증 코드를 보내서,
해당 인증 코드와 함께, 비밀번호를 바꿀 수 있게하는 동작을 통합테스트로 실행하였다.
@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()
메서드이다.
mailService.sendEmailLink()
@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()
에서도 같은 테이블을 참조한다.
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문제였다.
궁금해서 local 에 postgreSQL을 직접 띄워서 다시 해봤다.
놀랍게도 됐다.
흠..?
그렇다면 추측컨데 lock time이 h2가 더 짧았기 때문이 아닐까 생각했다.
그래서 h2의 db locktime을 늘려보았다.
jdbc:h2:tcp://localhost/~/develope/test;MODE=PostgreSQL;LOCK_TIMEOUT=5000
이러니까 성공했다!!