스프링 데이터 JPA 구현체 분석
스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체
org.springframework.data.jpa.repository.support.SimpleJpaRepository
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 적용
- Spring Bean의 Component Scan 대상이 되어 Bean으로 등록
- JPA 예외를 스프링이 추상화한 예외로 변환
- @Transactional 트랜잭션 적용
- JPA의 모든 변경은 트랜잭션 안에서 동작
- 스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션 처리
- 서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작
- 서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
- 그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했음(사실은 트랜잭션이 리포지토리 계층에 걸려있는 것임)
- @Transactional(readOnly = true)
- 데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있음
- 자세한 내용은 JPA 책 15.4.2 읽기 전용 쿼리의 성능 최적화 참고
@Transactional 을 건다는 것은 JDBC 메커니즘 안에서는 데이터베이스 커넥션에 set auto commit = false 라는 옵션을 넘기는 것이다.
그리고 @Transactional(readOnly = true) 라고 하는 것도 동일하게 위의 과정은 발생한다. 즉, readOnly = true라고 되어 있어도 실제 트랜잭션을 얻는 과정은 동일하게 일어난다는 것이다.
다만 이때 스프링은 JPA한테서 1가지 기능을 빼았는다. 즉, 플러시(flush)를 생략하라는 명령을 내린다.
본래 순수 JPA에서는 트랜잭션이 끝나면 기본적으로 JPA의 영속성 컨텍스트에 있는 정보들을 DB에 플러시를 하고 commit을 한다. 그런데 readOnly = true 라고 한다면 플러시를 생략한다. 이는 DB에 변경 감지가 일어나지 않고 데이터를 보내지 않겠다는 것이다. 어차피 readOnly 니깐 변경하는 것이 없다고 가정하는 것이다.
이 과정에서 dirty check 과 같은 과정들이 생략되기 때문에 약간의 성능 최적화도 얻을 수 있다.
매우 중요!!! * save() 메서드*
- 새로운 엔티티면 저장( persist )
- 새로운 엔티티가 아니면 병합( merge )
- 새로운 엔티티가 아니란 뜻은 DB에 한번 저장됐다가 반환된 엔티티를 뜻한다.
그럼 어떻게 새로운 엔티티인지 확인 가능할까?
새로운 엔티티를 구별하는 방법
새로운 엔티티인지 판단하는 기본 전략은 아래와 같다.
- 식별자가 객체일 때(Integer, Long...) null 로 판단한다.
- 식별자가 자바 기본타입(Primitive type : int, long...)일 때 0 으로 판단한다.
- Persistable 인터페이스를 구현해서 판단 로직 변경도 가능하다.
식별자가 객체일 때로 간단한 테스트 코드와 디버깅을 해보자
Item
// == Item Entity == //
@Entity
@Getter
public class Item {
@Id
@GeneratedValue
private Long id;
}
// == Item Repository == //
public interface ItemRepository extends JpaRepository<Item, Long> {
}
Test
@SpringBootTest
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
@Test
public void save () {
Item item = new Item();
itemRepository.save(item);
}
}
이 부분이 중요하다. 위에서 persist() 를 실행하기 전까진 식별자 값이 null 이 였지만 persist 이후로 식별자 값을 할당 받을 것을 확인할 수 있다.
이는 @GenerateValue의 매커니즘으로 em.persist()를 해야 @GenerateValue가 JPA 에서 식별자를 만들어 주입을 해주는 것이다.
문제 상황
위에서 @GenerateValue의 동작 방식을 확인해봤다.
그런데 만약 식별자 값을 직접 생성하겠다고 @GenerateValue를 생략하고 엔티티를 작성했다고 한다면 어떻게 될까?
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {
@Id
private String id;
public Item(String id) {
this.id = id;
}
}
Test
@SpringBootTest
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
@Test
public void save () {
Item item = new Item("A");
itemRepository.save(item);
}
}
테스트 코드를 보면 식별자 값으로 미리 "A"라고 정해주고 save()를 호출해줬다.
그러고 디버그를 돌려보면 엔티티 em.merge()가 실행되는 것을 확인할 수 있다.
위에서 살펴본 것처럼 식별자가 객체일 경우 새로운 엔티티라고 판단하는 것은 null 값의 유무이다. 그러나 식별자 값을 직접 정의해주는 상황에서는 이미 객체 식별자 값이 존재하므로 새로운 엔티티가 아니라고 판단하는 것이다.
실행되는 쿼리를 살펴보자.
merge는 기본적으로 이미 해당 객체가 DB에 존재하고 있다는 가정으로 실행된다. 그래서 해당 식별자도 미리 select 쿼리를 날려서 데이터를 가져오려고 할것이다.
그러나 식별자 값이 "A" 인 것을 select 해도 값이 존재하지 않으므로 이때는 새로운 엔티티라고 판단해서 insert 쿼리를 날리는 것을 확인할 수 있다.
쿼리는 보면 알겠지만 일단 비효율적으로 동작한다는 것을 확인할 수 있다.
이러한 이유도 merge는 가급적이면 사용하지 않는 방향을 추천한다.
그렇다면 실무에서 여러 상황에 의해 @GenerateValue를 못 쓰고 식별자를 직접 생성해야 할 때는 어떻게 할까?
- 이럴땐 Persistable 인터페이스를 구현해서 판단 로직을 따로 구성해야 한다.
package org.springframework.data.domain;
public interface Persistable<ID> {
ID getId();
boolean isNew();
}
결론
JPA 식별자 생성 전략이 @GenerateValue 면 save() 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작한다.
그런데 JPA 식별자 생성 전략이 @Id 만 사용해서 직접 할당이면 이미 식별자 값이 있는 상태로 save() 를 호출하게 된다.
이 경우 save()에서는 새로운 엔티티가 아니라고 판단해서 병합(merge)가 호출된다. 그런데 병합(merge)은 우선 DB를 호출해서 값을 확인 후, DB에 값이 없으면 새로운 엔티티로 호출 할 뿐아니라 엔티티의 모든 값을 교체해버리기 때문에 사이드이펙트(의도치 않은 값 변경)가 발생할 수 있다.
따라서 Persistable 를 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 효과적이다.
참고로 등록시간( @CreatedDate )을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다. (@CreatedDate에 값이 없으면 새로운 엔티티로 판단)
@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;
}
}