스프링 데이터 JPA 구현체 분석
SimpleJpaRepository클래스가 스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체다.
📌 SimpleJpaRepository (org.springframework.data.jpa.repository.support.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 애노테이션이 적용 되어 있다.
- 스프링 빈 등록
- 예외 변환 AOP를 적용해서 JPA 예외를 스프링 추상화 예외로 변환시킨다.
- 서비스 계층에서 데이터 접근 기술에 직접 의존하는 것은 좋은 설계가 아니다. 예외도 마찬가지다. 서비스 계층에서 JPA 예외에 직접 의존하면 결국 JPA에 의존하게 되는 것이다. 스프링 프레임워크는 이럼 문제를 해결하기 위해 JPA 예외를 추상화해서 제공한다.
@Transcational 애노테이션이 적용되어 있다.
- JPA의 모든 변경은 트랜잭션 안에서 동작한다.
- 스프링 JPA는 변경(등록/수정/삭제) 메서드를 트랜잭션 안에서 처리한다.
- 서비스 계층에서 트랜잭션을 시작하지 않으면 리포지토리에서 트랜잭션을 시작한다.
- 서비스 계층에서 트랜잭션을 시작하면 리포지토리는 서비스 계층에서 시작한 트랜잭션을 이어 받는다.
- 그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했다.
- 트랜잭션이 리포지토리 계층에 걸려있던 것이다.
SimpleJpaRepository에서 save() 메서드가 정의된 것을 보면 새로운 엔티티일 경우에는 persist()로 저장한다.
기존에 있던 엔티티면 merge()로 병합한다.
그러니 기존에 있는 엔티티에 대해서는 save() 메서드를 호출하지 말고 변경감지 기능을 사용할 것.
참고) @Transctional(readOnly=true)으로 성능 향상을 얻을 수 있다.
데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly 옵션을 true로 설정하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있다.
새로운 엔티티를 구별하는 방법
📌 SimpleJpaRepository (org.springframework.data.jpa.repository.support.SimpleJpaRepository)
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> ...{
@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);
}
}
...
}
SimpleJpaRepository에서 save() 메서드가 정의된 것을 보면 새로운 엔티티일 경우에는 persist()로 저장한다.
기존에 있던 엔티티면 merge()로 병합한다.
그러니 기존에 있는 엔티티에 대해서는 save() 메서드를 호출하지 말고 변경감지 기능을 사용할 것.
1. 새로운 엔티티를 판단하는 기본 전략 3가지
- 식별자가 객체일 때 null로 판단한다.
- 식별자가 자바 기본 타입일 때는 0으로 판단한다.
- Persistable 인터페이스를 구현해서 '새로운 엔티티인지' 판단 로직 변경 가능하다.
🔍 식별자가 객체일 때 null로 판단하는 전략
📌 Item 엔티티
package study.datajpa.entity;
@Entity
@Getter
@NoArgsConstructor
public class Item {
@Id @GeneratedValue
private Long id;
}
식별자 값을 Long 타입으로 설정했다.
📌 ItemRepository
package study.datajpa.repository;
public interface ItemRepository extends JpaRepository<Item, Long>{
}
📌 ItemRepositoryTest
package study.datajpa.repository;
@SpringBootTest
class ItemRepositoryTest {
@Autowired ItemRepository itemRepository;
@Test
void save() {
Item item = new Item();
itemRepository.save(item);
}
}
itemRepository.save()를 호출하면 JpaRepository 구현페인 SimpleJpaRepository 클래스의 save()가 호출된다.
새 엔티티 판별 전략을 알기 위해 SimpleJpaRepository 클래스의 save() 메서드에 체크를 하고 디버깅 모드를 설정하고 실행시켰다.
엔티티를 영속성 컨텍스트에 저장하기 전에 해당 엔티티가 새로운 엔티티인지 아닌지 판별이 필요하다. 이 때 isNew() 메서드로 판별한다. 636번째 줄에서 isNew() 메서드까지 진행될 때 해당 엔티티의 id 값은 null 값이다. 그러므로 새 엔티티로 인식하여 isNew() 메서드는 true를 반환하고 637번쨰 줄이 진행될 것이다.
637번째 줄인 em.persist()가 호출되기 전까지 아직까지 id 값이 null 값이다
em.persist()가 호출된 후에야 @GeneratedValue 애노테이션에 의해 id 값이 채워졌다.
🔍 Persistable 인터페이스를 구현해서 새로운 엔티티인지 판단하는 전략
만약에 식별자 생성 전략을 @GeneratedValue를 쓰지 않을 경우 해당 엔티티가 새로운 엔티티인지 아닌지 판단은 어떻게 해야할지 알아보자.
💡 Persistable 인터페이스 구현을 하지 않아서 생기는 문제점
📌 Item 엔티티
package study.datajpa.entity;
@Entity
@Getter
@NoArgsConstructor(access = PROTECTED)
public class Item{
@Id
private String id;
public Item(String id) {
this.id = id;
}
}
@GeneratedValue 애노테이션을 사용하지 않았다.
📌 ItemRepositoryTest
package study.datajpa.repository;
@SpringBootTest
class ItemRepositoryTest {
@Autowired ItemRepository itemRepository;
@Test
void save() {
Item item = new Item("A");
itemRepository.save(item);
}
}
@GeneratedValue 애노테이션을 사용하지 않았기에 엔티티를 생성과 동시에 수동으로 직접 식별자 값을 넣어줘야 한다.
새 엔티티 판별 전략을 알기 위해 SimpleJpaRepository 클래스의 save() 메서드에 체크를 하고 디버깅 모드를 설정하고 실행시켰다.
해당 엔티티를 생성과 동시에 식별자를 미리 주입했기 때문에 영속성 컨텍스트에 저장하기 전에 이미 식별자 값을 가지고 있다.
식별자 타입이 객체일 경우 isNew() 메서드는 식별자 값 존재 유무에 따라서 null 값일 경우에만 새로운 엔티티로 판별하기 때문에 이미 식별자가 있는 엔티티는 기존 엔티티로 판별하여 false를 반환한다.
isNew() 메서드의 반환값이 false 이므로 바로 640번쨰 줄인 em.merge()가 호출됐다.
이렇듯, 새로운 엔티티로 판별할 경우에는 em.persist()가 호출되고, 기존 엔티티로 판별할 경우에는 em.merge()가 호출된다.
이제 테스트의 출력 결과를 봐보자.
select
item0_.id as id1_0_0_
from
item item0_
where
item0_.id=?
insert
into
item
(id)
values
(?)
해당 item 엔티티가 기존 엔티티로 판별하여 em.merge()가 호출됐다.
em.merge()는 되도록이면 아예 안쓰는 것이 좋다. 일단 em.merge()가 호출되면 DB에서 select 쿼리로 먼저 조회한 후에 그리고 나서 inset 쿼리가 나간다. 매우 비효율적이다.
하지만 해당 item 엔티티는 식별자가 이미 존재하지만 새로운 엔티티다. 그렇다면 이 문제를 해결하기 위해서는 어떻게 해야할까?
이 문제를 해결하기 위해서는 Item 엔티티가 Persistable 인터페이스를 구현한 후 isNew() 메서드를 재정의하여 새로운 엔티티인지 아닌지에 대한 판별 로직을 개발자가 직접 작성해야 한다.
💡 Persistable 인터페이스를 구현을 통해 해결
📌 Persistable 인터페이스
public interface Persistable<ID> {
/**
* Returns the id of the entity.
*
* @return the id. Can be {@literal null}.
*/
@Nullable
ID getId();
/**
* Returns if the {@code Persistable} is new or was persisted already.
*
* @return if {@literal true} the object is new.
*/
boolean isNew();
}
📌 Item 엔티티
package study.datajpa.entity;
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = 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() {
// 생성일이 null 값일 떄 새로운 엔티티로 인식하고 true 반환
return createdDate==null;
}
}
Persistable<ID> 인터페이스를 구현한다. ID 제네릭 타입에는 식별자 값의 데이터 타입을 적어주면 된다.
getId() 메서드와 isNew() 메서드를 오버라이딩 해야한다.
getId() 메서드를 통해 Id 값을 얻는다.
isNew() 메서드를 통해 새로운 엔티티인지 아닌지 판별한다.
isNew() 메서드를 재정의할 때 판별 로직을 작성해야 하는데, 해당 엔티티가 새로운 엔티티인지에 대한 기준을 정해야한다. 이럴 때 @CreatedDate 애노테이션을 통해 생성일 속성을 추가해주는 것이 좋다. 생성일 값이 비워져 있을 경우에 새로운 엔티티로 판별하는 것이다.
참고로 모든 엔티티는 생성일과 수정일 속성을 기본적으로 가지고 있는 것이 좋다. 그래야 값을 추적하기 편하다.
📌 ItemRepository
package study.datajpa.repository;
public interface ItemRepository extends JpaRepository<Item, Long>{
}
📌 ItemRepositoryTest
package study.datajpa.repository;
@SpringBootTest
class ItemRepositoryTest {
@Autowired ItemRepository itemRepository;
@Test
void save() {
Item item = new Item("A");
itemRepository.save(item);
}
}
새 엔티티 판별 전략을 알기 위해 SimpleJpaRepository 클래스의 save() 메서드에 체크를 하고 디버깅 모드를 설정하고 실행시켰다.
636번째 줄인 isNew() 메서드가 호출되기 전에 식별자에는 값이 존재하지만 createDate 속성 값은 null 값이다.
해당 Item 엔티티는 Persistable 인터페이스를 구현하면서 isNew() 메서드를 재정의할 때 createDate 속성 값으로 판별하는 로직을 작성했기에 isNew() 메서드는 true를 반환할 것이다.
636번쨰 줄인 isNew()메서드가 true를 반환하여 637번쨰 줄로 넘어와서 em.persist() 메서드가 호출될 것이다.
아직 em.persist()가 호출된 것이 아니기에 createDate 속성 값은 null 값이다.
em.persist()가 호출되기 직전에 createDate 속성 값이 채워진다.
637번쨰인 em.persist()가 호출된 이후 상황이다. createDate 속성 값에 현재 시간이 담긴 것을 알 수 있다.
🧷 결론
JPA 식별자 생성 전략이 @GeneratedValue면 itemRepository.save() 메서드가 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작한다. 그런데 JPA 식별자 생성 전략이 @Id 애노테이션만 사용하여 식별자 값을 직접 할당 하는 것이라면 이미 식별자 값이 있는 상태로 itemRepository.save() 메서드를 호출하는 것이다. 따라서 이 경우 내부에서 em.merge() 메서드가 호출된다. em.merge() 메서드는 우선 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인식하므로 매우 비효율적이다. 따라서 Persistable 인터페이스를 구현해서 새로운 엔티티 판별 로직을 직접 구현하는게 효과적이다. 참고로 생성일(@CreatedDate)을 조합해서 사용하면 생성일 속성 값으로 새로운 엔티티 판별 로직을 쉽게 작성할 수 있다. 그저 생성일(@CreatedDate) 값이 null 값이면 새로운 엔티티로 인식하면 된다.
👀 참고 자료
실전! 스프링 데이터 JPA - 인프런 | 강의
스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.
www.inflearn.com
'[JPA] > 실전! 스프링 데이터 JPA' 카테고리의 다른 글
[JPA] 나머지 기능들 (0) | 2022.04.22 |
---|---|
[JPA] 확장 가능 (0) | 2022.04.20 |
[JPA] 쿼리 메소드 기능 (0) | 2022.04.17 |
[JPA] 공통 인터페이스 기능 (0) | 2022.04.16 |
[JPA] 예제 도메인 모델 (0) | 2022.04.16 |