대부분 데이터 접근 계층은 일명 CRUD로 같은 코드를 반복 개발해야 한다. JPA를 사용하여 이 같은 문제가 발생한다.
회원 리포지토리와 상품 리포지토리가 하는 일이 비슷하다. 이런 문제를 해결하려면 제네릭과 상속을 적절히 사용해서 공통 부분을 처리하는 부모 클래스를 만들면 된다. 이것을 GenericDAO라 한다. 이 방법은 공통 기능을 구현한 부모 클래스에 너무 종속되고 구현 클래스 상속이 가지는 단점에 노출된다.
1. 스프링 데이터 JPA 소개
데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
일반적 CRUD 메서드는 JpaRepository 인터페이스가 공통으로 제공하므로 문제가 없다. 그런데 MemberRepository.findByUsername{...} 처럼 직접 작성한 공통으로 처리할 수 없는 메소드는
스프링 데이터 jpa가 메서드 이름을 분석해 위와 같이 실행한다.
스프링 데이터 프로젝트
스프링 데이터 프로젝트는 JPA, 몽고 DB, NEO4J,REDIS,HADOOP, GEMFIRE 같은 다양한 데이터 저장소에 대한 접근을 추상화해서 개발자 편의를 제공하고 지루하게 반복하는 데이터 접근 코드를 줄여준다.
여기서 스프링 데이터 JPA 프로젝트는 JPA에 특화된 기능을 제공한다.
스프링 프레임워크와 JPA를 같이 사용한다면 SPRING DATA JPA 사용을 적극 추천한다.
2. 스프링 데이터 JPA 설정
- 필요 라이브러리
- 환경 설정
<jpa :repositories> 를 사용하고 검색할 base-package를 적는다.
3. 공통 인터페이스 기능
스프링 데이터 JPA는 간단한 CRUD 기능을 공통으로 처리하는 JpaRepository 인터페이스를 제공한다. 스프링 데이터 jpa를 사용하는 가장 단순한 방법은 밑과 같이 이 이너페이스를 상속받는 것이다. 그리고 제네릭에 엔티티 클래스와 엔티티 클래스가 사용하는 식별자 타입을 지정하면 된다.
위를 보면 제네릭에 회원 엔티티와 회원 엔티티 식별자 타입을 지정했다. (JpaRepository <Member, Lng>) 이제부터 회원 리포지토리는 JpaRepository 인터페이스가 제공하는 다양한 기능을 사용할 수 있다.
JpaRepository 인터페이스를 상속받으면 사용할 수 있는 주요 메서드 몇 가지를 알아보자.
- save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다.
- delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출한다.
- findOne(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find()를 호출한다.
- getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()를 호출한다.
- findAll(...) : 모든 엔티티를 조회한다 . 정렬이나 페이징 조건을 파라미터로 제공할 수 있다.
save(S) 메서드는 엔티티에 식별자 값이 없으면(null) 새로운 엔티티로 판단해서 EntityManager.persist()를 호출하고 식별자 값이 있으면 이미 있는 엔티티로 판단해서 EntitiyManager.merge()를 호출한다.
4. 쿼리 메서드 기능
- 메서드 이름으로 쿼리 생성
- 메서드 이름으로 JPA NamedQuery 호출
- @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의
이 기능들을 활용하면 인터페이스만으로 필요한 대부분 쿼리 기능을 개발 할 수 있다.
메서드 이름으로 쿼리 생성
인터페이스에 정의한 findByEmailAndName (...) 메서드를 실행하면 스프링 데이터 JPA는 메서드 이름을 분석해서 JPQL을 생성하고 실행한다. 실행된 JPQL은 다음과 같다.
물론 정해진 규칙에 따라서 메서드 이름을 지어야 한다.
참고로 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
JPA NamedQuery
어노테이션이나 XML에 쿼리를 정의할수 있고, 같은 방법으로 Named 네이티브 쿼리도 지원한다.
이렇게 정의한 Named 쿼리를 JPA에서 직접 호출하려면
위처럼 작성해야 한다.
스프링 데이터 JPA를 사용하면 밑과 같이 메서드 이름만으로 Named 쿼리를 호출 할 수 있다.
"도메인 클래스 + . + 메서드 이름" 으로 Named 쿼리를 찾아서 실행한다. 따라서 위는 Member.findByUsername이라는 Named 쿼리를 실행한다. 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.(필요하면 전략을 변경할 수 있다.)
위에서 findByUsername() 메서드의 파리미터에 @Param을 사용했는데 이것을 이름기반 파라미터를 바인딩할 때 사용하는 어노테이션이다.
@Query, 리포지토리 메서드에 쿼리 정의
리포지토리에 직접 쿼리를 정의하려면 @org.springframework.data.jpa.repository.Query 어노테이션을 사용한다. 이 방법은 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다. 또한 JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견 할 수 있는 장점이 있다.
네이티브 SQL을 사용하려면 위와 같이 @Query 에서 nativeQuery = true를 설정한다. 참고로 스프링 데이터 JPA가 지원하는 파라미터 바인딩을 사용하면 JPQL은 위치 기반 파라미터를 1부터 시작하지만 네이티브 SQL은 0부터 시작한다.
파라미터 바인딩
스프링 데이터 JPA는 위치 기반 파라미터 바인딩, 이름 기반 파라미터 바인딩 모두 지원
기본값은 위치 기반인데 파라미터 순서로 바인딩한다.
벌크성 수정 쿼리
스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 org.springframework.data.jpa.repository.Modifying 어노테이션을 사용하면 된다.
벌크성 쿼리를 실행 후 영속성 컨텍스트를 초기화하고 싶으면 @Modifying(clearAutomatically = true)처럼 clearAutomatically 옵션을 true로 설정하면 된다. ( 기본값 false)
반환 타입
결과 한 건 이상이면 컬렉션 인터페이스, 단건이면 반환 타입을 지정한다.
조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환, 단건은 null을 반환한다. 단건을 기대하고 반환 타입을 지정했는데 결과가 2건 이상 조회되면 밑과 같은 예외 발생한다.
참고로 단건으로 지정한 메서드를 호출하면 JPA는 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출한다. 조회 결과가 없으면 위와 같은 예외가 발생하는데 상당히 불편하다. 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null을 반환한다.
페이징과 정렬
- org.springframework.data.domain.Sort : 정렬기능
- org.springframework.data.domain.Pageable : 페이징 기능
Pageable을 사용하면 반환 타입으로 List나 org.springframework.data.domain.Page를 사용할 수 있다. 반환 타입으로 Page를 사용하면 스프링 데이터 JPA는 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.
다음 조건으로 페이징과 정렬을 사용하는 예제 코드
- 검색 조건 : 이름이 김으로 시작하는 회원
- 정렬 조건 : 이름으로 내림차순
- 페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 10건
힌트
JPA 쿼리 힌트를 사용하려면 org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용하면 된다. 참고로 힌트는 SQL이 아닌 JPA구현체에게 제공하는 힌트다.
forCounting 속성은 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출 하는 페이징을 위한 count 쿼리에도 쿼리 힌트를 적용할지를 설정하는 옵션이다. (기본값 true)
Lock
5. 명세
명세 기능을 사용하려면 org.springframework.data.jpa.repository.JpaSpecificationExecutor 인터페이스를 상속받으면 된다.
위 인터페이스 안 JpaSpecificationExecutor 메서드들은 Specification을 파라미터로 받아서 검색 조건을 사용한다.
Specifications 는 명세들을 조립할 수 있도록 도와주는 클래스인데 where, and, or, not 메서드를 제공한다. findAll을 보면 회원 이름 명세(memberNmae)와 주문 상태 명세 (isOrderStatus)를 and로 조합해서 검색 조건으로 사용한다.
참고로 명세 기능을 사용할 때 위처럼 자바의 import static을 적용하면 더 읽기 쉬운 코드가 된다.
명세를 정의하려면 Specification 인터페이스를 구현하면 된다. 위에서는 편의상 내부 무명 클래스를 사용했다. 명세를 정의할 때는 toPredicate(...) 메서드만 구현하면 되는데 JPA Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스가 모두 파라미터로 주어진다. 이 파라미터들을 활용해 적절한 검색 조건을 반환하면 된다.
6. 사용자 정의 리포지토리 구현
다양한 이유로 메서드를 직접 구현해야 할 때도 있다. 그렇다고 리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 한다. 스프링 데이터 JPA는 이런 문제를 우회해서 필요한 메서드만 구현할 수 있는 방법을 제공한다.
클래스 이름을 짓는 규칙은 리포지토리 인터페이스 이름 + Impl로 지어야 한다.
마지막으로 밑과 같이 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다.
만약 다른 이름을 붙이고 싶으면 repository-impl-postfix 속성을 변경하면 된다.
JavaConfig 설정은 다음과 같다.
7. web 확장
설정
Web 확장 기능을 활성화하려면
을 스프링 빈으로 등록하면 된다.
JavaConfig를 사용하려면 다음 과 같이 org.springframework.data.web.config.EnableSpringWebSupport 어노테이션을 사용하면 된다.
설정을 완료하면 도메인 클래스 컨버터와 페이징과 정렬을 위한 HandlerMethodArgumentResolver가 스프링 빈으로 등록된다.
도메인 클래스 컨버터 기능
도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩해준다. ex) 특정 회원을 수정하는 화면을 보여주려면 컨트롤러는 HTTP 요청으로 넘어온 회원의 아이디를 사용해 리포지토리를 통해 회원 엔티티를 조회해야 한다. 다음과 같은 URL을 호출했다고 가정하자.
@RequestParam("id") Member member 부분을 보면 HTTP 요청으로 회원 아이디(id)를 받지만 도메인 클래스 컨버터가 중간에 동작해서 아이디를 회원 엔티티 객체로 변환해서 넘겨준다. 따라서 컨트롤러를 단순하게 사용할 수 있다. 참고로 도메인 클래스 컨버터는 해당 엔티티와 관련된 리포지토리를 사용해서 엔티티를 찾는다. 여기서는 회원 리포지토리를 통해서 회원 아이디로 회원 엔티티를 찾는다.
페이징과 정렬 기능
- 페이징 기능 :PageableHandlerMethodArgumentResolver
- 정렬 기능 : SortHandlerMethodArgumentResolver
Pageable은 다음 요청 파라미터 정보로 만들어진다.
- page : 현재 페이지 , 0부터 시작
- size : 한 페이지에 노출할 데이터 건수
- sort : 정렬 조건을 정의한다. ex) 정렬 속성(ASC | DESC), 정렬 방향을 변경하고 싶으면 sort 파라미터를 추가하면 된다.
- 접두사
사용해야 할 페이징 정보가 둘 이상이면 접두사를 사용해서 구분할 수 있다. @Qualifier 어노테이션을 사용한다. 그리고 "{접두사명}_"로 구분한다.
- 기본값
Pageable의 기본값은 page = 0, size = 20이다. 만약 기본값을 변경하고 싶으면 @PageableDefault 어노테이션을 사용하면 된다.
8. 스프링 데이터 JPA 가 사용하는 구현체
@Repository 적용 : JPA 예외를 스프링이 추상화한 예외로 변환한다.
@Transactional 트랜잭션 적용 : JPA의 모든 변경은 트랜잭션 안에서 이루어져야 한다.
@Transactional(readOnly = true) : 데이터를 조회하는 메서드에는 readOnly = true 옵션이 적용되어 있다. 데이터를 변경하지 않는 트랜잭션에서 이 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있다.
@save() 메서드 : 이 메서드는 저장할 엔티티가 새로운 엔티티명 저장하고 이미 있는 엔티티면 병합한다.
9. JPA 샵에 적용
- 환경설정
- 리포지토리 리팩토링
- 명세적용
- 기타
환경설정
pom.xml에 spring-data-jpa라이브러리를 추가하자
밑과 같이 src/main/resources/appConfig.xml에 <jpa :repositories> 를추가하고 base-package 속성에 리포지토리 위치를 지정하자.
이것으로 스프링 데이터 JPA 사용할 준비는 완료되었다.
리포지토리 리팩토링
- 회원 리포지토리 리팩토링
우선 클래스를 인터페이스로 변경하고 스프링 데이터 JPA가 제공하는 JpaRepository를 상속받았다. 이때 제네릭 타입을 <Member, Long>으로 지정해서 리포지토리가 관리하는 엔티티 타입과 엔티티의 식별자 타입을 정의했다.
코드에서 save, fineone, findall 메서드를 제거했다. 이런 기본 메서드는 JpaRepository가 모두 제공한다. findByName()은 스프링 JPA가 해당 메서드의 이름을 분석해서 메서드 이름으로 적절한 쿼리를 실행해 줄 것이다.
- 상품 리포지토리 리팩토링
- 주문 리포지토리 리팩토링
명세기능을 사용하기 위해 위의 주문 리포지토리를 리팩토링 한 후 코드에는 JpaSpecificationExecutor를 추가로 상속받았다.
명세 적용
명세로 검색하는 기능을 사용하려면 JpaSpecificationExecutor를 추가로 상속받아야 한다.
다음으로 검색조건을 가지고 있는 밑의 OrderSearch 객체에 자신이 가진 검색조건으로 Specification을 생성하도록 코드를 추가하자.
마지막으로 밑의 리포지토리의 검색 코드가 명세를 파라미터로 넘기도록 변경하자.
기존 리포지토리 코드들을 스프링 데이터 JPA를 사용하도록 리팩토링했다. 다음으로 스프링 데이터 JPA에 추가로 QueryDSL을 사용해보자.
10. 스프링 데이터 JPA와 QueryDSL 통합
- org.springframework.data.querydsl.QueryDslPredicateExecutor
- org.springframework.data.querydsl.QueryDslRepositorySupport
QueryDslPredicateExecutor 사용
첫 번 째 방법은 QueryDslPredicateExecutor를 상속 받으면 된다.
이제 상품 리포지토리에서 QueryDSL을 사용할 수 있다.
QueryDslPredicateExecutor 인터페이스를 보면 QueryDSL을 검색조건으로 사용하면서 스프링 데이터 JPA가 제공하는 페이징과 정렬 기능도 함께 사용할 수 있다.
하지만 QueryDslPredicateExecutor 기능에 한계가 있다. 예를 들어 join, fetch를 사용할 수 없다. 따라서 QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용, 스프링이 제공하는 QueryDslRepositorySupport를 사용해야 한다.
QueryDslRepositorySupport 사용
이걸 사용하면 QueryDSL을 좀 더 편하게 사용할 수 있다.
JPA가 제공하는 공통 인터페이스는 직접 구현할 수 없기 때문에 사용자 정의 리포지토리를 만들었다. 이제 QueryDslRepositorySupport를 사용하는 코드를 보자.
위는 주문 내역 검색 기능을 QueryDslRepositorySupport를 사용해서 QueryDSL로 구현한 예제다. 검색 조건에 따라 동적으로 쿼리를 생성한다. 참고로 생성자에서 QueryDslRepositorySupport에 엔티티 클래스 정보를 넘겨주어야 한다.
밑은 QueryDslRepositorySupport 핵심 기능이다.
'Spring > JPA' 카테고리의 다른 글
웹 애플리케이션 제작(애플리케이션 구현) (0) | 2023.07.17 |
---|---|
웹 애플리케이션 제작(도메인 모델과 테이블 설계) (0) | 2023.07.17 |
웹 애플리케이션 제작(프로젝트 환경설정) (0) | 2023.07.17 |
객체지향 쿼리 언어(4) 네이티브 SQL (0) | 2023.07.17 |
객체지향 쿼리 언어(3) QueryDSL (0) | 2023.07.17 |