인프런/실전! 스프링 데이터 JPA

5) 스프링 데이터 JPA 구현체 분석,merge,새로운 엔티티 구별방법

backend dev 2024. 6. 14.

 

스프링 데이터 JPA 구현체 분석

스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체

org.springframework.data.jpa.repository.support.SimpleJpaRepository
@Repository
@Transactional(
    readOnly = true
)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

@Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환

 

@Transactional 트랜잭션 적용

JPA의 모든 변경은 트랜잭션 안에서 동작 [ 트랜잭션 필수 ]

 

스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션 처리

@Transactional
public <S extends T> S save(S entity) {

 

서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작

 

서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용

 

그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했음

(사실은 트랜잭션 이 리포지토리 계층에 걸려있는 것임)

 

@Transactional(readOnly = true) 데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서

readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있음

[ flush를해서 , 변경감지를 진행한다던지, 쓰기지연 sql저장소에있는 쿼리를 실행한다던지하는데, 조회만 한거니까 그 과정이 필요가없다. 그러므로 flush를 생략해서 성능향상을 얻는다.]

[ 자세한 내용은 JPA 책 15.4.2 읽기 전용 쿼리의 성능 최적화 참고]

[ 조회가 많은경우 클래스레벨에서는 읽기전용으로 설정, 변경메소드에서는 메소드레벨에서 일반 트랜잭션으로 설정]

 

스프링 데이터 jpa 공통 인터페이스의 구현체인 SimpleJpaRepository에 많은 메소드중

findById를 보면

public Optional<T> findById(ID id) {
    Assert.notNull(id, "The given id must not be null");
    Class<T> domainType = this.getDomainClass();
    if (this.metadata == null) {
        return Optional.ofNullable(this.entityManager.find(domainType, id));
    } else {
        LockModeType type = this.metadata.getLockModeType();
        Map<String, Object> hints = this.getHints();
        return Optional.ofNullable(type == null ? this.entityManager.find(domainType, id, hints) : this.entityManager.find(domainType, id, type, hints));
    }
}

엔티티매니저.find로

순수 JPA를 이용하는 모습 확인가능.

 

@Transactional
public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null");
    if (this.entityInformation.isNew(entity)) {
        this.entityManager.persist(entity);
        return entity;
    } else {
        return this.entityManager.merge(entity);
    }
}

isNew가 아닌경우라는 말은

이미 DB에는 저장되어있지만 준영속상태인가 라는말

 

DB에 저장이 되어있지만 준영속상태인 엔티티면 merge를 진행

 

jpa는 준영속상태 [ 영속성컨텍스트에서 관리 되고있지않는상태] 라면 merge

영속상태라면 변경감지를 사용하는게 권장된다.

 

merge 동작과정

1. DB에는 저장되어있고, 준영속상태인 엔티티이므로 DB에서 조회해온다. -> 영속상태로 만든다.

2. 파라미터로 넘어온 엔티티의 값을 이용하여 영속상태로 만든 엔티티에 덮어씌운다. -> 변경감지로 인해 update된다.

 

merge는 DB에 select 쿼리를 한번 해야한다는것이 단점이다.

 

가급적 merge 사용 X , 변경감지를 사용해야한다.

merege는 어떤이유로 엔티티가 영속상태를 벗어났을때, 다시 영속상태로 만들기위할때만 사용해야한다. 

데이터 업데이트에 사용되는것을 권장하지않는다.

 

매우 중요!!!

save() 메서드

-> 새로운 엔티티면 저장( persist )

-> 새로운 엔티티가 아니면 병합( merge )

 

 

새로운 엔티티 구별방법은 아래


 

새로운 엔티티를 구별하는 방법

 

새로운 엔티티를 판단하는 기본 전략

 

- 식별자가 객체일 때 null로 판단  [ pk값이 객체면 null로 판단 ex) wrapper 타입 , Long과같은  ]

 

- 식별자가 자바 기본 타입일 때 0 으로 판단 [ pk값이 primitive타입이라면 0으로 판단 ]

 

- Persistable 인터페이스를 구현해서 판단 로직 변경 가능

 

 

만약 식별자를 자동생성 [ @GeneratedValue ] 을 사용하지 않고 직접 식별자를 넣어준다면?

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {
    @Id
    private String id;
    
    public Item(String id) {
        this.id = id;
    }
}
@Test
public void save() {
    Item item = new Item("A");
    itemRepository.save(item);
}

이렇게 식별자를 직접 넣어서 엔티티를 생성하고 save()를 호출한다면

식별자가 null이 아니므로 새로운 엔티티로 판단하지않는다.

@Transactional
public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null");
    if (this.entityInformation.isNew(entity)) {
        this.entityManager.persist(entity);
        return entity;
    } else {
        return this.entityManager.merge(entity);
    }
}

그래서 persist가 동작하지않고, merge가 동작한다.

 

merge는 DB에 해당 엔티티의 데이터가 존재할거라고 생각하고 동작한다.

그러므로 해당 엔티티의 식별자를 이용하여 DB에서 조회를 진행한다.

 

그런데 DB에서 조회를 못해올시 , 새로운 엔티티라고 판단하여 DB에 저장하는 쿼리가 발생한다.

 

save는 저장말고 업데이트까지 가능하긴한데 , 데이터를 강제로 다 바꾸기 때문에 좋지않다고 한다.

그래서 데이터 업데이트는 변경감지를 사용해야한다.

 

GTP -> merge를 이용하여 데이터 업데이트를 하는것이 좋지않은 이유

 

  • 전체 필드 업데이트: merge는 엔티티의 모든 필드를 업데이트합니다. 이는 변경된 필드가 일부일 경우에도 전체 엔티티를 업데이트하기 때문에 불필요한 데이터베이스 작업이 발생할 수 있습니다.
  • 두 번의 데이터베이스 액세스: merge는 엔티티를 영속성 컨텍스트에서 조회하고, 이후 변경된 필드를 업데이트합니다. 이는 불필요한 데이터베이스 액세스를 유발할 수 있습니다.

 

 

 

 

임의로 식별자를 생성해야하는 경우 Persistable를 이용한다.

@Entity
@EntityListeners(AuditingEntityListener.class) // spring data jpa 이벤트 어노테이션을 사용하기 위함
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String>, Serializable {
    @Id
    @GeneratedValue
    private String id;

    @CreatedDate
    private LocalDateTime createdDate;


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

    @Override
    public boolean isNew() { // 여기다가 이 엔티티가 새로운 엔티티인지 아닌지에 대한 로직을 구현하면 된다.
        return createdDate==null; // createdDate , 즉 생성일 필드를 이용하여 새로운 엔티티인지 판단하는것을 많이 사용한다고 한다.
    }
}

 

참고

JPA 식별자 생성 전략이 @GenerateValue면 save() 호출 시점에 식별자가 없으므로

새로운 엔티티로 인식해서 정상 동작한다.

 

그런데 JPA 식별자 생성 전략이 @Id 만 사용해서 직접 할당이면 이미 식별자 값이 있는 상태로 save() 를 호출한다

따라서 이 경우 merge() 가 호출된다.

merge()는 우선 DB를 호출해서 값 을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율적이다.

 

따라서 Persistable를 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 효과적이다.

 

참고로 등록시간( @CreatedDate )을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다. (@CreatedDate에 값이 없으면 새로운 엔티티로 판단)

 

댓글