3. Spring-Data-Jpa
프로젝트 환경설정
H2 데이터베이스 설치
라이브러리에 맞는 버전을 설치
db 파일로 접근
url: jdbc:h2:~/datajpa
이 방법은 직접적으로 파일에 접근했기 떄문에 lock이 걸려 한 번에 여러 곳에서 접근할 수 없음
원격으로 접근
url: jdbc:h2:tcp://localhost/~/datajpa
스프링 데이터 JPA와 DB 설정, 동작확인
application.yml 설정
기본 엔티티 생성 및 테스트 코드 작성
jpa vs spring-data-jpa 맛보기 코드비교
예제 도메인 모델
Member, Team 생성 및 연관관계 설정
생성자 레벨 Protected로 설정
ToString은 연관관계가 설정되어 있지 않은 필드만 출력
공통 인터페이스 기능
순수 JPA 기반 리포지토리 만들기
기본 CRUD
저장
변경 > 변경감지 사용
삭제
전체 조회
단건 조회
카운트
참고
JPA에서 수정은 변경감지 기능을 사용
트랜잭션 안에서 엔티티를 조회한 다음에 데이터를 변경하면, 트랜잭션 종료 시점에 변경감지 기능이 작동해서 변경된 엔티티를 감지하고 UPDATE SQL을 실행
확인사항
JPA 코드로 기본 CRUD 작성 및 테스트
지연로딩을 이용한 수정 테스트
스프링 데이터 JPA 공통 인터페이스 설정
스프링부트 사용시
@SpringBootApplication
위치를 지정(해당패키지와 하위 패키지 인식)만약 위치가 달라지면
@EnableJpaRepositories
필요org.springframework.data.repository.Repository
를 구현한 클래스는 스캔 대상MemberRepository
인터페이스가 동작한 이유실제 출력해보기(Proxy)
memberRepository.getClass()
classcom.sun.proxy.$ProxyXXX
@Repository
애노테이션 생략 가능컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리
JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리
스프링 데이터 JPA 공통 인터페이스 적용
위 JPA 코드로 작성한 내용을 Spring-data-jpa에서 제공하는
JpaRepository
를 사용하여 구현JpaRepository<T, ID>
T
: 엔티티 타입ID
: 식별자 타입(PK)
스프링 데이터 JPA 공통 인터페이스 분석
JpaRepository
data.jpa 패키지 하위에 존재하는 인터페이스
대부분의 공통 메서드를 제공
PagingAndSortingRepository
data 패키지 하위에 존재하는 인터페이스
수정 사항 확인
T findOne(ID)
->Optional<T> findById(ID)
변경
제네릭 타입 확인
T
: 엔티티ID
: 엔티티의 식별자 타입S
: 엔티티와 그 자식 타입
주요 메서드
쿼리 메서드 기능
여러 기능
메소드 이름으로 쿼리 생성
NamedQuery
@Query - 리파지토리 메소드에 쿼리 정의 파라미터 바인딩
반환 타입
페이징과 정렬
벌크성 수정 쿼리
@EntityGraph
쿼리 메소드 기능 3가지
메소드 이름으로 쿼리 생성
메소드 이름으로 JPA NamedQuery 호출
@Query 어노테이션을 사용해서 리파지토리 인터페이스에 쿼리 직접 정의
메소드 이름으로 쿼리 생성
메서드 이름을 분석하여 JPQL 쿼리를 생성하고 실행
주의
엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다.
그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.
JPA NamedQuery
@NamedQuery
어노테이션으로 Named 쿼리 정의JPA기반 쿼리 테스트
Spring Data JPA 기반 쿼리 테스트
@Query를 생략하고 메서드 이름만으로 Named쿼리를 호출할 수 있다.
스프링 데이터 JPA는 선언한
도메인 클래스 + .(점) + 메서드 이름
으로 Named 쿼리를 찾아서 실행만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
필요하면 전략을 변경할 수 있지만 권장하지 않는다.
주의
스프링 데이터 JPA를 사용하면 실무에서
NamedQuery
를 직접 등록해서 사용하는 일은 드물다.대신
@Query
를 사용해서 리파지토리 메소드에 쿼리를 직접 정의한다.
@Query, 리포지토리 메소드에 쿼리 정의하기
@org.springframework.data.jpa.repository.Query 어노테이션을 사용
실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있음
JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우 큰 장점!)
실무 환경
메서드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 지저분해진다.
따라서 @Query 기능을 더 선호한다.
단순 값 하나 조회
JPA 값 타입
@Embedded
도 이 방식으로 조회가 가능
DTO로 직접 조회
DTO로 직접 조회 하려면 JPA의 new 명령어를 사용
생성자가 맞는 DTO가 필요
파라미터 바인딩
코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용
컬렉션 파라미터 바인딩
Collection 타입으로 in절 지원
조회 결과 값에 따른 방식
컬렉션
결과 없음 -> 빈 컬렉션
단건 조회
결과 없음 -> null
결과 2건 이상:
javax.persistence.NonUniqueResultException
예외 발생
참고
단건
으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의Query.getSingleResult()
메서드를 호출한다.이 메서드를 호출했을 때 조회 결과가 없으면
javax.persistence.NoResultException
예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다.스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에
null
을 반환한다.
순수 JPA 페이징과 정렬
JPA 페이징
페이징에 대한 개념을 가지고 설계 해야하는 부담이 있다.
스프링 데이터 JPA
Page, Slice
와 같은 인터페이스를 제공페이징과 정렬 파라미터
Sort
: 정렬기능Pageable
: 페이징 기능 (내부에 Sort 포함)
특별한 반환 타입
Page
: 추가 count 쿼리 결과를 포함하는 페이징Slice
: 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로limit + 1
조회)List
: 추가 count쿼리 없이 결과만 반환
PageRequest
클래스를 활용하여 조건을 만들어Repository
로 전달하여 사용Jpa가 두 번째 파라미터로
Pageable
인터페이스를 제공Page
는 0부터 시작하기때문에 주의
페이징에 성능상 문제가 되는경우
@Query(value = "{...}", countQuery = "{...}")
를 사용하여 분리가 가능하다.페이징에는 Join이 필요없음에도 기본 쿼리가 Join을 하여 페이징에 부하가 있는 경우
복잡한 sql에서 사용, 데이터는 join으로 가져와야하나, count는 join이 필요없는 경우
페이징에서도 Entity는 API 로 바로 노출하면 문제가 발생하기 때문에 DTO로 변환하는 작업은 필수
정리
사용자의 요청 쿼리를 PageRequest로 만들어 JPA로 전달
기본은 Page를 사용, 더보기와 같은 버튼이 있는 페이지의 경우 Slice를 사용하도록한다.
Page를 사용하던 Slice를 사용하던 dto로 변환하는 것을 잊지 말 것
JPA의 페이징 인터페이스를 사용하여 domain 개발에 집중
벌크성 수정 쿼리
Jpa 기반 bulk 수정 쿼리
executeUpdate()
으로 호출영속성 컨텍스트를 초기화
EntityManager.clear()
스프링 데이터 JPA 기반 bulk 수정 쿼리
bulk
성 쿼리 설정@Modifying
을 사용하여 bulk 성 수정 및 삭제 쿼리를 실행하게 하는 어노테이션해당 어노테이션이 설정되어야
executeUpdate()
메서드로 실행이 됨해당 어노테이션이 없으면 예외를 발생시킴
@Modifying(clearAutomatically = true)
벌크성 쿼리를 실행한 뒤 영속성 컨텍스트 초기화를 하도록 한다.
bulk
성 쿼리의 주의사항bulk
연산은 영속성 컨텍스트를 무시하고 DB에 직접 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB 엔티티 상태가 달라질 수 있다.@Modifying
를 사용하지 않는 경우QueryExecutionRequestException
예외 발생또한 영속성 컨텍스트를 초기화하지 않는 경우 해당 데이터를 다시 조회하면 영속성 컨텍스트에 값이 남아 있어 문제가 될 수 있다.
조회가 필요한 경우 영속성 컨텍스트를 필히 초기화 해야한다.
bulk
연산 권장 방법영속성 컨텍스트에 엔티티가 없는 상태에서
bulk
연산을 먼저 실행영속성 컨텍스트에 엔티티가 있는 경우
bulk
연산 직후 영속성 컨텍스트를 초기화
@EntityGraph (
Fetch Join
의 간소화 버전)
연관된 엔티티들을 SQL 한 번에 조회하는 방법
Fetch Join과 같은 방식?
기존 방법
Hibernate5Module에 기반하여 Proxy객체를 초기화하여 값을 세팅
1:N 쿼리 조회 시 1을 먼저 조회 후 N에 대해서 지연로딩을 수행
가장 기본적인 방식 hibernate 모듈을 통한 지연로딩 대처 방법 (강의에 없는 내용)
지연로딩 여부를 확인
hibernate 메서드 사용
Hibernate.isInitialized(member.getTeam())
JPA 표준으로 확인하는 방법
em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(member.getTeam())
결국 엔티티를 한번에 조회하기 위해서는
Fetch Join
이 필요함
Fetch Join
으로 지연로딩에 대처하는방법@Query("select ... from ... join fetch ...")
연관된 엔티티를 한 번에 조회할 수 있도록
Fetch Join
을 사용하지만 이는 outer join으로 조회되기 때문에 중복 데이터가 존재
@EntityGraph
를 사용하여 지연로딩에 대처하는 방법Fetch Join
의Annotation
버전left outer join 실행
@NamedEntityGraph
설정을 통한 지연로딩 대처방법Entity 설정
@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
Repository 설정
@EntityGraph("Member.all")
정리
간단한 fetch join이 필요한 부분에서는
@EntityGraph
조금 복잡해지면 JPQL
fetch join
을 활용
JPA Hint & Lock (사용하려는 경우 깊은 이해가 필요)
database의 hint가 아님
데이터를 오직 조회하는 기능에 dirty check 기능을 비활성화 하도록 하는 설정
기본적으로 데이터를 저장하고 영속성 컨텍스트를 비운 뒤, 데이터를 새로 조회했을 때 db의 데이터를 조회 해 1차 캐시에 데이터를 저장하고 그 상태를 감지하게 되는데
단순히 조회만 하는 기능에서는 해당 작업이 비효율적이게 된다. 해당 기능을 통해 dirty checking을 하지 않도록하여 효율적으로 관리 하는 방법
반환 타입을 Page 인터페이스로 사용하는 경우에도 페이징을 위한 count 쿼리에도 힌트 적용 가능
확장 기능
사용자 정의 인터페이스 (실무에서 많이 씀)
기본 프로세스
스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성
Custom 인터페이스 구현 선택 사항
JPA 직접 사용( EntityManager )
스프링 JDBC Template 사용
MyBatis 사용
데이터베이스 커넥션 직접 사용 등등...
Querydsl 사용
실무에서는 주로 QueryDSL이나 SpringJdbcTemplate을 함께 사용할 때 사용자 정의 리포지토리 기능을 사용한다.
를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록하여 직접 사용해도 된다. (이때 스프링 데이터 JPA와는 관계 없이 별도로 동작한다.)
스프링 데이터 2.x 버전 사용방식
사용자 정의 구현 클래스에
리포지토리 인터페이스 이름 + Impl
을 적용하는 대신사용자 정의 인터페이스 명 + Impl
방식도 지원MemberRepository
Impl ->MemberRepositoryCustom
Impl으로도 적용 가능
정리
Custom으로 추가되는 기능들이 핵심 비즈니스인지 화면에 출력되는 내용인지 확실히 분류하여 클래스별로 설계할 수 있는 능력이 필요
Auditing
엔티티 생성, 변경의 작성자 및 시간을 관리하는 방식
JPA 기반 관리 방법
@MappedSuperclass
@PrePersist
,@PostPersist
@PreUpdate
,@PostUpdate
스프링 데이터 JPA 기반 관리 방법
@EnableJpaAuditing
스프링 부트 설정 클래스에 적용
@EntityListeners(AuditingEntityListener.class)
엔티티에 적용
@CreatedDate
엔티티 최초 등록일@LastModifiedDate
엔티티 최종 수정일@CreatedBy
엔티티 최초 등록자@LastModifiedBy
엔티티 최종 수정자
실무에서 사용하는 방법
기본적으로 Entity의 이력 정보를 확인하기 위해서는 무조건 필요한 필드
다만 등록일, 수정일은 어떤 엔티티든 필수인데 등록자, 수정자의 경우 필요하지 않은 경우가 존재하기 때문에 분리하여 사용
BaseEntity
,BaseTimeEntity
클래스로 구분하여 필요한 엔티티에서 사용JPA의 이벤트를 스프링 데이터가 제공해주기 때문에 도메인 코드를 줄일 수 있다.
@EntityListeners(AuditingEntityListener.class)
를 설정하지 않고 글로벌로 설정하는 방법META-INF/orm.xml
Web 확장 - 도메인 클래스 컨버터 (잘 안씀)
HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩
Http 기본 동작
view에서 controller로 넘어온 파라미터 값을 통해 repository에 파라미터 값을 넘겨 데이터를 조회
도메인 클래스 컨버트를 사용하는 경우 동작
파라미터로 엔티티를 받되 PathVariable로 설정된 파라미터를 해당 엔티티에서 가져와 엔티티의 Repository를 통해 데이터를 조회
Repository 로직을 skip할 수 있으나 트랜잭션이 없는 범위에서 엔티티를 조회 하므로 엔티티를 변경했을 때 DB에 반영이 되지 않는다.
두 방식의 차이점
HTTP 요청은 회원 id를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환
도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾음
주의사항
도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다.
(트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)
Web 확장 - 페이징과 정렬
페이징관련 인터페이스 & 클래스
Pageable
인터페이스PageRequest
실제 사용 객체
페이징 사용법
```http request /members?page=0&size=3&sort=id,desc&sort=username,desc
```
page: 현재 페이지,
0
부터 시작한다.size: 한 페이지에 노출할 데이터 건수
sort: 정렬 조건을 정의한다.
예) 정렬 속성, 정렬 속성...(ASC | DESC), 정렬 방향을 변경하고 싶으면 sort 파라미터 추가 ( asc 생략 가능)
페이징 설정
글로벌 설정
개별 설정
@PageableDefault
어노테이션을 사용
페이징 정보가 둘 이상인 경우 설정 방법
페이징 정보가 둘 이상이면 접두사로 구분
@Qualifier
에 접두사명 추가 "{접두사명}_xxx”요청 쿼리스트링
```http request /members?member_page=0&order_page=1
```
컨트롤러 파라미터 설정
Paging 커스텀
Page의 기본 값이 0인 것을 1부터 시작하는 방법
Pageable
,Page
를 파리미터와 응답 값으로 사용히자 않고, 직접 클래스를 만들어서 처리spring.data.web.pageable.one-indexed-parameters 를 true 로 설정
이 방법은 web에서 page 파라미터를 -1 처리 할 뿐, 응답 값
Page
에는 0으로 되어 있음반환하는 pageable 객체의 응답값이 의도한 값으로 반환되지 않음
페이징 결과값 반환
엔티티를 API로 노출하면 다양한 문제가 발생
엔티티를 꼭 DTO로 변환해서 반환해야 한다.
Page는 map() 을 지원해서 내부 데이터를 다른 것으로 변경할 수 있다.
스프링 데이터 JPA 분석
기본 개념
@Repository
JPA 예외를 스프링이 추상화한 예외로 변환
@Transactional
JPA의
모든 변경
은트랜잭션 안
에서 동작스프링 데이터 JPA는
변경(등록, 수정, 삭제)
메서드를트랜잭션 처리
서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작 -> 기본적으로
Repository
에@Transactional
이 걸려있음서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했음
(사실은 트랜 잭션이 리포지토리 계층에 걸려있는 것임)
@Transactional(readOnly = true)
데이터를 단순히 조회만하고 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러 시를 생략해서 약간의 성능 향상을 얻을 수 있음
JPA를 사용하여 데이터를 저장할 때 주의할 점
persist를 사용해야함
merge는 db에 데이터가 존재하는 지 select를 한번 조회하기 때문에 비효율적임
임의의 Id를 사용하려는 경우
기본적으로
식별자(@Id)
가객체
인경우null
로 판단,기본 타입
인 경우0
으로 판단임의로 식별자를 채번하여 임의로 생성해야 하는 경우 ->
Persistable
인터페이스를 사용하여 엔티티가 신규인지 여부를 판단실무에서는 사용하는 방법
생성일자(createDate)가 존재하는 경우를 기준으로 신규 데이터인지 여부를 판단
Persistable
JPA 식별자 생성 전략이 @GenerateValue 면 save() 호출 시점에 식별자가 없으므로 새로운 엔티 티로 인식해서 정상 동작
JPA 식별자 생성 전략이 @Id 만 사용해서 직접 할당이면 이미 식별자 값이 있는 상태로 save() 를 호출
이 경우
merge()
가 호출된다.merge()
는 우선 DB를 호 출해서 값을 확인하고, DB에 값이 없으면 새로운 엔티티로 인지하므로 매우 비효율적임Persistable
를 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 효과적이다.참고
등록시간(
@CreatedDate
)을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다.(
@CreatedDate
에 값이 없으면 새로운 엔티티로 판단)
나머지 기능들
Specifications (명세) - 유지보수가 힘든 코드
실무에서는 JPA Criteria를 거의 안쓴다! 대신에 QueryDSL을 사용하자.
스프링 데이터 JPA는 JPA Criteria를 활용하여 해당 개념을 사용할 수 있도록
Specification
클래스를 지원동적 쿼리를 대처하기 위한 방법으로 사용
술어(predicate)
참 또는 거짓으로 평가
AND OR 같은 연산자로 조합해서 다양한 검색조건을 쉽게 생성(컴포지트 패턴)
예) 검색 조건 하나하나
스프링 데이터 JPA는
org.springframework.data.jpa.domain.Specification
클래스로 정의
명시 기능 사용방법
Repository
에JpaSpecificationExecutor<Member>
상속조회 쿼리 메서드에
Specification
파라미터로 받아 검색 조건으로 사용검색 조건에 대한 Spec을 미리 정의 해두어야 함
사용 방식
Probe: 필드에 데이터가 있는 실제 도메인 객체
ExampleMatcher: 특정 필드를 일치시키는 상세한 정보 제공, 재사용 가능
Example:
Probe
와ExampleMatcher
로 구성, 쿼리를 생성하는데 사용
장점
동적 쿼리를 편하게 처리하는 방법
도메인 객체를 그대로 사용하는 것이 큰 장점
데이터 저장소를 RDB에서 NOSQL로 변경해도 코드 변경이 없게 추상화 되어 있음 (spring data 패키지 내에 존재하기 때문)
스프링 데이터 JPA
JpaRepository
인터페이스에 이미 포함
단점
조인은 가능하나 내부 조인만 가능 ( outer join에서 제약 )
중첩 제약조건 쿼리에 제한
매칭 조건이 단순한 것만 가능 (정확하게 매칭되는 것만 가능)
정리
좋은 기술처럼 보이지만 쿼리의 중요한 기능 들을 제공하지 않아 사용을 해야하는 선택을 해야할지에 대해서 생각해야 한다.
복잡한 쿼리를 해결하기에는 한계가 있다. 실무에서는 단순할 때만 사용하고, 조금만 복잡해지면 QueryDSL을 사용하자
사용 용도
엔티티 대신에 DTO를 편리하게 조회할 때 사용
전체 엔티티가 아니라 만약 회원 이름만 딱 조회하고 싶은경우
주의사항
프로젝션 대상이 root 엔티티인 경우, JPQL SELECT 절 최적화가 가능하다.
프로젝션 대상이 root가 아닌경우
left outer join 처리하기 때문에 모든 필드를 select 해서 엔티티로 조회한 다음에 계산
최적화가 하기 힘듬
정리
프로젝션 대상이 ROOT 엔티티인 경우 유용
프로젝션 대상이 ROOT 엔티티를 넘어가면 JPQL SELECT 최적화기 되지 않는다.
실무의 복잡한 쿼리를 해결하기에 한계가 있다.
Native Query
최후의 수단으로 사용하는 수단중 하나
Native Query 또는 Projections 이라고는 하나 QueryDSL로 못할 수가 없다.
한번에 못가져올거면 차라리 두 세번에 걸쳐서 가져올것..
스프링 데이터 JPA Native Query
페이징 지원
반환 타입
Object[]
Tuple
DTO (스프링 데이터 인터페이스 Projections 지원)
제약 사항
sort 파라미터를 통한 정렬이 정상 동작하지 않을 수도 있다.
JPQL처럼 애플리케이션 로딩 시점에 문법 확인 불가
동적 쿼리 불가
위치기반 파라미터
JPQL의 경우 1부터 시작, Native Query의 경우 0부터 시작
DTO 변환 시
DTO 대신 JPA TUPLE 조회
DTO 대신 MAP 조회
@SqlResultSetMapping 복잡
Hibernate ResultTransformer를 사용해야함 복잡
네이티브 SQL을 DTO로 조회할 때는
JdbcTemplate
orMyBatis
권장
참고
Last updated