본문 바로가기
Spring|Spring-boot/Spring-Data-JPA

스프링 데이터 JPA 구현체를 알아보자.

by oncerun 2022. 12. 3.
반응형

 

Rest API 서버를 만들거나, 웹 서버를 만들거나 데이터베이스를 빼놓을 수 없는 애플리케이션을 만들 때

어느샌가부터 Spring Framework 없이 만드는 것을 두려워하기 시작했다. 

너무 많은 기능을 제공하고 그 기능을 통해 기존과는 비교할 수 없는 생산성을 가지고 애플리케이션을 만들 수 있기 때문인 것 같다.

 

데이터베이스를 다룰 때 ORM을 사용하고 자바를 사용하고 Spring Framework 기반의 프로젝트라면 Spring-data-jpa는 너무 매력적인 스프링 프로젝트이다. 

 

그래서 오늘은 스프링 데이터 JPA 구현체에 대해 알아볼 것이다. 

 

스프링 데이터 JPA 구현체는 다음과 같은 패키지에 존재하는 SimpleJpaRepository <T, ID>이다.

package org.springframework.data.jpa.repository.support
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {}

 

 

우리가 사용하는 findById 메서드를 어떻게 구현했는지 확인해 보자.

@Override
public Optional<T> findById(ID id) {

   Assert.notNull(id, ID_MUST_NOT_BE_NULL);

   Class<T> domainType = getDomainClass();

   if (metadata == null) {
      return Optional.ofNullable(em.find(domainType, id));
   }

   LockModeType type = metadata.getLockModeType();

   Map<String, Object> hints = new HashMap<>();
   getQueryHints().withFetchGraphs(em).forEach(hints::put);

   return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

 

 

실제 JPA 기능을 사용하는 것을 확인할 수 있습니다.

 

EntityManager를 통해 Type, id 값을 받아 Optional로 감싸서 반환해준다.

부가적으로 LockMode에 대한 정보를 가져온 이후

 

EntityManager methods that take locks (lock, find, or refresh) or 

to the Query.setLockMode() or

TypedQuery.setLockMode() method.

 

다음과 같은 메서드를 사용하여 LockMode를 지정하는 경우 락 Type을 가져옵니다. 

추가적으로 QueryHint를 설정합니다. 

 

@Override
public List<T> findAllById(Iterable<ID> ids) {

   Assert.notNull(ids, "Ids must not be null!");

   if (!ids.iterator().hasNext()) {
      return Collections.emptyList();
   }

   if (entityInformation.hasCompositeId()) {

      List<T> results = new ArrayList<>();

      for (ID id : ids) {
         findById(id).ifPresent(results::add);
      }

      return results;
   }

   Collection<ID> idCollection = Streamable.of(ids).toList();

   ByIdsSpecification<T> specification = new ByIdsSpecification<>(entityInformation);
   TypedQuery<T> query = getQuery(specification, Sort.unsorted());

   return query.setParameter(specification.parameter, idCollection).getResultList();
}

 

 

흥미로운 점은 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);
   }
}

 

 

난 이 부분에서  새로운 엔티티를 판별한다는 것이 궁금하다.

 

새로운 엔티티를 판단하는 전략이 무엇일까?

/*
 * (non-Javadoc)
 * @see org.springframework.data.support.IsNewStrategy#isNew(java.lang.Object)
 */
@Override
public boolean isNew(Object entity) {

   Object value = valueLookup.apply(entity);

   if (value == null) {
      return true;
   }

   if (valueType != null && !valueType.isPrimitive()) {
      return false;
   }

   if (value instanceof Number) {
      return ((Number) value).longValue() == 0;
   }

   throw new IllegalArgumentException(
         String.format("Could not determine whether %s is new; Unsupported identifier or version property", entity));
}

 

1. 식별자가 객체일 때 null로 판단한다. 
UUID와 같은 객체로 사용하는 경우 null이면 새로운 엔티티라고 판단하여 persist 한다.

 

2. 식별자가 기본 타입의 인스턴스가 Number  라면  0으로 판단한다.

 

3. Persistable 인터페이스를 구현해서 이러한 판단 로직 변경이 가능하다. 

 

 

이는 약간의 의문점을 가질 수 있는데 우리가 식별자를 직접 할당하는 경우에는 이를 merge 할지 persist 할지 EntityManger는 어떻게 판단할까?

 

@GeneratedValue가 없다면? 

 

우리가 별다른 설정을 하지 않는다면 save() 호출 대신 merge()를 호출한다. 

 

merge는 새로운 엔티티를 저장하는 로직에 적합하지 않다.

 

그 이유는 merge라는 개념 자체가 준영속 상태의 엔티티를 영속화하기 위해 사용하는 것이지 비영속 상태의 엔티티를 영속시키기 위한 목적이 아니라는 것이다.

 

merge의 동작 방식은 한 번 데이터베이스의 select 쿼리를 날린 이후 동작한다.

 

이 과정에서 해당 데이터가 없으면 새로운 엔티티로 인식한다. 

 

따라서 저장과 관련 없는 select 쿼리가 발생한다는 문제가 있다.

 

이 경우에는 다음과 같이 Persistable 인터페이스를 구현해야 한다.

 

@ToString
@Getter @Setter
@Entity @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity implements Persistable<Long> {

    @Id
    private Long id;
    private String username;
    private Integer age;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "team_id")
    private Team team;

    @Builder
    private Member(Long id,String username, Integer age, Team team) {
        this.id = id;
        this.username = username;
        this.age = age;
        this.team = team;

    }

    @Override
    public boolean isNew() {
        return false;
    }
}

 

 

isNew를 오버라이드 해서 해당 Entity가 isNew의 조건을 정의해야 한다. 

 

아 근데 직접 id를 할당하는 경우 어떤 경우 새로운 엔티티인지 구별하는 조건이 생각보다 까다롭다. 

 

이럴 경우 @CreatedDate를 활용해볼 수 있다.

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false, insertable = true)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @CreatedBy
    @Column(updatable = false, insertable = true)
    private String createdBy;

    @LastModifiedBy
    private String updatedBy;
}

 

createdAt이 null이라면 이는 새로운 엔티티이다. Auditing이 발생하는 시점은 엔티티가 persist 되는 순간이기 때문에 새로운 엔티티를 판단할 수 있다.

 

@Override
public boolean isNew() {
    return getCreatedAt() == null;
}

 

null이면 새로운 엔티티로 판단할 수 있다.

 

-- ps

 

추가로 다음 궁금증을 풀기 위해 자료를 찾고 있는 중이다.

 

새로운 엔티티를 기본키 직접 할당 전략을 사용하는데, 이 새로운 엔티티를 변경 감지로 추가하는 경우 어떻게 될까?

 

특정 엔티티를 조회하여 영속성 콘텍스트에 저장하고 스냅숏을 뜬다.

 

특정 엔티티에 기본키 직접 할당 전략을 사용하는 엔티티를 추가한다. ( 연관관계 설정 O )

 

트랜잭션을 커밋한다. 

 

flush() 발생

 

쓰기 지연 저장소에 쿼리 저장

 

이후 DB에 쿼리 발생

 

 

이 과정에서 특정 엔티티에 추가된 새로운 엔티티는 비영속 상태의 엔티티이지만 기본키가 할당되어 있다. 

Persistable 인터페이스를 별도로 구현하지 않았다고 가정하면 이 엔티티를 저장하기 위해선 어떤 쿼리가 나가야 할까?

 

새로운 엔티티를 추가할 때 이 엔티티도 영속성 콘텍스트에 추가해야 한다.?

 

그런데 pk 값이 할당되어 있으니까 select 문 이후 insert문이 나가는 게 맞지 않을까?

 

테스트에서는 select문이 나가지 않고 insert문만 발생했다. 

 

어떤 이유일까?

반응형

'Spring|Spring-boot > Spring-Data-JPA' 카테고리의 다른 글

Initialize Database Using SQL Script in Spring Boot  (0) 2023.06.21
Projections  (1) 2022.12.03
도메인 컨버터 & 페이징과 정렬  (0) 2022.12.03
Auditing  (0) 2022.11.30
Custom Repository  (0) 2022.11.30

댓글