Series/내가 해본

DynamicUpdate 활용기

Hyunec 2021. 7. 27. 18:10
 

GitHub - Hyune-c/blog-code: https://hyune-c.tistory.com/ 의 예제 code

https://hyune-c.tistory.com/ 의 예제 code. Contribute to Hyune-c/blog-code development by creating an account on GitHub.

github.com

팀으로 진행하고 있는 사이드 프로젝트에서는 회원 관리를 자체적으로 구현했습니다.
로그인도 별 문제없고 잘 되는 줄 알았는데, 어느 순간 로그인이 되지 않는 현상이 발생했습니다.
다행히도 문제는 금방 해결했지만 생각할게 많아진 주제였기에 기록을 남겨봅니다.

 

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();
}

1번에서 회원이 생성되고, 암호화된 비밀번호가 들어갔음을 확인할 수 있습니다.
4번에서는 자기소개만 수정했는데 비밀번호도 같이 update 됨을 볼 수 있습니다. 그리고 insert 의 비밀번호와 update 의 비밀번호 해시값이 서로 다릅니다.

 

의도하지 않게 비밀번호가 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 필드만 업데이트하기 때문에)