팀으로 진행하고 있는 사이드 프로젝트에서는 회원 관리를 자체적으로 구현했습니다.
로그인도 별 문제없고 잘 되는 줄 알았는데, 어느 순간 로그인이 되지 않는 현상이 발생했습니다.
다행히도 문제는 금방 해결했지만 생각할게 많아진 주제였기에 기록을 남겨봅니다.
1. 회원 구현
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
@Convert(converter = PasswordEncryptConverter.class)
private String password;
private String introduce;
}
비밀번호를 BCryptPasswordEncoder로 암호화한, 별로 특별할 게 없어 보이는 회원 구현입니다.
2. 테스트
@Test
public void update_withoutDynamicUpdate() {
// 1. member 생성
String password = "1234";
Member member = new Member("choi8608@gmail.com", password, "변경전 - withoutDynamicUpdate");
memberRepository.save(member);
long memberId = member.getId();
// 2. 영속성 컨텍스트 초기화 후 member 조회
em.clear();
member = memberRepository.findById(memberId).get();
// 3. 암호화된 비밀번호 확인
System.out.println("### " + member.getPassword());
assertThat(passwordEncoder.matches(password, member.getPassword())).isTrue();
// 4. '자기소개' 만 수정 (dirtyCheck)
// 4-1. 모든 컬럼이 업데이트되면서 password 가 또 암호화됨
memberService.updateIntroduce(member.getId(), "변경후 - withoutDynamicUpdate");
// 5. 영속성 컨텍스트 초기화 후 member 조회
em.clear();
member = memberRepository.findById(memberId).get();
// 6. 비밀번호가 틀려짐
System.out.println("### " + member.getPassword());
assertThat(passwordEncoder.matches(password, member.getPassword())).isFalse();
}
의도하지 않게 비밀번호가 update 된 것도 문제지만, 비밀번호가 달라진 게 더 큰 문제였습니다!
JPA의 구현체인 hibernate는 애플리케이션이 처음 로드될 때 entity 들을 모두 스캔하여 update 쿼리를 캐시 해놓고 사용합니다. 그렇기에 dirtyCheck 된 entity가 통째로 update 된 것입니다.
문제는 여기에 password가 있었고, convert 되어버렸기 때문이었습니다.
즉 영속성 초기화 후 조회된 entity는 이미 암호화된 비밀번호를 가지고 있었고, 전체 update를 통해 암호화된 비밀번호가 또 한 번 암호화되어 저장된 것이죠.
3. 테스트 - @DynamicUpdate
해결 방법은 쉽습니다.
entity에 @DynamicUpdate를 달아주면 됩니다.
@DynamicUpdate
@Entity
public class MemberWIthDynamicUpdate {
...
}
아까의 테스트와 다르게 의도한 자기소개 필드만 update 된 것을 확인할 수 있습니다.
4. 언제 써야 될까?
얼핏 보기에는 영향받는 칼럼도 적고 의도하지 않은 변경도 일어나지 않아 더 좋을 것 같아 보이지만 기본 설정이 아닌 이유가 있습니다.
위에서 설명한 것처럼 JPA의 구현체인 hibernate는 애플리케이션이 처음 로드될 때 entity 들을 모두 스캔하여 업데이트할 쿼리를 캐시 해놓고 사용합니다. 즉 DynamicUpdate를 사용하면 캐시 된 쿼리가 아닌 동적 쿼리를 새로이 생성하고, 이 과정에서 오버헤드가 발생해 더 적은 필드만 수정됨에도 오히려 성능이 떨어집니다.
대신 entity의 필드 개수가 많고 그 중 소수의 필드만 업데이트 되는 일이 잦을 때는 유용합니다.
(물론 이런 경우는 entity 가 적절하게 분리된 것이 맞느냐를 먼저 의심해야 됩니다.)
마지막으로 격리 수준을 낙관적 락으로 @Version 어노테이션을 같이 사용할 경우 사용하는 것이 좋습니다.
(주로 @Version 필드만 업데이트하기 때문에)
'Series > 내가 해본' 카테고리의 다른 글
Elastic Beanstalk 구성 삽질기 - 글쑤시개 (0) | 2021.08.14 |
---|---|
Slack Bot 으로 채널에 글쓰기 (0) | 2021.08.04 |
표준 예외 처리에서 로깅까지 (2) (0) | 2021.07.03 |
표준 예외 처리에서 로깅까지 (1) (0) | 2021.07.02 |
Spring Security 로 회원 가입을 구현해보자 (3) (0) | 2021.06.19 |