개요
JPA로 API 서버 개발을 하던 중 insert query에서 매번 select query가 발생하여 엄청난 성능 저하가 발생했습니다.
도대체 왜 이런 것인가? select를 하지 않게 바꿀 수는 없는가? 찾아봤습니다.
현상 확인
1. User 생성
@Entity
@Table
@Getter
@Setter
public class User {
@Id
private String userId;
private Integer age;
}
2. UserRepository 생성
public interface UserRepository extends JpaRepository<User, String> {
}
3. 테스트 클래스 생성
@SpringBootTest
class UserTest {
@Autowired
private UserRepository repository;
@Test
void test() {
User user = new User();
user.setUserId("test");
user.setAge(20);
repository.save(user);
}
}
4. 결과 확인
save() 처리 방식
그렇다면 도대체 왜 이렇게 동작하는 것일까요?
CrudRepository 구현체인 SimpleJpaRepository를 보면 save는 아래와 같이 처리됩니다.
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}
- 새로운 객체 (isNew = true)이면 persist
- 아니면 merge
- merge는 persist → detach → persist 상태가 될 때를 의미
isNew
결론은 insert를 할 때 isNew가 false이기 때문에 select query를 하는 것이었습니다.
따라서 해결 방법은 isNew가 true가 될 수 있도록 하는 것인데요
현재 isNew가 true가 되는 기준을 찾아봤습니다.
결론은 entity의 ID가 존재하면 false 라는 것입니다.
아래는 isNew의 동작을 확인한 과정입니다.
1) SimpleJpaRepository
2) JpaMetamodelEntityInformation
3) AbstractEntityInformation
IDENTITY를 사용하는 경우?
그렇다면 아래와 같이 IDENTITY를 사용하는 ENTITY는 어떻게 될까요?
@Entity
@Table
@Getter
@Setter
public class History {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long seq;
private String value;
}
@SpringBootTest
class HistoryTest {
@Autowired
private HistoryRepository repository;
@Test
void test() {
History history = new History();
history.setValue("test");
repository.save(history);
}
}
다음과 같이 select query가 발생하지 않습니다.
자동증가값을 사용해서 entity의 ID가 null이기 때문입니다.
즉, ID에 자동증가값(Auto Increament)을 사용하지 않는 테이블의 경우 save를 호출하는 시점에 ID의 값이 존재하기 때문에 isNew가 false가 되어 select query를 하는 것입니다.
해결 방법
해결 방법은 간단합니다 isNew가 true가 되도록 변경하면 됩니다.
기존 isNew = true가 되는 조건은 ID가 존재하는 여부였는데, 이 조건을 다음과 같이 변경합니다.
- 영속성 콘텍스트에서 조회되지 않았을 때
- 영속 상태가 되기 전
위 조건을 맞추기 위해 다음과 같은 절차로 코드를 변경합니다.
- isNew라는 상태값 변수를 만들고 default 값을 true로 선언
- 이렇게 하면 Entity를 저장하기 위해 새로운 객체를 생성하면 기본 값인 true가 됩니다.
- @PrePersist를 통해 JPA Entity가 영속 상태가 되기 전에 false로 변경
- @PostLoad를 통해 영속성 콘텍스트에 조회된 직후, refresh를 호출한 직후에 false로 변
참조
@PrePersist, @PreUpdate 애너테이션 활용하기
<br /><br />
junhyunny.github.io
[JPA] 리스너 - 엔티티의 생명주기에 따른 이벤트 처리
리스너 모든 엔티티를 대상으로 언제 어떤 사용자가 삭제를 요청했는지 모두 로그로 남겨야 하는 요구사항이 있다고 가정하자. 이때 애플리케이션 삭제 로직을 하나 하나 찾아가며 로그를 남기
ttl-blog.tistory.com
Spring Data JPA에서 Insert 전에 Select 하는 이유
이전 글에서 bulk insert 처리를 할 수 있는 방법 중에 하나가 @Id 값 알고 있는 경우라고 했었는데요. 실제로 잘 되는지 확인하는 과정에 발생한 문제 내용을 공유합니다.
kapentaz.github.io
Spring Data Jpa Insert 할 때 Select가 나가네..
문제 상황 설계한 Entity의 id가 Auto Increament값이 아니다. 생성자가 호출되는 시점에 fk의 조합으로 생성된다. makeReservedSeatId 함수에서 만들어진다. @Entity @Table(name = "reserved_seat") public class ReservedSeat
taesan94.tistory.com
1. 아래와 같이 User 테이블과 History 테이블이 있다고 가정합니다.
@Entity
@Table
@Getter
@Setter
public class History {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long seq;
private String value;
}
2. 아래와 같이 테스트를 작성합니다.
@SpringBootTest
class HistoryTest {
@Autowired
private HistoryRepository repository;
@Test
void test() {
History history = new History();
history.setValue("test");
repository.save(history);
}
}
@SpringBootTest
class UserTest {
@Autowired
private UserRepository repository;
@Test
void test() {
User user = new User();
user.setUserId("test");
user.setAge(20);
repository.save(user);
}
}
'back end > java' 카테고리의 다른 글
Hibernate 성능 튜닝 Tips (0) | 2025.03.10 |
---|---|
[JUNIT] Spring Test MockMvc의 한글 깨짐 처리 (0) | 2025.03.10 |
[java] 문자열 자르기 함수 split 함수 사용 시 주의 사항. 문자열 자르는 방법 (0) | 2024.01.17 |
[JAVA] Google Authenticator(OTP)를 이용한 two factor 개발 방법 (0) | 2024.01.16 |
spring boot session timeout setting (0) | 2023.11.16 |