본문 바로가기

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

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

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

본래 강의 영상의 제목은 "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)쿼리에서의 성능 이슈입니다. 뭣하면 쉬운 말로 뭉텅이쿼리라고 말하는 것도 괜찮지 않습니까?