본문 바로가기

Spring

SpringDataJPA(스프링데이터JPA)를 뜯어보자

1. 스프링 데이터 JPA

💡 Spring Data Jpa는 SQL 쿼리를 직접 작성하는 대신 객체와 객체 간의 관계에 집중할 수 있게 돕는 기술, 덕분에 Java의 ORM 기술인 JPA (Java Persistence API)를 더 쉽고 편리하게 사용

2. 특징은 무엇?

  • Repository 인터페이스
    • JPA를 추상화 시킨 인터페이스만 정의하면, Spring Data JPA가 구현체를 자동으로 생성해주는 기능을 제공, 이를 통해 CRUD (Create, Read, Update, Delete) 작업을 위한 코드 중복을 줄일 수 있움
  • 쿼리 메소드
    • 메소드 이름을 분석하여 SQL 쿼리를 자동으로 생성하는 기능을 제공
    • 예를 들어, **findByName(String name)**이라는 메소드를 Repository 인터페이스에 추가하면, Spring Data JPA는 이를 "SELECT u FROM User u WHERE u.name = :name” JPQL로 변환해서 데이터베이스에 전달
  • 페이징과 정렬
    • 페이징과 정렬 기능을 간단하게 적용이 가능함
    // Service
    public Page<User> findUsersByNameWithPagination(String name, int page, int size, String sortBy) {
            Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
            return userRepository.findByName(name, pageable);
    }
    
    // Repository
    public interface UserRepository extends JpaRepository<User, Long> {
        Page<User> findByName(String name, Pageable pageable);
    }

 

3. 공통 인터페이스

  • 스프링 데이터 JPA를 사용하려면 JpaRepository를 상속받은 공통 인터페이스만 생성하면 됨
  • public interface UserRepository extends JpaRepository<User, Long> { }
  • JpaRepository<T, ID>는 무엇 이냐? 
    • T: 엔티티 타입
    • ID: 식별자 타입(PK)
  • 그러면 스프링 데이터 JPA가 구현 클래스 대신 만들어줌 (공통 CRUD를 자동으로 생성)
  • JpaRepository를 상속받은 인터페이스를 호출 하면 프록시 객체가 나옴JpaRepository를 상속받은 memberRepository를 .getClass()로 찍어봤을 때

JpaRepository를 상속받은 memberRepository를 .getClass()로 찍어봤을 때

4. 공통 인터페이스 구성

김영한님 Spring Data Jpa에서 발췌

 

5. 인터페이스를 뜯어보자

JpaRepository

@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

	@Override
	List<T> findAll();

	@Override
	List<T> findAll(Sort sort);
}
  • @NoRepositoryBean
    • 인터페이스의 구현체가 실제로 생성되지 않도록 하고 싶을 수 있기 때문에, 이러한 경우 @NoRepositoryBean 어노테이션을 사용하여 해당 인터페이스가 빈으로 생성되는 것을 방지
  • JpaRepository는 PagingAndSortingRepository와 QueryByExampleExecutor를 상속 받고 있음
    • 아 그래서 Pageable 객체를 이용해서 페이징이나 정렬을 수행할 수 있구나.!!
    • QueryByExampleExecutor 는 Example Query라는 개념을 사용하여 엔티티 객체의 일부 필드를 사용하여 데이터베이스를 검색하는 기능을 제공 (동적 쿼리의 검색 조건으로 사용하고자 할 때)

PagingAndSortingRepository

@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {

	Iterable<T> findAll(Sort sort);

	Page<T> findAll(Pageable pageable);
}
  • PagingAndSortingRepository는 CrudRepository 인터페이스를 상속 받고 있고 Sort, Pageable 객체를 사용할 수 있게 만든 인터페이스 인 것을 볼 수 있음

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html

 

PagingAndSortingRepository (Spring Data Core 3.1.0 API)

All Superinterfaces: Repository All Known Subinterfaces: ListPagingAndSortingRepository Repository fragment to provide methods to retrieve entities using the pagination and sorting abstraction. In many cases this will be combined with CrudRepository or sim

docs.spring.io

CrudRepository

@NoRepositoryBean
public interface CrudRepository<T, ID> extends Repository<T, ID> {

	<S extends T> S save(S entity);

	<S extends T> Iterable<S> saveAll(Iterable<S> entities);
}
  • 앞서 Spring Data JPA가 구현체를 자동으로 생성해서 CRUD를 제공해준다고 했는데,
  • 네이밍과 동일하게 create, read, update, delete등을 제공하기 위한 인터페이스

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html

 

CrudRepository (Spring Data Core 3.1.0 API)

All Superinterfaces: Repository All Known Subinterfaces: ListCrudRepository Interface for generic CRUD operations on a repository for a specific type. Author: Oliver Gierke, Eberhard Wolff, Jens Schauder Method Summary All MethodsInstance MethodsAbstract M

docs.spring.io

Repository

@Indexed
public interface Repository<T, ID> {
}
  • Repository는 마커 인터페이스로, 타입과 id의 타입을 설정하여 Bean 객체를 스프링 컨테이너에 올릴 수 있도록 도와주는 마킹하는? 개념 같음

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/Repository.html

 

Repository (Spring Data Core 3.1.0 API)

Type Parameters: T - the domain type the repository manages ID - the type of the id of the entity the repository manages All Known Subinterfaces: CrudRepository , ListCrudRepository , ListPagingAndSortingRepository , PagingAndSortingRepository , ReactiveCr

docs.spring.io

QueryByExampleExecutor

package org.springframework.data.repository.query;

public interface QueryByExampleExecutor<T> {

	<S extends T> Optional<S> findOne(Example<S> example);

	<S extends T> Iterable<S> findAll(Example<S> example);
}

엔티티의 일부 필드를 사용하여 검색할 때 동적 쿼리를 편리하게 만들어 주는 기능을 한다고 보면 될 것 같다.

간단히 Query by Example의 구성을 살펴보면

  • Probe: 채워진 필드가 있는 실제 도메인 객체
  • ExampleMatcher: 특정 필드를 일치시키는 방법에 대한 세부 정보를 전달.
  • Example: 는 Probe와 ExampleMatcher로 구성, 쿼리를 생성하는 데 사용
  • FetchableFluentQuery: Example에서 파생된 쿼리를 커스텀하고, Fluent API를 사용하면 쿼리에 대한 순서 및 결과를 지정

단점은 조인될 때 inner join만 가능하고, 매칭 조건이 단순에서 실무에서 사용하기 좋지않음

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example

 

Spring Data JPA - Reference Documentation

Example 121. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

6. 그냥 궁금해서 구현 클래스도 하나만 볼까?

SimpleJpaRepository

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

	private static final String ID_MUST_NOT_BE_NULL = "The given id must not be null!";

	private final JpaEntityInformation<T, ?> entityInformation;
	private final EntityManager em;
	private final PersistenceProvider provider;

	private @Nullable CrudMethodMetadata metadata;
	private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;

	// 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));
	}
}
  • 자동으로 생성해주는 JpaRepository를 간단히 살펴보면 Jpa 영속성을 관리하는 EntityManager도 보이고 실제 구현된 객체에서 em.find()하는 것을 볼 수 있다
  • SimpleJpaRepository 클래스 자체에 @Transactional(readOnly = true) 설정이 되어있는 것을 볼 수 있다. JPA는 트랜잭션 안에서 동작하기 때문에 Service 단에서 트랜잭션이 시작되지 않더라도 Repository단에서 기본으로 트랜잭션을 가져간다.
  • 추가로 @Repository 어노테이션을 통해 Bean객체로 올려주고 JPA 예외를 스프링이 추상화한 예외로 변환해 줄 수 있다고 한다.
  • 그리고 제네릭을 이용해서 여러 엔티티들을 모두 받을 수 있게 잘 설계 되어있다는 것
    "Class<T> domainType = getDomainClass(); "

Reference

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

 

Spring Data JPA - Reference Documentation

Example 121. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/

 

JPA, Hibernate, 그리고 Spring Data JPA의 차이점

개요 Spring 프레임워크는 어플리케이션을 개발할 때 필요한 수많은 강력하고 편리한 기능을 제공해준다. 하지만 많은 기술이 존재하는 만큼 Spring 프레임워크를 처음 사용하는 사람이 Spring 프레

suhwan.dev

 

정리하면, Spring Data 프로젝트는 프록시를 활용하여 CRUD, 정렬, 페이징 등 중복으로 많이 사용되는 구현체를 자동으로 쿼리를 만들어주어 쿼리를 작성하지 않고 데이터베이스에 접근 할 수 있도록 하는 인터페이스들의 모음이라고 할 수 있겠네요. 🙃
사용하기 쉬워서 안 쓸 이유가 없다는 생각이 들어요.
그래도 사용하기에 앞서 내부가 어떻게 되어있는지 살펴보면 더 잘 활용할 수 있지 않을까요?
부족한 글 읽어주셔서 감사합니다. 또한 잘못된 내용 있으면 지적해주시면 감사하겠습니다. 🙏