본문 바로가기

Database & ORM/JPA 프로젝트

9. 스프링 데이터 JPA 적용

스프링 데이터 JPA

스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트이다. Repository에서 지루하게 반복되는 CRUD를 처리하기 위해 공통 인터페이스를 제공한다.
Repository를 개발할 때 인터페이스만 작성하면, 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다.

아래와 같이 JpaRepository를 상속받은 Interface를 구현해주면

아래와 같은 구현체를 실행 시점에 스프링 데이터 JPA가 생성해서 주입해준다.
따라서 개발자가 직접 구현체를 개발하지 않아도 된다.

 

JpaRepository

스프링 데이터 JPA는 간단한 CRUD 기능을 공통으로 처리하는 JPA Repository를 제공한다.
JpaReposiztory의 제네릭은 <엔터티 타입, 식별자 타입> 으로 설정한다.

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

 

주의
T findOne(ID) 은 Optional findById(ID) 로 변경되었다.

 

주요 메서드

  • save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
  • delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
  • findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
  • getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
  • findAll(…) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.

쿼리 메소드 기능

쿼리 메소드 기능은 스프링 데이터 JPA가 제공하는 마법 같은 기능이다.
대표적으로 메소드 이름으로 적절한 JPQL을 생성하는 기능이 있다.

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능은 크게 3가지가 있다.

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 호출
  • @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의

메소드 이름으로 쿼리 생성

메소드 이름을 분석해서 JPQL 쿼리를 실행한다.

순수 JPA 리포지토리
이름과 나이를 기준으로 회원을 조회하는 메소드

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

정해진 규칙에 따라서 메소드 이름을 지어야 한다.
스프링 데이터 JPA 공식 문서를 참조해서 메소드 이름을 만들면 된다. 아래 표는 JPA에 공식 문서가 제공하는 표이다.

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능

  • 조회: find…By ,read…By ,query…By get…By,
    예:) findHelloBy 처럼 ...에 식별하기 위한 내용(설명)이 들어가도 된다.
  • COUNT: count…By 반환타입 long
  • EXISTS: exists…By 반환타입 boolean
  • 삭제: delete…By, remove…By 반환타입 long
  • DISTINCT: findDistinct, findMemberDistinctBy
  • LIMIT: findFirst3, findFirst, findTop, findTop3

 

참고로, 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.

메소드 이름으로 JPA NamedQuery 호출

메소드 이름으로 JPA Named 쿼리를 호출하는 기능이다.
JPA Named 쿼리는 아래와 같이 @NamedQuery 애노테이션을 이용해 쿼리에 이름을 부여해서 사용하는 방법이다.

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

순수 JPA 리포지토리

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

스프링 데이터 JPA

@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);

@Query를 생략하고, 아래처럼 메서드 이름만으로 Named query를 호출할 수 있다.

public interface MemberRepository
 extends JpaRepository<Member, Long> { 
 List<Member> findByUsername(@Param("username") String username);
} 
  • 스프링 데이터 JPA는 선언한 "도메인 클래스+.(점)+메서드 이름"으로 Named Query를 찾아서 실행한다.
  • 여기서는 Member.findByUername이 된다.
  • 만약 실행할 Named Query가 없으면 메서드 이름으로 JPQL 쿼리 생성 전략을 사용한다.

스프링 데이터 JPA를 사용하면 실무에서 Named Query를 직접 등록해서 사용하는 일은 드물다.
대신 @Query를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.

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

  • 이름 없는 Named Query라 할 수 있다.
  • @Query 애노테이션을 사용해서 JPQL을 작성하면 된다.

실무에서는 @Query 기능을 자주 사용한다.

public interface MemberRepository extends JpaRepository<Member, Long> {
	
  @Query("select m from Member m where m.username= :username and m.age = :age")
  List<Member> findUser(@Param("username") String username, @Param("age") int
age);
}
 
  • 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다.
    • @Query에 JPQL 작성 시 띄어쓰기를 조심해야 한다.

@Query, 값, DTO 조회하기

단순히 값 하나를 조회

@Query("select m.username from Member m")
List<String> findUsernameList();

JPA 값 타입( @Embedded )도 위 방식으로 조회할 수 있다

DTO로 직접 조회

@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
"from Member m join m.team t")
List<MemberDto> findMemberDto();

DTO로 조회할 때, 스프링 데이터 JPA에서 DTO의 new 명령어를 사용해야한다.
따라서 아래와 같이, MemberDto(m.id, m.username, t.name)에 맞는 생성자가 필요하다.

@Data
public class MemberDto {

   private Long id;
   private String username;
   private String teamName;

   public MemberDto(Long id, String username, String teamName) {
       this.id = id;
       this.username = username;
       this.teamName = teamName;
   }
}

파라미터 바인딩

스프링 데이터 JPA는 위치 기반 파라미터 바인딩 이름 기반 파라미터 바인딩을 모두 지원한다. 기본 값은 위치 기반 파라미터 바인딩이다.
코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자

위치 기반 파라미터 바인딩

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

@Query(select m from Member m where m.username = ?0)
Member findByUsername(String username);

이름 기반 파라미터 바인딩

이름 기반은 @Param 애노테이션을 사용하면 된다.

@Query(select m from Member m where m.username = :name)
Member findByUsername(@Param("name") String username);

프로젝트에 스프링 데이터 JPA 적용

순수 JPA를 사용했던 Repository를 스프링 데이터 JPA로 변경하였다.

ItemRepository

순수 JPA 리포지토리

@Repository
public class ItemRepository {

    @PersistenceContext
    EntityManager em;

    public void save(Item item) {
        if (item.getId() == null) {
            em.persist(item);
        } else {
            em.merge(item);
        }
    }

    public Item findOne(Long id) {
        return em.find(Item.class, id);
    }

    public List<Item> findAll() {
        return em.createQuery("select i from Item i",Item.class).getResultList();
    }
}

스프링 데이터 JPA
순수 JPA 리포지터리에서 작성한 save, findOne, findAll 기능은 모두 JpaRepository가 제공한다.

@Repository
public interface ItemRepository extends JpaRepository<Item, Long> {

}

MemberRepository

순수 JPA 리포지토리

@Repository
public class MemberRepository {

  @PersistenceContext
  EntityManager em;

  public void save(Member member) {
      em.persist(member);
  }

  public Member findOne(Long id) {
      return em.find(Member.class, id);
  }

  public List<Member> findAll() {
      return em.createQuery("select m from Member m", Member.class)
              .getResultList();
  }

  public List<Member> findByName(String name) {
      return em.createQuery("select m from Member m where m.name = :name", Member.class)
              .setParameter("name", name)
              .getResultList();
  }
}

스프링 데이터 JPA
순수 JPA 리포지터리에서 작성한 save, findOne, findAll 기능은 모두 JpaRepository가 제공한다. findByName은 스프링 데이터 JPA가 메소드의 이름을 분석해서 메소드 이름으로 적절한 JPQL을 실행해준다.

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByName(String name);
}