스프링 데이터 JPA가 실제 어떻게 동작하는지 코드 레벨로 들어가서 살펴보자.
스프링 데이터 JPA 구현체 분석
- 스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체가 있다.
- org.springframework.data.jpa.repository.support.SimpleJpaRepository
리스트 12.31 SimpleJpaRepository
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> ... {
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
...
}
@Repository 적용
- 스프링 빈의 컴포넌트 스캔 대상
- JPA 예외를 스프링이 추상화한 예외로 변환
- 어떤 이점이 있냐면 하부 기술을 JDBC에서 JPA로 바꿔도 서비스나 컨트롤러나 리포지토리 계층을 가져다 쓰는 계층에서는 예외를 처리하는 메커니즘이 동일하다. 즉 하부 구현 기술을 바꿔도 기존 비즈니스 로직에 영향을 최대한 주지 않도록 설계된거다.
@Transactional 트랜잭션 적용
위 트랜잭션 적용이 무슨 소리냐면 Spring Data JPA의 모든 기능이 일단 트랜잭션을 걸고 시작한다는거다.
JPA의 모든 변경은 트랜잭션 안에서 동작해야 한다.
그런데 스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션 안에서 처리해야 한다.
서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작(Spring Data JPA를 쓰는경우)
서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했다
(사실은 트랜잭션이 리포지토리 계층에 걸려있다.)
@Transactional(readOnly = true)
readOnly = true 옵션으로 해도 그냥 트랜잭션과 똑같이 동작한다.
다만 readOnly = true 라고 되어 있으면 flush()를 안한다.
트랜잭션이 끝나면 기본적으로 영속성 컨텍스트의 쓰기 지연 저장소에 쌓여있던 쿼리를 보내는 flush()가 발생하고 커밋을 한다. 그래서 데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있음
save() 메서드(매우 중요!!!)
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
새로운 엔티티면 저장( persist )
새로운 엔티티가 아니면 병합( merge )
merge: DB에 있는 데이터를 가져온다. 그리고 현재 save한 데이터를 DB에서 가져온 데이터로 바꿔치기 한다.
merge 단점: DB에 select 쿼리가 한번 나간다. merge는 가급적 쓰면 안된다.
새로운 엔티티를 구별하는 방법
매우 중요!!!
save() 메서드
- 새로운 엔티티면 저장( persist )
- 새로운 엔티티가 아니면 병합( merge )
새로운 엔티티를 판단하는 기본 전략
- 식별자가 객체일 때 null 로 판단
- 식별자가 자바 기본 타입 일때 0 으로판단
- Persistable 인터페이스를 구현해서 판단 로직 변경 가능
package org.springframework.data.domain;
public interface Persistable<ID> {
ID getId();
boolean isNew();
}
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {
@Id
private String id;
public Item(String id) {
this.id = id;
}
}
@SpringBootTest
class ItemTest {
@Autowired
ItemRepository itemRepository;
@Test
public void save() {
// 현재 PK에 값이 있는 상태라서 persist()가 호출이 안되고 merge()가 호출된다.
Item item = new Item("A");
itemRepository.save(item);
}
}
JPA 식별자 생성 전략이 @GenerateValue면 save() 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작한다. 그런데 JPA 식별자 생성 전략이 @Id만 사용해서 직접 할당이면 이미 식별자 값이 있는 상태로 save()를 호출한다. 따라서 이 경우 merge()가 호출된다. merge()는 우선 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지하고 persist()를 해서 매우 비효율적이다. 따라서 Persistable를 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 효과적이다. 예를들어 등록시간( @CreatedDate )을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다.(@CreatedDate에 값이 없으면 새로운 엔티티로 판단)
package study.datajpa.entity;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
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
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
@Id
private String id;
@CreatedDate
private LocalDateTime createdDate;
public Item(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
return createdDate == null;
}
}
'JPA > Spring Data Jpa' 카테고리의 다른 글
나머지 기능들 (0) | 2024.08.27 |
---|---|
확장 기능 (0) | 2024.08.26 |
쿼리 메소드 기능 (0) | 2024.08.26 |
공통 인터페이스 기능 (0) | 2024.08.25 |