Database ( DB )/JPA, Querydsl

JPA에서 insert, update, delete 할 때 자동으로 select 하지 않게 하는 방법

노루아부지 2022. 9. 11. 01:03

JPA에서 CRUD 중 Create(insert), Update, Delete query를 할 때, 원하지 않는 select query가 발생합니다.

 

다음 예제를 먼저 보겠습니다.

select query가 발생하는 예제

먼저 User entity class를 생성합니다.

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
@Getter
@Setter
public class User {
  @Id
  private String userId;
  private String userName;
  private int age;
}

 

entity를 사용할 repository class를 생성합니다.

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, String> {
}

 

마지막으로 테스트를 진행할 Controller를 생성합니다.

import com.example.demo.domain.User;
import com.example.demo.domain.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Optional;

@RequiredArgsConstructor
@RestController
public class TestController {
  private final UserRepository repository;

  @GetMapping("/test")
  public String test() {
    User user = new User();
    user.setUserId("hong1");
    user.setUserName("hong gil dong");
    user.setAge(20);

    repository.save(user);

    return "test";
  }
}

 

이제 /test를 호출하면 다음과 같은 쿼리가 Console에 표시됩니다.

Hibernate: select user0_.user_id as user_id1_0_0_, user0_.age as age2_0_0_, user0_.user_name as user_nam3_0_0_ from user user0_ where user0_.user_id=?
Hibernate: insert into user (age, user_name, user_id) values (?, ?, ?)

 

이와 같이 코드에서는 insert만 했는데 실제로는 insert 하기 전에 select가 먼저 된 것을 볼 수 있습니다.

 

 

원인

select를 먼저 하는 이유는 JpaRepository의 save method를 보면 알 수 있습니다.

다음은 SimpleJpaRepository의 코드입니다.

@Transactional
@Override
public <S extends T> S save(S entity) {
  Assert.notNull(entity, "Entity must not be null.");

  if (entityInformation.isNew(entity)) {
    em.persist(entity);
    return entity;
  } else {
    return em.merge(entity);
  }
}

여기서의 핵심은 isNew() method인데요.

isNew() method에서 select query를 수행하여 새로운 데이터인지 확인하는 것입니다.

 

그렇다면 select query를 하지 않게 하는 방법은 무엇일까요?

 

 

 

select query 하지 않게 하는 방법

방법은 간단합니다. Entity class에 Persistable 인터페이스를 구현하면 됩니다.

isNew()가 중요한데, 여기서는 테스트를 위해 true로 설정했습니다. 이에 따라 무조건 새로운 객체로 인식되기 때문에 위의 컨트롤러에서 userId를 변경하고 다시 실행하면 select 없이 insert가 되는 것을 알 수 있습니다.

import lombok.Getter;
import lombok.Setter;
import org.springframework.data.domain.Persistable;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
@Getter
@Setter
public class User implements Persistable<String> {
  @Id
  private String userId;
  private String userName;
  private int age;

  @Override
  public String getId() {
    return userId;
  }

  @Override
  public boolean isNew() {
    return true;
  }
}

 

하지만 이 방법에는 문제가 있는데요. 무조건 new이기 때문에 update를 해야 할 때도 insert를 수행하여 문제가 발생합니다.

따라서 제대로 된 isNew() method를 구현할 필요가 있습니다.

일반적으로는 createdDate를 이용한 방법을 쓴다고 합니다.

먼저 Application 클래스에 다음과 같이 @EnableJpaAuditing을 추가합니다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class DemoApplication {
  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}

 

그다음 User entity class를 다음과 같이 수정합니다.

package com.example.demo.domain;

import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.domain.Persistable;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Entity
@Getter
@Setter
// 1
@EntityListeners(AuditingEntityListener.class)
public class User implements Persistable<String> {
  @Id
  private String userId;
  private String userName;
  private int age;
  // 2
  @CreatedDate
  private LocalDateTime createdDate;

  @Override
  public String getId() {
    return userId;
  }

  // 3
  @Override
  public boolean isNew() {
    return createdDate == null;
  }
}

 

  1. @CreatedDate Annotation을 사용하기 위한 Annocation입니다.
  2. @CreatedDate Annotation을 사용하면 data가 insert 될 때 자동으로 현재 시간이 입력됩니다.
  3. createdDate가 null이라면 새로운 데이터로 취급합니다.

 

물론 위처럼 변경하더라도 이미 존재하는 PK를 가진 데이터를 insert 시도하면 에러가 발생합니다. 따라서 이 부분은 개발자의 역량에 달려있는데요.

그렇다면 기존 동작을 유지하면 되지 왜 귀찮게 변경하는지 의문이 생길 수 있습니다.

귀찮지만 이렇게 수정을 하는 이유는 실무에서 기존 동작인 select - insert를 유지하는 경우 엄청난 문제가 발생할 수 있기 때문입니다.

 

"없으면 추가하고 있으면 업데이트해라"는 우리가 개발을 하면서 많이 사용하는 전략인데요, 업데이트할 때도 업데이트해야 할 필드만 선택적으로 하는 것이 아닌 객체 전체가 변경되기 때문에 merge() 기능을 의도하고 사용해야 할 일은 거의 없습니다.

변경을 위해선 변경 감지(dirty-checking), 저장을 위해선 persist()만이 호출되도록 유도해야 실무에서 성능 이슈 등을 경험하지 않을 있습니다.

728x90
loading