[JPA] 자바 ORM 표준 JPA 프로그래밍 12장

xyzw·2023년 8월 13일
0

Spring

목록 보기
18/22

스프링 데이터 JPA


스프링 데이터 JPA 소개

스프링 데이터 JPA

  • 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트
  • CRUD를 처리하기 위한 공통 인터페이스를 제공
    org.springframework.data.jpa.repository.JpaRepository
  • 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해줌
  • 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있음


스프링 데이터 프로젝트

스프링 데이터 프로젝트: JPA, 몽고DB, NEO4J, REDIS, HADOOP, GEMFIRE 같은 다양한 데이터 저장소에 대한 접근을 추상화해서 개발자 편의를 제공하고 지루하게 반복하는 데이터 접근 코드를 줄여준다.

스프링 데이터 JPA 프로젝트: 스프링 데이터 프로젝트의 하위 프로젝트 중 하나로, JPA에 특화된 기능을 제공한다.




스프링 데이터 JPA 설정

필요 라이브러리

spring-data-jpa 라이브러리 1.8.0.RELEASE 버전

<!-- 스프링 데이터 JPA -->
<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-jpa</artifactId>
	<version>1.8.0.RELEASE</version>
</dependency>

환경설정

  • XML 사용할 경우
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       	http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/data/jpa 
        http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

	<jpa:repositories base-package="jpabook.jpashop.repository" />
    
</beans>
  • JavaConfig 사용할 경우
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig { }

구현 클래스 생성


스프링 데이터 JPA는 애플리케이션을 실행할 때
basePackage에 있는 리포지토리 인터페이스들을 찾아서 해당 인터페이스를 구현한 클래스를 동적으로 생성한 다음 스프링 빈으로 등록하므로
개발자가 직접 구현 클래스를 만들지 않아도 된다.




공통 인터페이스 기능

스프링 데이터 JPA를 사용하는 가장 단순한 방법은 JpaRepository 인터페이스를 상속받는 것이다.

public interface MemberRepository extends JpaRepository<Member, Long>{
}

제네릭에 회원 엔티티(Member)와 회원 엔티티의 식별자 타입(Long)을 지정한다.
회원 리포지토리는 JpaRepository 인터페이스가 제공하는 다양한 기능을 사용할 수 있다.

주요 메소드

T: 엔티티, ID: 엔티티의 식별자 타입, S: 엔티티와 그 자식 타입

  • save(S): 새로운 엔티티는 저장(내부에서 EntityManager.persist() 호출)하고, 이미 있는 엔티티는 수정(내부에서 EntityManager.merge() 호출)
  • delete(T): 엔티티 하나를 삭제, 내부에서 EntityManager.remove() 호출
  • findOne(ID): 엔티티 하나 조회, 내부에서 EntityManager.find() 호출
  • getOne(ID): 엔티티를 프록시로 조회, 내부에서 EntityManager.getReference() 호출
  • findAll(...): 모든 엔티티 조회, 정렬이나 페이징 조건을 파라미터로 제공 가능



쿼리 메소드 기능

1. 메소드 이름으로 쿼리 생성

이메일과 이름으로 회원을 조회하려면 다음과 같은 메소드를 정의하면 된다.

public interface MemberRepository extends JpaRepository<Member, Long>{
	List<Member> findByEmailAndName(String email, String name);
}

스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행한다.

select m from Member m where m.email = ?1 and m.name = ?2

2. 메소드 이름으로 JPA NamedQuery 호출

스프링 데이터 JPA는 메소드 이름으로 JPA Named 쿼리를 호출하는 기능을 제공한다.

JPA Named 쿼리는 이름 그대로 쿼리에 이름을 부여해서 사용하는 방법인데, 어노테이션이나 XML에 쿼리를 정의할 수 있다. 같은 방법으로 Named 네이티브 쿼리도 지원한다.

  • @NamedQuery 어노테이션으로 Named 쿼리 정의
@Entity
@NamedQuery(
	name="Member.findByUsername"
    query="select m from Member m where m.username = :username")
public class Member {
	...
}
  • orm.xml의 XML 사용
<named-query name="Member.findByUsername">
	<query><CDATA[
    	select m
        from Member m
        where m.username = :username
    ]></query>
</named-query>

이렇게 정의한 Named 쿼리를 JPA에서 직접 호출하려면 아래처럼 코드를 작성해야 한다.

public class MemberRepository {
	public List<Member> findByUsername(String username) {
    	...
        List<Member> resultList = 
        	em.createNamedQuery("Member.findByUsername", Member.class)
            	.setParameter("username", "회원1")
                .getResultList();
    }
}

스프링 데이터 JPA를 사용하면 다음과 같이 메소드 이름만으로 Named 쿼리를 호출할 수 있다.

public interface MemberRepository extends JpaRepository<Member, Long> {
	List<Member> findByUsername(@Param("username") String username);
}

스프링 데이터 JPA는 선언한 "도메인 클래스 + . + 메소드 이름"으로 Named 쿼리를 찾아서 실행한다.
따라서 위에서 Member.findByUsername 이라는 Named 쿼리를 실행한다.
만약 실행할 Named 쿼리가 없으면 메소드 이름으로 쿼리 생성 전략을 사용한다.

3. @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의

리포지토리 메소드에 직접 쿼리를 정의하려면 @org.springframework.data.jpa.repository.Query 어노테이션을 사용한다.

public interface MemberRepository extends JpaRepository<Member, Long> {
	
    @Query("select m from Member m where m.username = ?1")
    Member findByUsername(String username);
}

네이티브 SQL을 사용하려면 @Query 어노테이션에 nativeQuery = true를 설정한다.

public interface MemberRepository extends JpaRepository<Member, Long> {
	
    @Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0",
    	nativeQuery = true)
    Member findByUsername(String username);
}

스프링 데이터 JPA가 지원하는 파라미터 바인딩을 사용하면 JPQL은 위치 기반 파라미터를 1부터 시작하지만,
네이티브 SQL은 0부터 시작한다.


파라미터 바인딩

스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다.

select m from Member m where m.username = ?1  //위치 기반
select m from Member m where m.username = :name  //이름 기반

기본값은 위치 기반인데, 파라미터 순서로 바인딩한다.

이름 기반 파라미터 바인딩을 사용하려면 org.springframework.data.jpa.repository.Param(파라미터 이름) 어노테이션을 사용하면 된다.

코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자.

벌크성 수정 쿼리

스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 org.springframework.data.jpa.repository.Modifying 어노테이션을 사용하면 된다.

벌크성 수정 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶으면 @Modifying(clearAutomatically = true)로 설정하면 된다.

반환 타입

스프링 데이터 JPA는 결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고, 단건이면 반환 타입을 지정한다.

만약 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고, 단건은 null을 반환한다.

그리고 단건을 기대하고 반환 타입을 지정했는데 결과가 2건 이상 조회되면 javax.persistence.NonUniqueResultException 예외가 발생한다.

페이징과 정렬

스프링 데이터 JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공한다.

  • org.springframework.data.domain.Sort: 정렬 기능
  • org.springframework.data.domain.Pageable: 페이징 기능(내부에 Sort 포함)

파라미터에 Pageable을 사용하면 반환 타입으로 List나 org.springframework.data.domain.Page를 사용할 수 있다.
반환 타입으로 Page를 사용하면 스프링 데이터 JPA는 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.

페이징과 정렬을 사용하는 예제 코드를 보자.

  • 검색 조건: 이름이 김으로 시작하는 회원
  • 정렬 조건: 이름으로 내림차순
  • 페이징 조건: 첫번째 페이지, 페이지당 보여줄 데이터는 10건
public interface MemberRepository extends JpaRepository<Member, Long> {
	
    Page<Member> findByNameStartingWith(String name, Pageable pageable);
}
//페이징 조건과 정렬 조건 설정
PageRequest pageRequest = 
	new PageRequest(0, 10, new Sort(Direction.DESC, "name"));
    
Page<Member> result =
	memberRepository.findByNameStartingWith("김", pageRequest);
    
List<Member> members = result.getCount();  //조회된 데이터
int totalPages = result.getTotalPages();  //전체 페이지 수
boolean hasNextPage = result.hasNextPage();  //다음 페이지 존재 여부

실제로 사용할 때는 Pageable 인터페이스를 구현한 PageRequest 객체를 사용한다.
PageRequest 생성자의 첫번째 파라미터에는 현재 페이지를, 두번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다.
페이지는 0부터 시작한다.

힌트

JPA 쿼리 힌트를 사용하려면 org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용하면 된다.

@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly",
	value = "true") }, forCounting = true)
Page<Member> findByName(String name, Pageable pageable);

forCounting 속성: 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리에도 쿼리 힌트를 적용할지를 설정하는 옵션 (기본값: true)

Lock

쿼리 시 락을 걸려면 org.springframework.data.jpa.repository.Lock 어노테이션을 사용하면 된다.

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);



명세

명세를 이해하기 위한 핵심 단어는 술어인데, 이것은 단순히 참이나 거짓으로 평가된다.
그리고 이것은 AND, OR과 같은 연산자로 조합할 수 있다.

스프링 데이터 JPA는 이 술어를 org.springframework.data.jpa.domain.Specification 클래스로 정의했다.

Specification은 컴포지트 패턴으로 구성되어 있어서 여러 Specification을 조합할 수 있다.
따라서 다양한 검색 조건을 조립해서 새로운 검색 조건을 쉽게 만들 수 있다.

명세 기능을 사용하려면 org.springframework.data.jpa.repository.JpaSpecificationExecutor 인터페이스를 상속받으면 된다.




사용자 정의 리포지토리 구현

먼저 직접 구현할 메소드를 위한 사용자 정의 인터페이스를 작성해야 한다. 인터페이스 이름은 자유롭게 지으면 된다.

public interface MemberRepositoryCustom {
	public List<Member> findMemberCustom();
}

다음으로 사용자 정의 인터페이스를 구현한 클래스를 작성해야 한다. 이때 클래스 이름은 리포지토리 인터페이스 이름 + Impl 로 지어야 한다. 이렇게 하면 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.

public class MemberRepositoryImpl implements MemberRepositoryCustom {
	
    @Override
    public List<Member> findMemberCustom() {
    	...  //사용자 정의 구현
    }
}

마지막으로 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다.

public interface MemberRepository 
	extends JpaRepository<Member, Long>, MemberRepositoryCustom{
}



Web 확장

스프링 데이터 프로젝트는 스프링 MVC에서 사용할 수 있는 편리한 기능을 제공한다.

설정

스프링 데이터가 제공하는 Web 확장 기능을 활성화하려면

  • XML을 사용하는 경우
    org.springframework.data.web.config.SpringDataWebConfiguration을 스프링 빈으로 등록하면 된다.
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
  • JavaConfig를 사용하는 경우
    org.springframework.data.web.config.EnableSpringDataWebSupport 어노테이션을 사용하면 된다.
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
	...
}

설정을 완료하면 도메인 클래스 컨버터와 페이징과 정렬을 위한 HandlerMethodArgumentResolver가 스프링 빈으로 등록된다.

등록되는 도메인 클래스 컨버터는 다음과 같다.
org.springframework.data.repository.support.DomainClassConverter

도메인 클래스 컨버터 기능

도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩해준다.

예를 들어 특정 회원을 수정하는 화면을 보여주려면 컨트롤러는 HTTP 요청으로 넘어온 회원의 아이디를 사용해서 리포지토리를 통해 회원 엔티티를 조회해야 한다.
/member/memberUpdateForm?id=1 를 호출했다고 가정하자.

@Controller
public class MemberController {

    @Autowired MemberRepository memberRepository;
    
    @RequestMapping("member/memberUpdateFrom")
    public String memberUpdateFrom(@RequestParam("id") Long id, Model model) {
    	Member member = memberRepository.findOne(id);  //회원을 찾는다.
        model.addAttribute("member", member);
        return "member/memberSaveFrom";
    }
}

컨트롤러에서 파라미터로 넘어온 회원 아이디로 회원 엔티티를 찾는다. 그리고 찾아온 회원 엔티티를 model을 사용해서 뷰에 넘겨준다.

이번에는 도메인 클래스 컨버터를 적용한 예제를 보자.

@Controller
public class MemberController {
    
    @RequestMapping("member/memberUpdateFrom")
    public String memberUpdateFrom(@RequestParam("id") Member member, Model model) {
        model.addAttribute("member", member);
        return "member/memberSaveFrom";
    }
}

@RequestParam("id") Member member 부분을 보면
HTTP 요청으로 회원 아이디를 받지만, 도메인 클래스 컨버터가 중간에 동작해서 아이디를 회원 엔티티 객체로 변환해서 넘겨준다. 따라서 컨트롤러를 단순하게 사용할 수 있다.

도메인 클래스 컨버터는 해당 엔티티와 관련된 리포지토리를 사용해서 엔티티를 찾는다.

페이징과 정렬 기능

스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver를 제공한다.

  • 페이징 기능: PageableHandlerMethodArgumentResolver
  • 정렬 기능: SortHandlerMethodArgumentResolver
@RequestMapping(value = "/members", method = RequestMethod.GET)
public String list(Pageable pageable, Model model) {
    	
    Page<Member> page = memberService.findMembers(pageable);
    model.addAttribute("members", page.getContent());
    return "members/memberList";
}

페이징과 정렬 예제를 보면 파라미터로 Pageable을 받았는데, Pageable은 다음 요청 파라미터 정보로 만들어진다.

  • page: 현재 페이지, 0부터 시작
  • size: 한 페이지에 노출할 데이터 건수
  • sort: 정렬 조건을 정의한다.

접두사

사용해야 할 페이징 정보가 둘 이상이면 접두사를 사용해서 구분할 수 있다.
접두사는 스프링 프레임워크가 제공하는 @Qualifier 어노테이션을 사용한다. 그리고 "{접두사명}_"으로 구분한다.

public String list (
	@Qualifier("member") Pageable memberPageable,
    @Qualifier("order") Pageable orderPageable, ...
예) /members?member_page=0&order_page=1

기본값

Pageable의 기본값은 page=0, size=20이다. 만약 기본값을 변경하고 싶으면 @PageableDefault 어노테이션을 사용하면 된다.




스프링 데이터 JPA와 QueryDSL 통합

스프링 데이터 JPA는 2가지 방법으로 QueryDSL을 지원한다.

  • org.springframework.data.querydsl.QueryDslPredicateExecutor
  • org.springframework.data.querydsl.QueryDslRepositorySupport

QueryDslPredicateExecutor 사용

리포지토리에서 QueryDslPredicateExecutor를 상속받으면 된다.

public interface ItemRepository
	extends JpaRepository<Item, Long>, QueryDslPredicateExecutor<Item> {
}

QueryDslPredicateExecutor 인터페이스를 보면 QueryDSL을 검색 조건으로 사용하면서 스프링 데이터 JPA가 제공하는 페이징과 정렬 기능도 함께 사용할 수 있다.
그러나 join, fetch를 사용할 수 없다.
따라서 QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용하거나 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용해야 한다.

QueryDslRepositorySupport 사용

QueryDSL의 모든 기능을 사용하려면 JPAQuery 객체를 직접 생성해서 사용하면 된다. 이때 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 상속받으면 조금 더 편리하게 QueryDSL을 사용할 수 있다.

public interface OrderRepositoryImpl
	extends QueryDslRepositorySupport implements CustomOrderRepository {
}

1개의 댓글

comment-user-thumbnail
2023년 8월 13일

좋은 글 감사합니다. 자주 방문할게요 :)

답글 달기