어째 쉬어가는 시간이 되어버렸습니다. 저번에 양이 많았던 것도 있으니 쉬어가기로.
본래 강의 영상의 제목은 "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)쿼리에서의 성능 이슈입니다. 뭣하면 쉬운 말로 뭉텅이쿼리라고 말하는 것도 괜찮지 않습니까?
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 11 - Dirty Check에 의한 batch 성능 이슈 - 패스트캠퍼스 챌린지 45일차 (0) | 2022.03.09 |
---|---|
JPA Ch 11 "N+1 문제" - 패스트캠퍼스 챌린지 43일차 (0) | 2022.03.07 |
JPA Ch 10 @Embeddable - 패스트캠퍼스 챌린지 41일차 (0) | 2022.03.05 |
JPA Ch 9 @Converter - 패스트캠퍼스 챌린지 40일차 (0) | 2022.03.04 |
JPA Ch 9 @Query (3), NativeQuery - 패스트캠퍼스 챌린지 39일차 (0) | 2022.03.03 |