어쩌다가 마지막 강의가 쉬어가는 시간이 된 건지.
JPA 강의의 마지막 내용은... 뒤에 더 없어요?... 자동으로 저장을 해주는 Dirty Check 기능에 의해서 성능적 이슈가 생기는 상황입니다.
한 줄로 정리하자면 다수의 내용을 읽을 때에는 @Transaction(readOnly=true) 를 설정하자는 거구요.
JPA의 "자동 저장"
JPA에는 dirty check 기능이 들어있어서 Persistence Context (영속 컨텍스트) 에서 관리되는 개체가 수정된 경우 세션(@Transactional) 종료 시 수정 여부를 파악해서(dirty check) 수정된 부분을 반영해줍니다. repo.save() 호출 없이도 반영되는 거죠. 예전 강의에서도 배웠던가요...?
이걸 확인하기 위해서 @Transactional 이 있는 예시를 만들어봅시다.
일단 업데이트되는 걸 좀 편하게 보기 위해서 @DynamicUpdate 부터 Comment 에 추가해줍시다. 이전에도 몇 번 나왔었지만, 이게 설정되면 JPA에서 업데이트된 필드만 반영하게 됩니다.
@Data
@NoArgsConstructor
@Entity
@DynamicInsert
@DynamicUpdate
public class Comment {
생략
테스트용으로 Transactional 이 달려있는 서비스 클래스를 추가합니다. Transactional 이 끝났을때 업데이트되었음을 확인하기 위한 용도입니다.
@Service
open class CommentService(
private val commentRepo: CommentRepo,
private val entityManager: EntityManager
) {
@Transactional
open fun initComments() {
(0 until 10).forEach {
val comment = Comment().apply { commentText = "우왕 댓글 단당" }
commentRepo.save(comment)
}
}
@Transactional
open fun updateComments(doSave: Boolean) {
commentRepo.findAll().forEach { comment ->
comment.commentText = "우왕 댓글 바뀌었당"
if (doSave) { commentRepo.save(comment) }
}
}
}
테스트 코드에서는 업데이트 후 저장 (commentRepo.save()) 을 호출하는 경우와 호출하지 않는 경우를 확인해봅니다.
@SpringBootTest
class CommentServiceTest {
@Autowired
private lateinit var commentService: CommentService
@Autowired
private lateinit var commentRepo: CommentRepo
@Test
fun testSave() {
commentService.initComments()
println("---------------------------------")
commentService.updateComments(doSave = true)
}
@Test
fun testNoSave() {
commentService.initComments()
println("---------------------------------")
commentService.updateComments(doSave = false)
}
}
예상사히는 대로 updateComments(doSave = false) 여도, 필드를 수정하는 부분이 트랜젝션 안에 있으므로 업데이트 쿼리가 호출됩니다.
더티 체크 눈으로 확인하기
필드의 dirty 여부를 판단하는 정황을 로그에서 확인해보려면 로그에서 dirty 를 검색하면 됩니다. 단, logging.level.root = trace 로 바꾸고 나서요. 당연하게도 로그가 미친듯이 뿜어져나오므로 프로덕션에서의 사용은 금물입니다.
logging:
level:
root: trace
로그를 보면 "dirty check" 라고 명확하게 찍혀있고, 어떤 클래스인지도 나옵니다. 너무 많아서 일부만 편집해서 가져와봤습니다.
///////////////////////////////////////////////////////
/////////////// initComments() 입니다. ////////////////
///////////////// 10번 찍힘 /////////////////////////
///////////////////////////////////////////////////////
DEBUG 3760 --- [ Test worker] org.hibernate.SQL :
insert
into
comment
(comment_text)
values
(?)
Hibernate:
insert
into
comment
(comment_text)
values
(?)
///////////////////////////////////////////////////////
/////////////// findAll() 입니다.///// ////////////////
///////////////////////////////////////////////////////
DEBUG 3760 --- [ Test worker] org.hibernate.SQL :
select
comment0_.id as id1_5_,
comment0_.comment_text as comment_2_5_,
comment0_.commented_at as commente3_5_,
comment0_.review_id as review_i4_5_
from
comment comment0_
Hibernate:
select
comment0_.id as id1_5_,
comment0_.comment_text as comment_2_5_,
comment0_.commented_at as commente3_5_,
comment0_.review_id as review_i4_5_
from
comment comment0_
////////////////////////////////////////////////
/////////////// 더티 체크 ///// ////////////////
///////////////////////////////////////////////////
DEBUG 3760 --- [ Test worker] o.h.e.i.AbstractFlushingEventListener : Dirty checking collections
TRACE 3760 --- [ Test worker] o.h.e.i.AbstractFlushingEventListener : Flushing entities and processing referenced collections
아래 3줄이 10번 반복
TRACE 3760 --- [ Test worker] o.h.p.entity.AbstractEntityPersister : kr.co.fastcampus.jpasandbox.review.Comment.commentText is dirty
TRACE 3760 --- [ Test worker] o.h.e.i.DefaultFlushEntityEventListener : Found dirty properties [[kr.co.fastcampus.jpasandbox.review.Comment#1]] : [commentText]
TRACE 3760 --- [ Test worker] o.h.e.i.DefaultFlushEntityEventListener : Updating entity: [kr.co.fastcampus.jpasandbox.review.Comment#1]
TRACE 3760 --- [ Test worker] o.h.e.i.AbstractFlushingEventListener : Processing unreferenced collections
TRACE 3760 --- [ Test worker] o.h.e.i.AbstractFlushingEventListener : Scheduling collection removes/(re)creates/updates
업데이트 쿼리도 10번 반복
DEBUG 3760 --- [ Test worker] org.hibernate.SQL :
update
comment
set
comment_text=?
where
id=?
Hibernate:
update
comment
set
comment_text=?
where
id=?
강의에서는 DefaultFlushEntityEventListener (hibernate 쪽 코드) 라는 클래스를 살펴봤습니다. 로그를 찍는 부분이 거기 있었죠.
readOnly 로 더티 체크 피하기
나는 그냥 조회만 해도 되는데, 변경되었는지 여부를 추적하는 기능이 켜져있으면 확인하려는 데이터가 한두개 정도면 괜찮겠지만 10만개같이 엄청 많은 수량이 되면 성능 저하는 피할 수 없을 것입니다. 그럴땐 @Transactional(readOnly=true) 를 넣어봅시다.
(강의 끝나고 따로 하는) 실습 과정에서 테스트 메소드에도 넣어봤는데, initComments() 에서 save를 호출하면서 예외가 발생했습니다. readOnly 트랜젝션에서 save 는 호출 불가능한걸로. readOnly 의 문서를 보니 힌트니까 무시될수도 있다고 하네요. 주의해야겠습니다.
대신 서비스에 @Transactional(readOnly = true) 인 메소드를 하나 추가해줍니다.
@Service
open class CommentService(
private val commentRepo: CommentRepo
) {
// vvvvvvvvvvvvvvv 추가
@Transactional(readOnly = true)
open fun updateCommentsReadonly(doSave: Boolean) {
commentRepo.findAll().forEach { comment ->
comment.commentText = "우왕 댓글 바뀌었당"
if (doSave) { commentRepo.save(comment) }
}
}
}
테스트도... 적당히 추가해줍시다.
@SpringBootTest
open class CommentServiceTest {
생략
@Test
open fun testReadonly() {
commentService.initComments()
println("---------------------------------")
commentService.updateCommentsReadonly(doSave = false)
}
}
readOnly 이므로 필드를 바꾸는 건 되지만 더티 체크 기능은 비활성화되어서 암만 수정을 해도 저장하지 않으면 반영되지 않습니다. 쿼리 로그에서도 findAll() 것만 찍히네요.
---------------------------------
Hibernate:
select
comment0_.id as id1_5_,
comment0_.comment_text as comment_2_5_,
comment0_.commented_at as commente3_5_,
comment0_.review_id as review_i4_5_
from
comment comment0_
readOnly 와 JpaRepository -> SimpleJpaRepository
강의에서의 언급으로는 readOnly=true 로 설정 시 FlushModeType 을 MANUAL 로 바꾼다고 합니다. 그것 뿐만이 아니라 JPA 백엔드에 따라 오류가 발생하기도 하는 거 같은데... 아무튼 FlushModeType 은 jakarta쪽께 있고 hibernate 께 있는데 hibernate 쪽 코드에 MANUAL 이 있습니다. readOnly 플래그의 문서에는 이런 언급이 없는 걸로 보아 소스를 읽으신듯.
또한, 리포지터리는 기본적으로 readOnly=true 이나 (SimpleJpaRepository 의 클래스에 어노테이션이 달려있음)
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
생략
save 같은 메소드에는 별도의 @Transaction 이 붙어있어서 readOnly 가 아닌 상태로 동작하게 됩니다.
@Transactional
@Override
public <S extends T> S save(S entity) {
요 점도 참고.
다량의 데이터를 다룰 때에는 readOnly 를 붙이는 방안을 고려해봅시다. 제 생각에는 readOnly 를 붙이는 게 습관이 되어있는게 맞을 거 같아요. 마치 const 를 습관화하는 것처럼요.
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
이런저런 딴짓을 한다고 시간이 예상보다 많이 걸렸네요; 오늘은 양이 적으니 쉬어갈줄 알았는데...
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 11 - 캐시와 DB의 불일치 - 패스트캠퍼스 챌린지 44일차 (0) | 2022.03.08 |
---|---|
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 |