FastCampus - 한번에 끝내는 Java|Spring 웹 개발/04 JPA

JPA Ch 11 - 캐시와 DB의 불일치 - 패스트캠퍼스 챌린지 44일차

sftblw-chan 2022. 3. 8. 21:34

어째 쉬어가는 시간이 되어버렸습니다. 저번에 양이 많았던 것도 있으니 쉬어가기로.

본래 강의 영상의 제목은 "Ch. 11 JPA 트러블슈팅" 챕터의 "02. 영속성 컨텍스트로 인해 발생하는 이슈" 인데, 강의 내용은 Persistence Context 나 실제 객체와 DB 간의 간극 등에 의해 차이가 생겨서 문제가 발생하는 경우에 대해 다룹니다. 뭐 비슷하죠? 결국은 캐시의 문제입니다.

3가지 정도의 사례가 나왔습니다.

  • 이전 강의에서 봤던 문제: @Embedded 된 객체의 null 여부가 실제 DB와는 무관하게 객체가 있거나 없는 문제
    -> 이전에 다뤘으니 생략합니다.
  • 기성 데이터베이스가 있거나 해서 AutoDDL을 사용하지 않은 경우, 데이터베이스의 정의와 객체에 매핑된 타임스탬프의 정밀도가 달라서 생기는 문제
    • 강의를 보면서 생각했는데, 이거 이전에 나왔던 의도치 않게 업데이트가 실행되는 경우에도 해당될 수 있을 거 같아요. 그 때는 Converter에 의한 거였지만...
  • 칼럼 정의를 수동으로 한 경우(@Column(columnDefinition=)), 기본값을 설정했을 때 저장 전에는 null이고 캐시도 null 이지만 다시 불러오면 해당 필드가 칼럼 정의에 의해 이미 기본값으로 설정되어있는 경우
    • 이 경우는 재현이 힘들었습니다 (물론 제가 아니고 강의에서... 삽질...). 내용은 적었는데 영상이 길어졌던 이유.
    • @DynamicUpdate 가 아닌 @DynamicInsert 도 나옵니다.

캐시와 DB가 달라지는 경우: 타임스탬프의 정밀도가 달라서

MySQL 을 기준으로 한 예시라서 저는 직접 실행해볼 생각이 없기는 한데, 칼럼의 정의가 초단위까지만 저장되는 경우를 다뤘습니다. 원래 주로 하던 게 datetime(6) 인걸로 봐서 숫자는 정밀도인듯?

/* ! 컴파일 여부를 확인해보지 않았음 ! */
생략
@Entity
data class Comment {
    생략
    @Column(columnDefinition = "datetime") // 초 단위로만 저장
    var commentedAt: LocalDateTime?
}

캐시에 남아있는 commentedAt과 DB에서 로드한 commentedAt 이 다른 걸 보여주려면 테스트 시나리오를 잘 짜야합니다. 먼저 Persistence Context의 캐시에 있다는 걸 보여줘야 하니 @Transactional 안에 있어야 하고, repo.saveAndFlush() 로 확실히 저장해줘야 하며, em.clear() 를 주석처리했다가 넣었다 하면서 실제 DB에서 조회한 것과 Persistence Context의 캐시에서 가져온 것의 차이를 보여줘야 합니다.

/* ! 컴파일 여부를 확인해보지 않았음 ! */
@Transactional
@Test
fun dateTimeDiffersTest() {
    val comment = Comment()
    comment.commentedAt = LocalDateTime.now()
    
    commentRepo.saveAndFlush(comment)
    val id = comment.id
    
    // 아래 구문의 여부에 따라 결과가 달라집니다.
    entityManager.clear();
    
    val retrievedComment = commentRepo.findById(id)
    println(retrievedComment.commentedAt) // 초단위의 유무가 달라집니다.
}

캐시와 DB가 달라지는 경우: 칼럼 정의의 디폴트값 때문에

칼럼의 정의에 디폴트값이 있어서 저장할 때는 null 이었지만, 저장한 걸 로드하면 null 이 아닌데 Persistence Context의 캐시에 남아있어서 다시 조회해도 여전히 null 로 찍히는 경우를 살펴봅니다.

이게 특히 강의에서 재현이 어려웠는데, 이전에 잠깐 나왔던 @DynamicUpdate 와 비슷한 @DynamicInsert 가 없으면 변경 여부에 상관없이 모든 필드를 저장하기 때문입니다. 이 어노테이션은 엔티티에 넣습니다.

Comment 를 재활용합니다. 친환경적이네요.

/* ! 컴파일 여부를 확인해보지 않았음 ! */
생략
@Entity
@DynamicInsert
data class Comment {
    생략
    @Column(columnDefinition = "datetime(6) default now(6)") // 초 단위로만 저장
    // pgsql 이라면: "timestamp default localtimestamp"
    var commentedAt: LocalDateTime?
}

이후의 시나리오는... 안 봐도 아시겠죠? 강의를 다시 확인 안 해보고 직접 만들어볼까요.

/* ! 컴파일 여부를 확인해보지 않았음 ! */
@Transactional
@Test
fun dateTimeNullCacheTest() {
    val comment = Comment()
    comment.commentedAt = null
    
    commentRepo.saveAndFlush(comment)
    val id = comment.id
    
    // 아래 구문의 여부에 따라 결과가 달라집니다.
    entityManager.clear()
    
    val retrievedComment = commentRepo.findById(id).get()
    println(retrievedComment.commentedAt) // null 여부가 달라집니다.
}

어... 이건 진짜로 예상대로 동작하는지 확인을 해봐야겠는데... 뭐때문인진 모르겠는데 인텔리제이가 안 켜져서 재부팅이 필요할 거 같네요. 위의 코드에는 예상되는 동작을 주석으로 달아놨는데 (확인을 못 해봤으니) 그렇게 안 돌아갈수도 있겠습니다.

update: 오 예상대로 동작하네요

이런 케이스는 알아두는 편이 좋음

마무리말로 덕담 아닌 덕담? 이라고 해야하나? 그런 말을 해주셨는데, JPA 에 의해 암시적으로 만들어진 캐시가 아니라 직접 캐시를 쓰는 경우에조차도 발생하는 사례를 두 가지정도 말씀해주시면서 JPA에서도 경각심을 가지도록 유도해주셨습니다.

  • 회원탈퇴는 했지만 캐시에 회원정보가 남아있어서 로그인을 한 사례
  • 환불은 이미 했지만 캐시에 환불 정보가 아직 반영되지 않았어서 환불이 한 번 더 된 사례
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC

#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online

다음 강의는 배치(Batch)쿼리에서의 성능 이슈입니다. 뭣하면 쉬운 말로 뭉텅이쿼리라고 말하는 것도 괜찮지 않습니까?