본문으로 바로가기

[Spring] 실전! 스프링 데이터 JPA - 2

category Spring 2021. 9. 3. 16:35

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84

 

실전! 스프링 데이터 JPA - 인프런 | 강의

스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다

www.inflearn.com

 

※ 예제 도메인 모델

ㅁ 예제 도메인 모델과 동작확인

- 엔티티 기본생성자 만들기를 lombok으로

 > @NoArgsConstructor(access = AccessLevel.PROTECTED)

- ToString 만들기

 > @ToString(of={"id","username","age"})

 

※ 공통 인터페이스 기능

ㅁ 순수 JPA 기반 리포지토리 만들기

- Optional return하기 위해 아래 ofNullable 사용

 > return Optional.ofNullable(team);

- count 반환 ( getSingleResult 사용 )

 > public long count(){
   return em.createQuery("select count(t) from Team t", Long.class).getSingleResult();
    }

 

ㅁ 공통 인터페이스 설정 & 공통 인터페이스 적용

- 원래는 @EnableJpaRepositories(basePackages = "study.datajpa.repository") 가 application에 설정되어있어야,

 해당 패키지 아래를 JPA repository로 등록해준다.

 > 부트를 쓸경우 없어도 됨 (자동으로 main application 아래 모든 패키지 scan하여 설정됨)

- 구현체가 없는데 interface가 JPArepository 기능을 할까?

 > 스프링이 JPA repository 를 상속받은 인터페이스를 보고 구현 클래스를 만들어서 넣어줌!!

 > 프록시 객체로 만들어 짐 (JDK dynamic 프록시 사용하는듯 - sun 어쩌구로 찍힘)

 

ㅁ 공통 인터페이스 분석

- PagingAndSortingRepository 는 org.springframework.data.repository에 있음

  > spring-data-commons 에 있음 (공통적으로 사용)

- JpaRepository는 org.springframework.data.jpa 에 있음

  > spring-data-jpa 에 있음

- getOne(ID) : 엔티티를 프록시로 조회 => EntityMangager.getReference() 호출

- findAll은 Sort와 Paging 조건을 제공할 수 있음

- JpaRepository는 왠만한 모든 기능을 전부 제공한다. (더 생각하기 힘들정도..?)

 

 

※ 쿼리 메소드 기능

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

- 메소드 이름으로 쿼리 만들수 있는데, 두가지 조건 이상이면 이름이 너무 길어져서 다른방법으로 풀어냄

 > find 후 by 가 없으면 전체 조회

- findHelloBy 처럼 식별하기 위한 내용(설명) 이 들어가도 됨

 

ㅁ JPA named Query

- 엔티티 위에 설정 후, repository 에서 em.createNamedQuery로 호출

@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username")

 

- JPA respository 에서는

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

로 호출

 @PARAM 잘 기억해야함

- 순서 : 설정된 엔티티의 네임드쿼리 찾음 -> 없으면 메소드이름으로 쿼리생성 진행

- 쿼리가 엔티티에 있는것이 조금 이상하고, repository에서 query 설정할 수 있기때문에, namedquery는 잘 안씀

- 네임드 쿼리의 가장 큰 장점은 애플리케이션 로딩 시점에 파싱해서 문법오류를 알려줌

 

ㅁ @Query, 리포지토리 메소드에 쿼리 정의하기

- 정말 막강한 기능이다.

- 아래 형식으로 구현

@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 에 쓰는 이름이 없는 namedQuery 로 생각하면 됨

 > 그래서 namedQuery와 마찬가지로 오타나 오류가 있을 경우 미리 파싱해서 알려줌

 

ㅁ @Query, 값, DTO 조회하기

- 특정 칼럼만 가져오기

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

 

- DTO는 @DATA 써도 되는데 엔티티는 Setter를 두지 않는걸 권장하기 때문에 @DATA 쓰면 안됨

- new operation으로 DTO로 바로 조회 가능

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

 

ㅁ 파라미터 바인딩

- 위치 기반과 이름 기반이 있는데, 위치기반은 위치가 바뀔경우 에러가 발생할 수 있어서 이름기반으로 해야함

- in 절에 사용할 수도 있음  - Collection<타입> 으로 변수 설정하는게 좋음

@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") Collection<String> names);

 

ㅁ 반환 타입

- 반환타입이 collection , List 일때 값이 없을 경우, null 이 아닌 empty collection 이 반환됨

 > size로 체크하면 됨

- 단건 조회일때는 없으면 null 이 돰

- jpa는 조회해서 값이 없을 경우, nullexception이 터지는데, spring data jpa는 try catch가 감싸고 있어서, null 이 넘어오게됨

 > 자바 8 이후에는 optional로 조회하게 되어, 개발자가 책임지게 됨

- 단건 조회에 2건이상 값이 뜨면, exception 이 발생하게 됨 (non unique result exception이 발생 -> 스프링 데이터 jpa가 스프링 프레임워크 exception으로 변환함 

 > jpa 예외에 의존하는게 아니라 스프링 프레임워크에 의존하기 위함

 

ㅁ 순수 JPA 페이징과 정렬

-  현재 페이지 구하는게 사실 제일 힘들지만( 페이지 관련 연산.. 첫페이지냐(offset), 마지막 페이지냐 등 ), 스프링 데이터는 제공해준다

- JPA 에서는 setFirstResult와 setMaxResult로 설정함

- totalCount 쿼리도 필요

 

ㅁ 스프링 데이터 JPA 페이징과 정렬

- pageRequest를 만들어서 정렬방법 및 갯수 설정 (PageRequest는 Pageable 인터페이스 상속받음)

 > 이것을 Pageable 인터페이스 파라미터로 받아서 Page<Entity>를 return으로 받음

 > page.getContent(); 각 값 내용들

 > page.getTotalElements(); totalCount

 > page.getNumber(): 페이지 번호

 > pagegetTotalPages()) 페이지 전체 갯수

> page.isFirst() : 첫째 페이지

> page.hasNext() : 다음페이지가 있냐

 

- Page 인터페이스는 Slice 인터페이스 상속

 > Slice가 Page의 위에 있음

 > Slice는 다음 객체가 있을때, 더보기 를 만든다던지... 요청한 갯수보다 하나 더 가지고 옴

 > totalcount 쿼리를 날리지않음

- totalCount 쿼리가 데이터 전체를 세기 때문에, 소요시간이 많이 걸릴 수 있다.

 > 페이징은 시간 단축을 위한 건데, 오히려 더 부하가 걸림 (카운트를 세기 위해  left inner join을 또하는 등..)

 > @Quert(countQuery = "" ) 를 작성하여 해결

ex) 

@Query(value = "select m from Member m left join m.team t", countQuery = "select count(m) from Member m")
Page<Member> findByAge(int age, Pageable pageable);

 

- Paging과 pagable 등의 등장으로 개발자는 totalcount 나 페이지연산로직 관련없이 실제 핵심로직만 집중하면 됨

 > sorting 등 복잡할 경우 @Query에 sort by 하면 됨

 

- Page<Entity> 를 api에서 그대로 반환하면 안되고 DTO로 변환해야함

 > Page<MemberDto> toMap = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));

 로 map을 통해 바로 변환 가능

 

ㅁ 벌크성 수정 쿼리

- command option n  : inline 단축하기

- jpa 쿼리로 update 할때는 , .executeUpdate(); 로 실행 (반환 타입이 실행된 행 갯수)

- Spring Data JPA는 @Query로 동일하게 update 쿼리를 만들고 @Modifying 을 꼭 넣어야함

 > 빼면 에러 발생  (Not Supprot DML ~~~)

- 벌크 연산은 JPA의 영속성 컨텍스트의 1차캐시 객체에 영향을 주지않음

 > 그렇지 않으면 영속성 컨텍스트의 값과 db값이 다름

 > 벌크 연산 후 영속성 컨텍스트를 날려줘야함 (em.flush(), em.clear() 필요)

 > 그렇지 않으면 @Modifying에 옵션 추가 => @Modifying(clearAutomatically = true)

 

ㅁ @EntityGraph

- fetch join을  명시하지 않고, 내부적으로 fetch join을 사용하여 프록시 객체를 만들지 않고 가져오게 할 수 있음

- 사용 예 ) 1. overrride 하기 2.jpql에 fetch join 명시하지않고 사용

   @Override
    @EntityGraph(attributePaths = {"team"})
    List<Member> findAll();

    @EntityGraph(attributePaths = {"team"})
    @Query("select m from Member m")
    List<Member> findMemberEntityGraph();

- @NamedEntityGraph(name = "Member.all",attributeNodes = @NamedAttributeNode("team"))

를 엔티티 위에 넣어서 named query 처럼

@EntityGraph("Member.all")
List<Member> findEntityGraphByUsername(@Param("username") String username);

 로 호출할 수 있음

- 실무에서는 간단한거는 @EntityGraph 로 해결하고, 어려운거는 jpql의 fetch join 작성함

 > 어려운거는 보통 무조건 쿼리를 작성해야할 수준이기 때문에..

 

ㅁ JPA Hint & Lock

- 단순 조회만을 위한거는 read only만 해야 성능 최적화에 도움이 됨

 > readonly 설정은 JPA는 제공해주지않고 , Hibernate에 구현되어 있음

@QueryHints(value = @QueryHint(name="org.hibernate.readOnly", value="true"))
Member findReadOnlyByUsername(String username);

- 그러나 실제 성능 최적화에는 효과가 없을 것... 쿼리 자체가 문제인게 많음

- @Lock을 통해 특정 리포지토리 쿼리 진행시 lock을 걸 수 있음

 > @Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String username);

 

 > select for update 구문 생성 확인

 > SELECT ~ FOR UPDATE란

SELECT ~ FOR UPDATE 구문은 "데이터 수정하려고 SELECT 하는 중이야~ 다른 사람들은 데이터에 손 대지 마!" 라고 할 수 있습니다. 좀 더 딱딱한 표현으로는 동시성 제어를 위하여 특정 데이터(ROW)에 대해 베타적 LOCK을 거는 기능입니다.