스터디/JPA 스터디

[JPA] 스프링 데이터 JPA

제이온 (Jayon) 2022. 2. 5.

jpa-study에서 스터디를 진행하고 있습니다.

스프링 데이터 JPA 소개

  • 스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트이다.
  • CRUD를 처리하기 위한 공통 인터페이스를 제공하고, Repository를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성하여 주입해 준다.
  • 일반적인 CRUD 메소드는 JpaRepository 인터페이스가 공통으로 제공하고, findByUsername() 과 같이 공통으로 처리할 수 없는 메소드는 스프링 데이터 JPA가 메소드를 분석하여 JPQL로 변환한다.

 

스프링 데이터 프로젝트

Untitled

  • 스프링 데이터 JPA는 스프링 데이터 프로젝트의 하위 프로젝트이다.

 

스프링 데이터 JPA 작성

  • spring-data-jpa 라이브러리를 gradle 혹은 maven을 이용하여 설치한다.
  • 사용할 Repository를 JpaRepository 를 상속한 인터페이스로 정의하면 끝이다.

 

Untitled

 

  • 위에서 언급했듯이, 인터페이스만 만들어 두면 스프링 데이터 JPA가 자동으로 실행 시점에 구현 클래스를 만들어 주입해 준다.

 

공통 인터페이스 기능

  • 스프링 데이터 JPA를 사용하는 가장 단순한 방법은 JpaRepository 를 상속받는 것이다.
  • JpaRepository 의 계층 구조는 다음과 같다.

 

Untitled

 

쿼리 메소드 기능

  • 스프링 데이터 JPA가 제공하는 쿼리 메소드 기능은 크게 3가지가 있다.
    • 메소드 이름으로 쿼리 생성
    • 메소드 이름으로 JPA NamedQuery 호출
    • @Query 어노테이션을 사용하여 Repository 인터페이스에 쿼리 직접 정의

 

메소드 이름으로 쿼리 생성

  • 스프링 데이터 JPA에서 정해진 규칙에 따라서 메소드 이름을 지으면 내부적으로 알아서 JPQL로 변경한다. 예를 들어, findByEmailAndName() 같이 메소드를 정의하면 된다.

 

JPA NamedQuery

  • JPA Named 쿼리는 쿼리에 이름을 부여해서 사용하는 방법이다.
  • @NamedQuery 어노테이션을 사용하거나, XML에 쿼리를 정의할 수 있다. 주로 전자의 방식을 많이 사용한다.

 

// NamedQuery 정의
@Entity
@NamedQuery(
    name = "Member.findByUsername",
    query = "select m from Member m where m.username = :username")
public class Member {
    ...
}

// NamedQuery 호출
public interface MemberRepository extends JpaRepository<Member, Long> {

    List<Member> findByUsername(@Param("username") String username);
}

 

  • 스프링 데이터 JPA는 “도메인 클래스 + . + 메소드 이름”으로 Named 쿼리를 알아서 찾아서 실행한다.

 

@Query, Repository 메소드에 쿼리 정의

  • 실행할 메소드 위에 @Query 어노테이션을 통해 정적 쿼리를 직접 작성할 수 있다.

 

public interface MemberRepository extends JpaRepository<Member, Long> {

    @Query("select m from Member m where m.username = ?1")
    List<Member> findByUsername(String username);
}

 

  • 만약 네이티브 쿼리를 사용하고 싶다면, nativeQuery 옵션에 true를 주고 네이티브 쿼리 문법을 지켜서 작성하면 된다.

 

파라미터 바인딩

  • 스프링 데이터 JPA는 이름 기반 파라미터 바인딩(:username)과 위치 기반 파라미터 바인딩(?1)을 모두 지원한다. 코드 가독성을 위해 전자의 방식을 사용하는 것을 추천한다.

 

기타 기능

  • 벌크 연산
  • 페이징과 정렬
  • JPA 쿼리 힌트
    • SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트임.
  • Lock

 

명세

  • 명세를 이해하기 위한 핵심 단어는 술어이다. 술어는 데이터를 검색하기 위한 제약 조건 하나 하나를 의미한다.
  • 스프링 데이터 JPA는 Specification 클래스로 정의하였는데, 다양한 검색 조건을 조립하여 새로운 검색 조건을 쉽게 만들 수 있다.

 

// 인터페이스 정의
public interface OrderRepository extends JpaRepository<Order, Long>, 
JpaSpecificationExecutor<Order> {

}

// 명세 정의
public class OrderSpec {

    public static Specification<Order> memberName(String memberName) {
        return new Specification<Order>() {
            public Predicate toPredicate(Root<Order> root,
                CriteriaQuery<?> query, CriteriaBuilder builder) {

                if (StringUtils.isEmpty(memberName)) return null;

                Join<Order, Member> m = root.join("member", JoinType.INNER);
                return builder.equal(m.get("name"), memberName);
            }
        }
    };
    // 비슷한 방식으로 isOrderStatus() 구현
}

// 명세 사용
List<Order> result = orderRepository.findAll(
    where(memberName(name)).and(isOrderStatus())
);

 

  • JPA Criteria 방식으로 명세를 위와 같이 정의하고 사용할 수 있다.

 

사용자 정의 Repository 구현

  • 스프링 데이터 JPA로 Repository를 개발하면 인터페이스만 정의하고 구현체는 만들지 않는데, 특정 메소드를 직접 구현하기 위해 구현체를 만들어야 하는 경우가 있다.
  • 이때 Repository를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현한다는 문제가 있다. 스프링 데이터 JPA는 해당 문제를 해결하여 필요한 메소드만 구현해 주는 방식을 제공한다.

 

// 사용자 정의 인터페이스
public interface MemberRepositoryCustom {

    public List<Member> findMemberCustom();
}

// 사용자 정의 구현 클래스
public class MemberRepositoryImpl implements MemberRepositoryCustom {

    @Override
    public List<Member> findMemberCustom() {
        ...
    }
}

// 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, 
    MemberRepositoryCustom {

}

 

  • Repository 구현 클래스 이름 끝에 반드시 Impl을 붙여 주어야 스프링 데이터 JPA가 사용자 정의 Repository로 인식한다.

 

Web 확장

설정

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
}

 

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

 

도메인 클래스 컨버터 기능

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

 

@Controller
public class MemberComtroller {

    @RequestMapping("member/memberUpdateForm")
    public String memberUpdateForm(@ReqeustParam("id") Member member, Model model) {
        model.addAttribute("member", member);
        return "member/memberSaveForm";

 

  • @RequestParam을 보면 도메인 클래스 컨버터가 중간에 동작해서 아이디를 회원 엔티티로 변환하여 넘겨주는 것을 볼 수 있다.

 

페이징과 정렬 기능

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

 

  • page, size, sort 요청 파라미터를 파싱하여 자동으로 Pageable 객체를 만들어서 넘겨준다.
  • ex) /members?page=0&size=20&sort=name,desc&sort=address.city

 

스프링 데이터 JPA가 사용하는 구현체

  • 스프링 데이터 JPA가 제공하는 공통 인터페이스는 SimpleJpaRepository 클래스가 구현한다.

 

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, Id extends Serializable> implements
    JpaRepository<T, ID>, JpaSpecificationExecutor<T> {

    @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 트랜잭션 적용
  • save() 메소드: 저장할 엔티티가 새로운 엔티티면 저장하고, 이미 있는 엔티티면 병합(merge)한다.
    • 식별자가 객체일 때는 null, 기본 타입일 때는 숫자 0 값이면 새로운 엔티티로 판단한다.

 

스프링 데이터 JPA와 QueryDSL 통합

  • QUeryDslPredicateExecutor 또는 QueryDslRepositorySupport를 사용하여 QueryDSL을 사용할 수 있다.

 

출처

김영한 - 자바 ORM 표준 JPA 프로그래밍

댓글

추천 글