티스토리 뷰
JPA 를 이용하면 엔티티의 상태를 변경해주는 것만으로 update 쿼리를 실행시키게 된다. 이때 발생하는 쿼리는 모든 컬럼을 대상으로 update 를 실행한다.
@Entity
@Table(name = "person")
public class Person {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "person_id")
private Long id;
@Column(name = "person_name")
private String name;
@Column(name = "person_age")
private int age;
// 생성자 생략
public void setAge(int age) {
this.age = age;
}
}
위와 같은 간단한 엔티티를 정의한 후
Optional<Person> optionalPerson = personRepository.findById(1L);
Person person = optionalPerson.get();
person.setAge(50);
personRepository.save(person);
엔티티 객체를 이용해 상태변경을 하면
Hibernate: update person set person_age=?, person_name=? where person_id=?
이런 쿼리가 생성되어 날아간다. 변경한 값은 age 하나뿐이지만 name 까지 update 쿼리가 날아가는걸 확인할 수 있다.
# @DynamicUpdate
변경되는 age 만 수정할 수는 없을까? 방법은 간단하다. @DynamicUpdate 라는 애노테이션을 달아주면 된다.
@Entity
@Table(name = "person")
@DynamicUpdate
public class Person {
정상적으로 변경 컬럼만 update 한다.
Hibernate: update person set person_age=? where person_id=?
참고로 @DynamicUpdate 는 JPA 스펙이 아니고 하이버네이트 기능이다. JPA 스펙으로는 변경되는 컬럼만 추적하는 기능이 없는 것 같다.
어떻게 생각하면 @DynamicUpdate 를 사용한 결과가 당연하게 느껴질 수 있다. 변경이 된 컬럼만 수정해야지 전체 컬럼을 수정하는게 위험할 수 도, 비효율적으로 느껴지기도 하기 때문이다. 하지만 비효율적으로 느껴지는 이런 동작은 사실 효율적으로 동작하기 위해 필요하다.
JDBC 를 직접 이용해서 SQL 작성해본 사람은 PreparedStatement 클래스가 익숙할 것이다. PreparedStatement 는 Statement 클래스와 달리 SQL 구문을 캐시하고, ? 로 작성된 파라미터 부분만 변경해가면서 재사용하게 된다. JPA 도 결국 내부적으로는 JDBC 와 PreparedStatement 를 사용하게 되는데, 변경된 컬럼에 대해서만 update 쿼리를 날리게 되면 이런 SQL 캐시 히트율이 떨어지게 될 것이다.
PreparedStatement preparedStatement = connection.prepareStatement(
"UPDATE person SET name = ?, age = ? WHERE id = ?"
);
preparedStatement.setString(1, "new name");
preparedStatement.setInt(2, 35);
preparedStatement.setLong(3, 1);
preparedStatement.executeUpdate();
그리고 JPA 입장에서도 추가적인 연산이 필요해지는데, 모든 컬럼을 수정할때는 엔티티 객체의 변경에 대해서만 추적하면 됐지만 @DynamicUpdate 를 실행하기 위해서는 필드 수준의 추적이 필요해지기 때문이다.
// 엔티티 객체의 상태비교만 하면 됐었는데
if(!beforePerson.equals(afterPerson)) {
}
// 이제 객체 내 모든 필드까지 비교해야 한다!
if(!beforePerson.equals(afterPerson)) {
if(beforePerson.name.equals(afterPerson.name)) {
}
if(beforePerson.age == afterPerson.age) {
}
}
그럼 @DynamicUpdate 는 언제 사용해야할까?
# 컬럼이 많을 때
어떻게 보면 가장 기본적이고 단순한 이유다. 컬럼이 많거나 컬럼의 크기가 클때 사용하면 좋다. 다만 "많다" 라는건 상당히 추상적이므로 잘 판단해야할 것 같다. 지극히 개인적으로 15~20개 정도로 기준을 잡고있다.
# 테이블에 인덱스가 많을 때
컬럼이 많을 때와 비슷한 이유이다. 인덱스가 걸려있는 컬럼은 변경이 발생하면 인덱스를 재정렬하게 되는데, 인덱스가 많으면 많을수록 update 쿼리에 영향을 주게된다. 값이 변경되지 않았다면 굳이 update 를 하지 않는게 update 쿼리 성능에 도움을 주게된다.
# 데이터베이스가 컬럼 락을 지원할 때
널리 사용하는 DBMS(oracle, mysql)는 컬럼 락을 지원하지 않기에 그다지 와닿지 않을 수 있다. 참고한 자료에서는 yugabyte 라는 데이터베이스를 얘기하고 있는데, 컬럼 락을 지원하는 DBMS 에서 사용하기 적절하다.
# 데이터베이스가 컬럼 버저닝을 지원할 때
사실 이것도 대중적인 DBMS 에서는 지원하지 않을 뿐더러 DBMS 내부 구현에 대한 내용이라 크게 와닿지 않는다.
# 다양한 컬럼에 대해 동시성 이슈가 발생할 때
위 3가지는 참고자료에 있는 내용들을 가져온 것이고, 이번 사례는 내가 직접 경험한 내용이다. 특정한 엔티티 A 가 있다고 가정할때 애플리케이션 요구사항에 따라 step 별로 A 의 서로 다른 필드들을 변경하는 경우이다. 모든 API 를 동기적으로 동작하게 했다면 문제가 없었을텐데 step 이라고 하는 단위가 매우 작은 단위였기 때문에 클라이언트쪽에서 비동기로 서버쪽 API 를 호출했고, 그때마다 각기 다른 컬럼을 변경하게 됐는데 T1(transaction 1)이 끝나기 전에 T2가 시작됐던 것이다. T1 과 T2 에서 각각 서로 다른 한가지 컬럼만 update 를 하는 상황에서 모든 컬럼을 update 하다보니 트랜잭션간 경합이 발생하면 먼저 실행된 트랜잭션의 변경은 무시되는 이슈가 발생했었다. 이때 @DynamicUpdate 를 활용해 문제를 해결했다.
참고자료
- https://vladmihalcea.com/how-to-update-only-a-subset-of-entity-attributes-using-jpa-and-hibernate/
- https://dzone.com/articles/when-to-use-the-dynamicupdate-with-spring-data-jpa
'Java > jpa' 카테고리의 다른 글
spring boot 2.x hibernate 5.x 에서 매핑키만 있고 데이터는 없는 경우 전체 엔티티 조회 실패 (0) | 2024.04.30 |
---|---|
Entity Merge 시에는 동일한 객체를 반환하지 않는 이유 (0) | 2023.05.07 |
non-primary key 를 이용해 연관관계 설정시 Serializable 인터페이스 구현 (1) | 2021.06.16 |
JPA 양방향 연관관계 매핑 (0) | 2020.04.25 |
JPA 조회조건 객체 매핑 (0) | 2017.12.15 |
- Total
- Today
- Yesterday
- JavaScript Core
- code
- toby
- go-core
- backend개발환경
- frontend개발환경
- EffectiveJava
- java
- 정규표현식
- clean code
- spring cloud
- programming
- db
- java8
- generics
- Jackson
- TEST
- javascript
- Git
- DesignPattern
- mariadb
- JPA
- Design Pattern
- Spring
- OOP
- Kotlin
- frontcode
- http
- servlet
- MySQL
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |