JPA/SpringDataJPA

[SpringDataJPA] JpaRepository 인터페이스

걸어가는 신사 2022. 6. 3. 01:21

Spring Data JPA

데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 해결할 수는 방법을 제공한다.

  • CRUD를 처리하기 위한 공통 인터페이스를 제공
  • Repository를 개발할 대 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다.
  • 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.

 

JpaRepository

(1) JpaRepository 공통 인터페이스란?

간단한 CRUD 기능을 공통으로 처리하기 위해 Spring Data JPA가 제공하는 org.springframework.data.repository.JpaRepository 인터페이스

  • 인터페이스의 구현체는 애플리케이션 실행 시점에 Spring Data JPA가 생성해서 주입해준다.
  • org.springframework.data.repository.JpaRepository 를 구현한 클래스는 스캔 대상
    • MemberRepository 인터페이스가 동작한 이유
    • 실제 출력해보기 (Proxy)
    • memberRepository.getClass() ⇒ class com.sum.proxy.$ProxyXXX
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> { ... }

JpaRepository 인터페이스를 구현한 대표적인 Class는 SimpleJpaRepository 가 있다.

(2) JpaRepository 사용

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom { ... }
  • @Repository 애노테이션 생략 가능
    • 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리
    • JPA 예외를 스프링 예외로 전환하는 과정도 자동으로 처리
  • JpaRepository<T, ID>
    • T : 엔티티 타입
    • ID : 식별자 타입 (PK)

(3) JpaRepository 계층구조

  • 스프링 데이터 모듈 안에 Repository, CrudRepository, PagingAndSortingRepository는 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스이다.
  • Spring Data JPA가 제공하는 JpaRepository 인터페이스는 JPA에 특화된 기능을 제공한다.
  • 제네릭 타입
    • T : 엔티티
    • ID : 엔티티의 식별자 타입
    • S : 엔티티와 그 자식 타입
  • 주요 메서드
    • save(S) : 새로운 엔티티는 저장하고 이미 잇는 엔티티는 병합한다.
    • delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
    • findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
    • getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
    • findAll(…) : 모든 엔티티를 조회한다. 정렬(Sort)이나 페이징(Pageable) 조건을 파라미터로 제공할 수 있다.

save(S) 메소드는 엔티티에 식별자 값이 없으면 (null이면) 새로운 엔티티로 판단해서 EntityManager.persist()를 호출하고 식별자 값이 있으면 이미 있는 엔티티로 판단해서 EntityManager.merge()를 호출한다.

 

JpaRepository 인터페이스 구현체 

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

  • org.springframework.data.jpa.repository.support.SimpleJpaRepository
  • save, find, delete 등의 기본 메서드들이 구현되어져 있다.

1. 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 예외를 스프링이 추상화한 예외로 변환시켜준다.
  • @Transactional 트랜잭션 적용
    • JPA의 모든 변경은 트랜잭션 안에서 동작한다.
    • Spring Data JPA는 변경 (등록, 수정, 삭제) 메서드를 트랜잭션 처리
    • 서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작
    • 서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
    • 그래서 Spring Data JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했다. (사실은 트랜잭션이 Repository 계층에 걸려있는 것이다.)
  • @Transactional(readOnly = true)
    • 데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있다.

2. 새로운 엔티티를 구별하는 로직

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

(1) save() 메서드

  • isNew 메서드를 통해서 새로운 엔티티를 판단한다.  
  • 새로운 엔티티면 저장 (persist)
  • 새로운 엔티티가 아니면 병합 (merge)

(2) 새로운 엔티티를 판단하는 기본 전략

  • Persistable 인터페이스를 구현한 추상클래스 AbstractPersistable에 구현되어져 있는 isNew 메서드
@MappedSuperclass
public abstract class AbstractPersistable<PK extends Serializable> implements Persistable<PK> {	
    @Override
	public boolean isNew() {
		return null == getId();
	}
}
  • 식별자가 객체일 때 null로 판단
  • 식별자가 자바 기본 타입일 때 0으로 판단
  • Persistable 인터페이스를 구현해서 판단 로직 변경 가능

3. 판단 로직 변경

(1) 문제상황 (@GenerateValue 전략일때)

  • JPA 식별자 생성 전략이 @GenerateValue면 save() 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작한다.
  • 하지만 JPA 식별자 생성 전략이 @Id만 사용해서 직접 할당이면 이미 식별자 값이 있는 상태로 save()를 호출한다.
    • 따라서 이 경우 merge()가 호출된다.
  • merge()은 우선 DB를 호출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율적이다.
  • Persistable를 사용해서 새로운 엔티티 확인 여부를 직접 구현하는게 효과적이다.

(2) Persistable 인터페이스

public interface Persistable<ID> {
	ID getId();
	boolean isNew();
}
  • 새로운 엔티티를 구별하는 isNew() 메서드가 존재한다.

(3) Persistable 구현

@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;
	}
}
  • 등록시간 @CreatedDate 을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다.
  • @CreatedDate에 값이 없으면 새로운 엔티티로 판단한다.
반응형