본문 바로가기
JPA/스프링 DATA JPA

Data JPA 13 - 스프링 데이터 JPA 분석

by 킹차니 2022. 1. 11.

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

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

-  org.springframework.data.jpa.repository.support.SimpleJpaRepository

 

스프링 데이터 JPA 구현체인 SimpleJpaRepository의 save 메서드는 아래와 같다.

@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); 
	}
 	... 
}

IDE에서 SimpleJpaRepository를 보면 결국 해당 구현체도 JPA를 사용하여 여러 기능들을 제공해주고 있다.

 

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

 @Transactional : 트랜잭션 적용

     - JPA의 모든 변경은 트랜잭션 안에서 동작한다.

     - 스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션처리한다. (아래의 SimpleJpaRepository의 deleteById메서드도 이에 해당)

더보기
	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable)
	 */
	@Transactional
	@Override
	public void deleteById(ID id) {

		Assert.notNull(id, ID_MUST_NOT_BE_NULL);

		delete(findById(id).orElseThrow(() -> new EmptyResultDataAccessException(
				String.format("No %s entity with id %s exists!", entityInformation.getJavaType(), id), 1)));
	}

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

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

    - 그래서 스프링 데이터 JPA를 사용하면 개발자가 트랜잭션을 걸지 않아도 데이터의 등록, 수정이 가능했음

• @Transactional(readOnly = true) : 데이터를 단순히 조회하고 변경하지 않는 트랜잭션에서 해당 어노테이션을 사용하면 플러시를 생략하여 약간의 성능향상 ( 스냅샷을 찍지 않는다. )

 

save 메서드

@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);
   }
}

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

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

 

 

save메서드는 새로운 엔티티인지를 구분하여 persist또는 merge를 한다. 그런데 어떻게 새로운 엔티티임을 구분하는 것일까?

 

새로운 엔티티를 구분하는 기본 전략

  - 식별자가 객체일 때 null로 판단 ( ex. String, Long )

  - 식별자가 자바 기본 타입일 때 '0'으로 판단 ( ex. int, long )

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

 

 

이에 대해 코드로 알아보자. 아래에 간단한 Item엔티티가 있다. 해당 엔티티는 id에 @GerneratedValue를 먹였다.

@Getter
@Entity
public class Item {

    @Id @GeneratedValue //@GerneratedValu가 있음
    private Long id;
    private String name;
}

이를 스프링 데이터 JPA를 사용하여 save해보자.

// 스프링 데이터 JPA 리퍼지토리
public interface ItemRepository extends JpaRepository<Item, Long> {
}

 

테스트 코드

@SpringBootTest
class ItemRepositoryTest {

    @Autowired ItemRepository itemRepository;

    @Test
    public void save() {
        Item item = new Item();//id 세팅하지 않음
        itemRepository.save(item);
    }
}

이를 디버깅하면 아래의 SimpleJpaRepository에서 Id가 null인 것을 보고 새로운 객체로 판단한 뒤, persist를 하도록 한다.

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

   Assert.notNull(entity, "Entity must not be null.");

   if (entityInformation.isNew(entity)) {//isNew(entity)에서 true가 나오고
      em.persist(entity);//여기서 persist된다.
      return entity;
   } else {
      return em.merge(entity);
   }
}

그렇게 되면 persist되어 새로운 Item엔티티의 id값이 @GeneratedValue로 자동 생성되는 것이다.

 

이때는 문제가 없지만 아래와 같은 경우에 문제가 된다. 즉 개발자가 직접 Id를 지정해줘야 하는 경우이다.

@NoArgsConstructor(access = AccessLevel.PROTECTED) //jpa는 기본 생성자가 반드시 있어야 한다.
@Getter
@Entity
public class Item {

    @Id // @GeneratedValue 없음.
    private String id;
    private String name;

//생성자에서 개발자가 id를 직접 넣어줘야 한다.
    public Item(String id) {
        this.id = id;
    }
}

이를 테스트해보면 아래와 같다.

@SpringBootTest
class ItemRepositoryTest {

    @Autowired ItemRepository itemRepository;

    @Test
    public void save() {
        Item item = new Item("18890197");//id 직접 지정
        itemRepository.save(item);
    }
}

이 경우에는 SimpleJpaRepository의 save메서드의 isNew는 Item의 id가 이미 존재하므로 새로운 객체가 아닌 이미 존재하는 객체로 보고 merge를 하도록 한다.

1. merge를 하면 기본적으로 DB에 해당 id를 가진 데이터가 존재할 것이라고 가정하고 select 쿼리를 날린다.

2. 그래서 select를 날렸는데 없다면? 그때서야 새로운 데이터임을 알고 insert문을 날린다.

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

   Assert.notNull(entity, "Entity must not be null.");

   if (entityInformation.isNew(entity)) { // id가 18890197이므로 false
      em.persist(entity);
      return entity;
   } else {//else문으로 넘어와 merge를 한다.
      return em.merge(entity);
   }
}

 

하여 이와 같은 문제를 해결하기 위해 JPA는 Persistable 인터페이스를 제공한다.

//JPA의 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();
}

 

이를 사용하기 위해서는 위의 Persistable<id타입>을 implements하고, getId와 isNew메서드를 구현하면 된다.

@EntityListeners((AuditingEntityListener.class)) // 얘가 있어야 @CreatedDate 사용 가능
@NoArgsConstructor(access = AccessLevel.PROTECTED) //jpa는 기본 생성자가 반드시 있어야 한다.
@Entity
public class Item implements Persistable<String> {//Persistable<id 타입>

    @Id //@GeneratedValue 없음.
    private String id;
    private String name;

    @CreatedDate
    private LocalDateTime createdDate;

    public Item(String id) {this.id = id;}

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

    @Override// isNew 를 구현해야한다.
    public boolean isNew() {
        return createdDate == null;//생성일이 null이면 새로운 데이터
    }
}

이때 isNew는 아래에서 보듯이 createdDate가 null인지의 여부로 판단하도록 하였다. (@CreatedDate는 JPA기반으로 동작한다. isNew가 실행되기 이전에는 동작하지 않으므로, IsNew를 실행할때는 아직 null인 상태이다.)

 

 

이제 아래 Test를 수행하면 select문을 하지 않고 바로 insert문을 실행한다.

@SpringBootTest
class ItemRepositoryTest {

    @Autowired ItemRepository itemRepository;

    @Test
    public void save() {
        Item item = new Item("18890197");//id 직접 지정
        itemRepository.save(item);
    }
}

 

 

 

 

 

김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.