[JPA-11] 스프링 데이터 JPA

이가희·2025년 1월 16일
0

JPA

목록 보기
12/16
post-thumbnail

자바를 하려면 스프링은 자연스럽게 따라오고,
마찬가지로 스프링에서 JPA를 하려면 스프링 데이터 JPA는 그냥 따라온다.

보통 국비 과정에서 배우는 JPA가 이 스프링 데이터 JPA를 말하는 것 같던데,
그 만큼 많이 쓰이는 스프링 데이터 JPA에 대해 오늘 알아보겠다.

1. Spring Data JPA 란?

스프링 데이터 JPA 는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트이다.

그냥 JPA를 쓸 때 보다 Spring Data JPA를 쓰면,
지루하고 반복적인 CURD 부분을 (ex INSERT INTO ... DELETE FROM.. 등) 훨씬 더 빠르고 간편하게 작성할 수 있다.

왜냐하면 Spring Data JPA는 CRUD를 처리하기 위해 공통 인터페이스를 제공하고,
개발자는 리포지토리를 개발할 때 인터페이스만 작성하기만 하면 되기 때문이다.

인터페이스 구현부는 누가 짜주는 건데? 싶을 수 있으나
실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해 주기 때문에 개발자는 구현부를 신경쓰지 않아도 된다.

간단하게 JPA와 Spring data JPA의 차이를 보여주자면,
회원 이름을 조회하는 기능을
기존 JPA에서는 em.createQuery("select m from Member m where m,name = :name", Member.class).set......
이런식으로 했다면 Spring Data Jpa에서는
Memberrepository.findByName(String name)
이렇게 간단하게 사용이 가능하다.
자세한 내용은 이어서 설명하겠다.


2. 공통 인터페이스 기능

Spring Data JPA는 CRUD를 처리하기 위한 공통 인터페이스를 제공한다고 했는데, 이것이 바로 JpaRepository 인터페이스이다.
Spring Data JPA를 사용하기 위해선 이 인터페이스를 상속받으면 된다.

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

이때 JpaRepository에 원하는 엔티티 (여기서는 회원 엔티티 = Member) 와 그 엔티티의 식별자 타입 (여기서는 Long)을 지정하면 된다.
이렇게 하면 JpaRepository 인터페이스가 제공하는 다양한 기능을 사용할 수 있다.

제공하는 주요 메소드는 다음과 같다.
(여기서 T는 엔티티, ID는 엔티티의 식별자 타입, S는 엔티티와 그 자식 타입을 의미한다)

  • save (S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다. (식별자 값이 없으면, EntityManager.persist()를 호출하고, 있으면 이미 있는 엔티티로 판단해서 EntityManager.merge()를 호출함)
  • delete(T): 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출한다.
  • findOne(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find()를 호출한다.
  • findAll(...) : 모든 엔티티를 조회한다. 정렬이나 페이징 조건을 파라미터로 제공할 수도 있다.

이 메소드들을 이용해,
정말 기초적은 CRUD는 단순 상속을 받음으로서 해결 할 수 있다.


3. 쿼리 메소드 기능

쿼리 메소드 기능은 메소드 이름만으로 적절한 JPQL 쿼리를 생성하는 등 말 그대로 쿼리 메소드를 생성하는 기능이다.
스프링 데이터 JPA 가 제공하는 쿼리 메소드 기능은 크게 3가지가 있다.

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

하나씩 알아보고, 쿼리 메소드에서 지원하는 페이징과 정렬 기능도 알아보겠다.

👊 메소드 이름으로 쿼리 생성

이메일과 이름으로 회원을 검색하려면 Repository를 상속받은 리포지토리에 다음과 같이 메소드를 정의해서 사용하면 된다.

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

엄청 간단하지 않은가?
근데 물론 마구잡이로 쓰면 안되고 규칙이 있다.

표를 보면 어떻게 사용해야 하는지 감이 올 것이다.
주의할 점으로, 엔티티의 필드명이 변경되면 인터페이스에 정의한 메소드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션 시작하는 시점에 오류가 발생하게 된다.
(엔티티 필드명 = 메소드에 사용한 이름 맞추기!)

👊 JPA NamedQuery

앞선 장들에서 설명한 NamedQuery를 Spring Data JPA에서도 사용 가능하다.
어노테이션이나 XML에 정의한 쿼리를 아래처럼 호출 할 수 있다.
여기서 Member.findByUserName이라는 이름의 쿼리를 등록하고 사용한다고 가정

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

Spring Data JPA는 선언한 "도메인 클래스 + . + 메소드 이름" 으로 Named쿼리를 찾아서 실행한다.
이렇게 간단하게 사용할 수 있다.

👊 @Query, 리포지토리 메소드에 쿼리 정의

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

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

여기서 네이티브 SQL을 사용하려면, 위의 예시처럼 nativeQuery = true 옵션을 주면 된다.

이때, 위치 기반과 이름 기반 파라미터 바인딩을 모두 지원한다.
그런데 가급적 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하는 것이 추천된다.

또한 벌크성 수정 쿼리의 경우
org.springframework.data.jpa.repository.Modifying 어노테이션을 사용하면 된다.
만약 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶다면,
@Modifying(clearAutomaitcally = true) 처럼 clearAutomatically 옵션을 주면 된다.

@Modifying
@Query ("update Product p set p.price = p.price * 1.1 where p.stockAmout < :stockAmount")
int bulkPriceUp(@Param ("stockAmout") String stockAmount);

참고로 반환 타입의 경우,
결과가 한 건 이상이면 컬렉션을, 단건이면 반환 타입을 지정하면 된다.

이때, 조회 결과가 없으면 빈 컬렉션을 나환하거나 단건의 경우 null을 반환한다.
또한 단건을 기대하고 반환 타입을 지정하였는데 2건 이상 조회되면 NonUniqueResultException이 발생된다.
(참고로 단건의 경우 JPA에서 Query.getSingleResult() 메소드를 호출하는데, 조회 결과가 없으면 NoResultException이 발생한다.
이렇게 되면 개발하는 입장에서 굉장히 수고스러운데, 스프링 데이터 JPA는 이 예외가 발생하면 예외를 무시하고 대신에 null을 반환한다.)

👊 페이징과 정렬

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

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

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

Page 사용 예시

//정의
public interface MemberRepository extends Repository<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.getContent(); //조회된 데이터
 int totalPages = result.getTotalPages(); //전체 페이지 수
 boolean hasNextPage = reuslt.hasNextPage(); //다음 페이지 존재 여부

추가로 설명하자면,
Pagable은 인터페이스인데 실제로 사용할 때는 이 인터페이스를 구현한 PageRequest 객체를 사용해야 한다.
생성자의 첫 번째 파라미터는 현재 페이지, 두 번째 파라미터는 조회할 데이터의 수, 그리고 추가적으로 정렬 정보도 파라미터로 사용할 수 있다.
참고로 페이지는 0부터 시작한다.


4. 명세

DDD 책에서 명세(Specification)라는 개념을 소개하는데,
명세를 이해하기 위한 핵심 단어는 술어(predicatie) 이다. 술어는 단순히 참이나 거짓으로 평가되고, 이것은 AND, OR 같은 연산자로 조합할 수 있다.
데이터를 검색하기 위한 제약 조건 하나하나를 술어라고 볼 수도 있다.

JPA는 org.springframework.data.jpa.domain.Specification class로 이 술어를 정의했다.
Speicification 은 컴포지트 패턴으로 구성되어 있어 여러 Specificaition을 조합할 수 있다. 그래서 다양한 검색조건을 간편하게 조립해서 새로운 검색조건을 쉽게 만들 수 있다.

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

검색조건을 조합한다는 의미는 사용 코드 예시를 보면 이해갈 것이다.


public List<Order> findOrders(String name) {
	List<Order> result = orderRepository.findall
		(where(memberName(name)).and(isOrderStatus());
	return result;
}

Speicifications는 명세들을 조립할 수 있도록 도와주는 클래스인데, where(), and(), or(), not() 메소드를 제공한다.
예시에서는 and를 사용해서 검색 조건을 조합하였다.

이제 이 명세를 어떻게 정의하는지 알아보겠다.

public class OrderSpec {
	public static Spcification<Order> memberName(final String memerName) {
    return new Specification<Order>(){
    	public Predicate toPredicate)(Root<Order> root,
        	CriteriaQuery<?> query, CriterialBuilder builder)	{
            
            if(StringUtils.isEmpty<memberName) return null;
            
            Join<Order,Member) m = root.join("member", JoinType.INNER);
            return builder.equal(m.get("name"),memberName);
            }
          };
       }
   
   public static Specification<Order> isOrderStatus() {
   	return new Specifiation<Order>() 	{
    	public Predicate toPredicate(Root<Order> root,
        	CreteriaQuery<?> query, CreteriaBuilder builder){
            	reutrn builder.equal(root.get("status"),
                	OrderStatus.ORDER);
                 }
               };
         }
  }

명세를 정의하려면 Specification 인터페이스를 구현하면 된다.
편의상 내부 무명 클래스를 사용했는데, toPredicate 메소드만 구현하면 도니다. JPA Creteria 의 Root, CreteriaQuery, CreteriaBuilder 클래스가 파라미터로 주어지고 이것들을 적절히 활용해서 검색 조건을 반환하면 된다.

예시 코드에서는 파라미터로 주어진 이름과 같은 주문들만 조회하고,
상품 상태가 ORDER인 상품들만 조회하는 검색 조건을 구현하였다.


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

스프링 데이터 JPA로 리포지토리를 개발하면, 인터페이스만 정의하고 구현체는 만들지 않는다.
그런데 다양한 이유로 메소드를 직접 구현해야 하는데 그렇다고 리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해버려야한다.
스프링 데이터 JPA는 이 문제를 우회해서 필요한 메소드만 구현하는 방법을 제공한다.

방법은 아래와 같다.

  1. 사용자 정의 인터페이스를 작성하기. 이름은 자유롭게 지으면 된다.☺️
public interface MyRepositoryCustom {
	public List<My< findMyCustom();
}
  1. 만든 사용자 정의 인터페이스를 구현할 클래스를 만든다. 이때 리포지토리 인터페이스 이름 + Impl로 지어야한다. 이렇게 하면 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.
public class MyRepositoryImpl implements MyRepositoryCustom {
	
    @Override
    public List<My> findMyCustom()	{
    	//원하는 사용자 정의 메소드 구현
    }
}
  1. 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받아 사용하기.
public interface MyRepository extends JpaRepository<My, Long> , MyRepositoryCustom {
...
}

이렇게 간단하게 사용 가능하다.


6. Web 확장

Spring Data 프로젝트는 스프링 MVC 에서 사용할 수 있는 편리한 기능을 제공한다.
도메인 클래스 컨버터 기능과, 페이징과 정렬 기능이 그것인데, 아래에서 자세히 살펴보지.

설정은 아주 간단하다.

org.springframework.data.web.config.SpringDataWebConfiguration을 스프링 빈으로 등록하면 스프링 데이터가 제공하는 Web 확장 기능을 활성화 할 수 있다.
이렇게 하면 도메인 클래스 컨버터/ 페이징과 정렬을 위한 HandlerMethodArgumentResolver가 스프링 빈으로 등록된다.

👊 도메인 클래스 컨버터 기능

도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로, 엔티티 객체를 찾아서 바인딩 해주는 것이다.
이 기능을 이용하면 굳이 파라미터로 받은 아이디로 엔티티를 찾는 로직을 작성하지 않아도 된다.

@Controller
public class MemberController {

	@RequestMapping("member")
    public String member(@RequestParam("id") Member member, Model model) {
    model.addAttribute("member", member);
    return "member";
    }
 }

코드를 보면 마치 HTTP 로 회원을 바로 넘겨받는 것 같지만,
실제로 회원 아이디만 넘겨 받아도 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체로 변환해서 넘겨준다.
이때 도메인 클래스 컨버터는 해당 엔티티와 관련된 리포지토리를 사용해서 엔티티를 찾는다.

도메인 클래스 컨버터를 통해 넘어온 회원 엔티티를 컨트롤러에서 직접 수정해도 실제 데이터 베이스에는 반영되지 않는다!
영속성 컨텍스트 동작 방식과 관련이 있는데, 컨트롤러와 뷰에서는 영속성 컨텍스트를 플러시하지 않기 때문이다. 만약 수정한 내용을 데이터베이스에 반영하고 싶으면 트랜잭션을 시작하는 서비스 계층을 호출해야 한다.

👊 페이징과 정렬 기능

스프링 MVC에서 HandlerMethodArgumentResolver를 통해 편리하게 페이징과 정렬 기능을 사용할 수 있다.

@RequestMapping(value ="/members")
public String list(Pageable pageable, Model model) {
	Page<Member> page = memberService.findMembers(pageable);
    model.addAttribute("members", page.getContent());
    return "members/memberList";
 }

파라미터로 Pageable을 받은 것을 볼 수 있다.
만약 사용해야 할 페이징 정보가 둘 이상이면 @Qualifier 어노테이션을 통해 접두사로 구분할 수 있다.
또한 Pageable의 기본값은 page=0, size=20인데 변경하고 싶다면 @PageableDefault 어노테이션을 사용하면 된다.

public String list(
	@Qualifier("member") Pageable memberPageable,
    @Qualifier("order"), @PageableDefault(size = 12, sort = "name", direction=Sort.Direction.DESC) Pageable pageable {
    ..
}

7. 스프링 데이터 JPA와 QueryDSL

스프링 데이터 JPAsms
QueryDslPredicateExecutor 과
QueryDslRepositorySupport 를 통해 QueryDSL을 지원한다.

이 중에 QueryDslPredicateExecutor 에 대해 알아보겠다.

리포지토리에 QueryDslPredicateExecutor 을 상속받으면
해당 리포지토리에서 QueryDsl을 사용 할 수 있게 된다.

// QueryDslPredicateExecutor 을 상속받은 myRepository를 통해 QueryDSL을 사용하는 예시
QMy my = Qmy.my;
Iterable<My> result = myRepository.findAll(
	my.name.contains("이").and(my.age.between(10,25))
);

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

지금까지는 인터페이스를 통해 기능을 사용하는 것에 대해 다루었다.
여기서 잠깐 스프링 데이터 JPA가 어떤 구현체로 작동하는지 살펴보겠다.

org.springframework.data.jpa.repository.support.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 적용 : JPA의 모든 변경은 트랜잭션 안에서 이뤄져야 하고, 스프링 데이터 JPA는 데이터를 변경하는 메소드에 트랜잭션 처리가 되어 있다. 만약 서비스 계층에서 트랜잭션을 시작하지 않으면 리포지토리에서 트랜잭션을 시작한다.
  • @Transactional(readOnly = true) : 데이터를 조회하는 메소드에는 이 옵션이 적용되어 있다. 이렇게 하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있다.
  • save() method : 이 메소드는 저장할 엔티티가 새로운 엔티티면 저장하고, 이미 있는 엔티티면 병합한다.

이렇게 스프링 데이터 JPA에 대해 알아보았다.
다음 시간엔 웹 애플리케이션과 영속성 관리에 대해 알아보겠다.

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

profile
안녕하세요 개발하는 사람입니다.

0개의 댓글

관련 채용 정보