스프링 데이터 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에 값이 없으면 새로운 엔티티로 판단)
'인프런 > 실전! 스프링 데이터 JPA' 카테고리의 다른 글
끝) [미완] 나머지 기능들 - Specifications(명세),Query By Example,Projections,네이티브 쿼리 (0) | 2024.06.14 |
---|---|
4) WEB 확장기능 - 도메인 클래스 컨버터, 페이징과 정렬 (0) | 2024.06.13 |
3) 확장 기능 - 사용자 정의 리포지토리 구현, Auditing (0) | 2024.06.12 |
2) 쿼리 메소드 기능 (1) | 2024.06.12 |
1) 예제 도메인 모델, 공통 인터페이스 기능 소개 (0) | 2024.06.11 |
댓글