JPA에서 엔티티로 업데이트를 처리하게 되면 전체 컬럼에 대한 업데이트를 진행하게 된다.
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
@Transactional
public void init() {
for(int i=0; i<10; i++) {
Comment comment = new Comment();
comment.setComment("최고에요");
commentRepository.save(comment);
}
}
@Transactional
public void updateSomething() {
List<Comment> comments = commentRepository.findAll();
for(Comment comment : comments) {
comment.setComment("별로에요");
commentRepository.save(comment);
}
}
}
@SpringBootTest
class CommentServiceTest {
@Autowired
private CommentService commentService;
@Autowired
private CommentRepository commentRepository;
@Test
void commentTest() {
commentService.init();
commentService.updateSomething();
}
}
테스트를 실행하면 insert나 update 모두 지정하지 않은 column까지 사용되는것을 볼 수 있다.
Hibernate:
insert
into
comment
(comment, commented_at, created_at, review_id, updated_at)
values
(?, ?, ?, ?, ?)
Hibernate:
update
comment
set
comment=?,
commented_at=?,
review_id=?,
updated_at=?
where
id=?
이런경우 @DynamicInsert,@DynamicUpdate을 해당 엔티티에 달아주게 되면 필요한 컬럼에 대해서만 업데이트를 하게된다.
Hibernate:
insert
into
comment
(comment, created_at, updated_at)
values
(?, ?, ?)
Hibernate:
update
comment
set
comment=?,
updated_at=?
where
id=?
이렇게 하면 쿼리가 간단해질 뿐만 아니라 영향받는 db컬럼도 적어지게 되어 좀더 안전한 쿼리가 된다.
created_at이나 updated_at은 엔티티리스너에서 지정되어있기때문에 자동으로 삽입되거나 수정된다.
@CreatedDate @Column(columnDefinition = "datetime(6) default now(6) comment '생성시간'", nullable = false, updatable = false) private LocalDateTime createdAt; @LastModifiedDate @Column(columnDefinition = "datetime(6) default now(6) comment '수정시간'", nullable = false) private LocalDateTime updatedAt;
그런데 만약 updateSomething()에서 save를 주석처리 한다면?
@Transactional
public void updateSomething() {
List<Comment> comments = commentRepository.findAll();
for(Comment comment : comments) {
comment.setComment("별로에요");
//commentRepository.save(comment);
}
}
Hibernate:
update
comment
set
comment=?,
updated_at=?
where
id=?
그래도 update가 일어난다. 이 부분이 dirty check이다. 앞서 학습했던 영속성 컨텍스트내에는 dirty check라는 내용이 존재한다. 즉, 영속성 관리중에 일어난 변경은 별도의 save메소드 호출이 없더라도 db데이터에 영속화시켜주는것이다.
logging:
level:
root: trace
를 application.yml에 추가하고 다시 테스트를 실행해보면
DefaultFlushEntityEventListener : Found dirty properties [[com.fastcampus.jpa.bookmanager.domain.Comment#1]] : [comment] 라고 해서 dirty check을 하고 있음을 로그에 보여준다.
로그레벨 trace는 실 운영서버에서는 사용하지 않는다.
영속성 컨텍스트내에 있는 엔티티들은 모두 dirty check의 대상이 되어서 추가적인 수정내용을 db에 반영해주게 된다.
영속성컨텍스트가 관리하는 범위는 영속성 세션, 외부에 세션을 설정하는 것은 @Transactional을 다는것
만약 @Transactional을 제거하고 테스트를 돌려보면 update쿼리가 존재하지않는다.
그렇다면 이와 같은 코드를 실행하면 어떻게 될까?
@Transactional
public void insertSomething() {
Comment comment = new Comment();
comment.setComment("이건뭐죠?");
commentRepository.save(comment);
}
@Transactional이 붙어있더라도 새로 생성된 객체라 영속화되어 있지 않기 때문에 dirty check가 일어나지 않는다. save메소드를 호출해서 영속화 시키겠다고 명시적으로 표시를 해줘야만 insert를 처리하게 된다.
dirty check에 미숙하다면 예외적인 동작뿐아니라 성능적인 이슈가 발생하게된다.
트랜잭션 내에서 select를 한 데이터에 대해서는 일일히 dirty check과정이 들어가게 된다. 수십건, 수백건 많은 데이터를 핸들링하는 배치로직의 경우에는 dirty checking을 하는 시간이 많이 늘어나게된다.
이런경우 dirty check를 하지않고 데이터를 읽으려면 어떻게 해야할까?
이 옵션을 사용하면 해당 트랜잭션에서는 읽기 전용으로 동작하며, 변경 사항을 데이터베이스에 반영하지 않는다. 이를 통해 성능 향상을 이끌어낼 수 있습니다.
@Trasansactional(readOnly = true)로 속성을 지정해준다, flush를 자동으로 해주지 않기 때문에 dirty check자체가 skip된다.
실제로 update쿼리가 발생하지 않는다.
그래서 이렇게 readOnly를 함으로써 조회만 해야하는 로직에서 dirty check를 스킵함으로써 대용량데이터를 처리할때 얻을수있는 성능적인 장점이 발생한다.
영속성 컨텍스트 : 엔티티를 관리하고 영속화하는데 사용되는 메커니즘이며, Dirty Check는 이러한 영속성 컨텍스트의 핵심 기능 중 하나이다.
Dirty Check : 영속성 컨텍스트 내에서 엔티티의 상태 변경을 감지하고 해당 변경을 데이터베이스에 반영하는 메커니즘
@DynamicInsert, @DynamicUpdate: 이러한 어노테이션을 사용하여 엔티티의 변경 사항에 따라 적절한 컬럼만을 업데이트할 수 있도록 설정하는 것이 좋다. 이렇게 하면 불필요한 컬럼까지 업데이트하는 비용을 줄일 수 있다.
배치 처리: 대용량 데이터를 처리할 때는 특히 Dirty Checking이나 영속성 컨텍스트의 관리에 주의를 기울여야 한다. 성능 이슈가 발생할 수 있기 때문에 배치 작업을 최적화하는 것이 중요하다.
@Transactional(readOnly = true): 이 옵션을 사용하면 해당 트랜잭션에서는 읽기 전용으로 동작하며, 변경 사항을 데이터베이스에 반영하지 않는다. 이를 통해 성능 향상을 이끌어낼 수 있다.
dirty check가 때로는 좋게 작동할수있지만 대용량데이터를 처리할때에는 성능적인 이슈가 생길수도있다. jpa에서는 영속성컨텍스트를 통해 자동으로 해주는 기능이 많은 만큼 불필요한 성능의 손해나 개발자가 생각하지못한 사이드 이펙트가 발생할 가능성이 있다. jpa로직들의 동작들이 어떻게 처리되는지 확인하고 사용하는것이 좋다.