티스토리 뷰

728x90

springf

스프링 데이터 JPA(inflearn) - 백기선

스프링 데이터 Common

스프링 데이터 JPA 활용 파트 소개

springjpa

  • 스프링 데이터 : 하나의 프로젝트를 말하는것이 아니고, 여러개의 프로젝트들의 묶음을 말하는것.
    (SQL & NoSQL 저장소 지원 프로젝트의 묶음.)
  • 스프링 데이터 Common : 여러 저장소 지원 프로젝트의 공통 기능 제공.
  • 스프링 데이터 REST : 저장소의 데이터를 하이퍼미디어 기반 HTTP 리소스로(REST API로) 제공하는 프로젝트.
  • 스프링 데이터 JPA : 스프링 데이터 Common이 제공하는 기능에 JPA 관련 기능 추가
  • 참고문서

Repository

  • 앞서 JpaRepository를 상속하는 클래스를 만들어 사용한적이 있었는데.. 사실 해당 부분은 JpaRepository가 스프링데이터 Common의 PagingAndSortingRepostiory를 상속받고 있다. 이미지
    • PagingAndSortingRepostiory : paging, sorting 메소드를 지원
    • CrudRepository : CRUD 메소드를 지원
      • @NoRepositoryBean가 붙어있는데.. 실제 빈을 만들어 등록하지 않도록한다.
        (중간단계의 Repository에는 붙어있다. 실제 Repository가 아님을 표현한다.)
    • Repository : marker 인터페이스, 실질적인 기능을 하진 않는다.

CrudRepository 메소드

  • save(entity);
    • 하나의 엔티티를 저장하고 그 엔티티를 리턴해준다.
  • saveall(Iterable<> entities);
    • 여러개의 엔티티를 저장하고 리턴(Iterable타입으로..)
  • findById(id);
    • id로 엔티티를 찾아서 리턴한다.
    • Optional<>로 리턴된다.(null방지..)
  • existById(id);
    • 해당 id에 해당하는 엔티티의 유무를 확인한다.
  • findAll();
    • 어떤 한 엔티티의 모든 정보를 불러온다.
    • 거의 테스트용으로 사용된다.
    • Iterable로 리턴한다. (List<>로 리턴하는경우는 스프링 데이터 JPA를 사용하는것)
  • findAllById(Iterable<> ids);
    • 여러id에 해당하는 목록을 가져온다.
  • count();
    • 갯수
  • delete(id);
    • 삭제
    • deleteAll() 도 있다.
test
  • 기본적으로 데이터 테스트들은 @Transactional을 가지고 있는데.. 기본적으로 롤백을 한다.

    • 어자피 롤백할 쿼리이기 때문에 test클래스 실행 시, insert쿼리문은 날라가지 않는다.
    • 보고 싶다면 @Rollback(false)를 추가하자.
  • CRUD
    springjpa

  • PagingAndSorting
    springjpa

Repository 인터페이스 정의하기

Repository 인터페이스로 공개할 메소드를 직접 일일히 정의하고 싶을 때

  • 특정 레퍼지토리 정의
    • @RepositoryDefinition
      springjpa
    • test(정의한 Repository인터페이스는 모두 테스트를 해주어야한다.)
      springjpa
  • 공통 인터페이스 정의

Null 처리하기

Optional

  • 자바8 부터 지원
  • Null을 리턴하지 않고, 비어있는 콜렉션을 리턴한다.
  • 예시
    springjpa
  • Optional 메소드 예시
    springjpa

Null 어노테이션

  • 스프링 프레임워크 5.0부터 지원하는 Null 애노테이션 지원.
    • @NonNullApi springjpa
      • package-info.java파일 생성 후, 어노테이션 사용
      • 패키지안의 모든 파라미터 및 리턴타입 및 모두 Null이 아니어야한다.
      • Null이 필요한곳에 @Nullable을 사용 해야한다.
    • @NonNull springjpa
      • Null이 아니길 바랄때 사용한다.
    • @Nullable.
  • 툴의 도움을 받을 수 있다. springjpaspringjpa
    • NonNullApi는 불가능

쿼리 만들기 개요

스프링 데이터 저장소의 메소드 이름으로 쿼리 만드는 방법

  • 메소드 이름을 분석해서 쿼리 만들기 (CREATE)
    springjpa
  • 미리 정의해 둔 쿼리 찾아 사용하기 (USE_DECLARED_QUERY)
    • 우선순위 Query > Procedure > NamedQuery
    • 기본적으로 JPQL을 써야하나, SQL을 쓰고 싶다면 nativequery를 true로 변경한다.
      springjpa
  • 미리 정의한 쿼리 찾아보고 없으면 만들기 (CREATE_IF_NOT_FOUND) (기본전략)

쿼리 만드는 방법

  • 리턴타입 {접두어}{도입부}By{프로퍼티 표현식}(조건식)[(And|Or){프로퍼티표현식}(조건식)]{정렬 조건} (매개변수)
    • 접두어 : Find, Get, Query, Count, …
    • 도입부 : Distinct, First(N), Top(N)
    • 프로퍼티 표현식 : Person.Address.ZipCode => find(Person)ByAddress_ZipCode(…)
    • 조건식 : IgnoreCase, Between, LessThan, GreaterThan, Like, Contains, …
    • 정렬 조건 : OrderBy{프로퍼티}Asc|Desc
    • 리턴 타입 : E, Optional, List, Page, Slice, Stream
    • 매개변수 : Pageable, Sort

쿼리 만들기 실습

기본예제

List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
distinct
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
ignoring case
List<Person> findByLastnameIgnoreCase(String lastname);
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

정렬

List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);

페이징

Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);

스트리밍

  • try-with-resource 사용할 것. (Stream을 다 쓴다음에 close() 해야 함)
Stream<User> readAllByFirstnameNotNull();

그 외 실습내용

Repository class
test class


비동기 쿼리

Future

@Async
Future<User> findByFirstname(String firstname);

CompletableFuture

@Async 
CompletableFuture<User> findOneByFirstname(String firstname);

ListenableFuture

@Async 
ListenableFuture<User> findOneByLastname(String lastname);
  • 스프링에서 만들었다.
  • 가장 깔끔함.
  • 해당 메소드를 스프링 TaskExecutor에 전달해서 별도의 쓰레드에서 실행함.
  • Reactive랑은 다른 것임

권장하지 않는 이유

  • 테스트 코드 작성이 어려움.
  • 코드 복잡도 증가.
  • 성능상 이득이 없음.
    • DB 부하는 결국 같고.
    • 메인 쓰레드 대신 백드라운드 쓰레드가 일하는 정도의 차이.
    • 단, 백그라운드로 실행하고 결과를 받을 필요가 없는 작업이라면 @Async를 사용해서 응답 속도를 향상 시킬 수는 있다.


커스텀 리포지토리

  • 쿼리 메소드(쿼리 생성과 쿼리 찾아쓰기)로 해결이 되지 않는 경우 직접 코딩으로 구현 가능.
    • 스프링 데이터 리포지토리 인터페이스에 기능 추가.
    • 스프링 데이터 리포지토리 기본 기능 덮어쓰기 가능.

구현방법

  1. 커스텀 리포지토리 인터페이스 정의
    springjpa
  2. 인터페이스 구현 클래스 만들기 (기본 접미어는 Impl) springjpa
    • EntityManager, JdbcTemplate 등을 주입받아 사용할 수 있다.
  3. 엔티티 리포지토리에 커스텀 리포지토리 인터페이스 추가
    springjpa
  • 기본 기능을 덮어쓰고 싶다면 기본기능 메소드를 그대로 정의하고 위와 같은 방법으로 구현한다.
    springjpa

접미어 설정하기

기본 리포지토리 커스터마이징

  • 모든 리포지토리에 공통적으로 추가하고 싶은 기능이 있거나 덮어쓰고 싶은 기본 기능이 있을때

구현방법

  1. JpaRepository를 상속 받는 인터페이스 정의 springjpa
  2. 기본 구현체를 상속 받는 커스텀 구현체 만들기
    springjpa
  3. @EnableJpaRepositories에 설정
    • repositoryBaseClass
      springjpa
  4. 엔티티 레포지토리에 설정
    springjpa

도메인 이벤트

도메인 관련 이벤트를 발생시키기에 대하여 스프링 프레임워크 사용방법 및 스프링 데이터 사용방법에 대해 알아보자.

스프링 프레임워크의 이벤트 관련 기능

  • 참고문서
  • 이벤트를 만들때는 ApplicationEvent를 상속받도록한다. (extends ApplicationEvent)
    springjpa
  • ApplicationContext는 BeanFactory와 더불어 ApplicationEventPublisher도 상속받고 있다. (이벤트 등록 기능 지원)
    springjpa
  • 리스너 (하기 두가지 방법중 선택하여 사용한다.)

스프링 데이터의 도메인 이벤트 Publisher

  • extends AbstractAggregateRoot springjpa
  • 현재는 save() 할 때만 발생 합니다.
    springjpa

QueryDSL(Domain Specific Language)

QueryDSL을 사용하는 이유

  • 조건문을 표현하는 방법이 타입세이프하다.
  • 조건문을 Predicate 인터페이스로 조건문을 표현하는데, 조합 및 별도 관리가 가능하다.

여러 쿼리 메소드는 대부분 두 가지 중 하나

  • Optional findOne(Predicate): 이런 저런 조건으로 무언가 하나를 찾는다.
  • List|Page|.. findAll(Predicate): 이런 저런 조건으로 무언가 여러개를 찾는다.
  • QuerydslPredicateExecutor 인터페이스

QueryDSL

연동 방법

  • 기본 리포지토리 커스터마이징 안 했을 때.

    • 의존성추가(스프링 부트가 버전관리 해줌)

      • querydsl-apt : 쿼리용 Domain Specific Laguage를 생성해준다. 플러그인 설정이 필요하다.
      • queydsl-jpa
      <dependency>
          <groupId>com.querydsl</groupId>
          <artifactId>querydsl-apt</artifactId>
      </dependency>
      
      <dependency>
          <groupId>com.querydsl</groupId>
          <artifactId>querydsl-jpa</artifactId>
      </dependency>
    • 플러그인 추가

    <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <executions>
            <execution>
                <goals>
                    <goal>process</goal>
                </goals>
                <configuration>
                    <outputDirectory>target/generated-sources/java</outputDirectory>
                    <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                </configuration>
            </execution>
        </executions>
    </plugin>
    • compile 시, 해당 파일이 생성된다.(QAccount)
      springjpa

    • Repostiory 인터페이스에 QuerydslPredicateExecutor<도메인타입> 상속을 추가한다.
      springjpa

    • predicate를 사용하여 조건문을 만들고 사용한다.
      springjpa

  • 기본 리포지토리 커스타마이징 했을 때.

    • 기존에는 만든 커스터마이징 구현체에 SimpleJpaRepository를 상속하지 않도록하고 QuerydslJpaRepository를 상속하도록 함으로 해결 할 수 있었다.
    • 현재는 상기 방법가 동일하게 해도 구현되는것을 확인하였다.


DomainClassConverter

스프링 Converter

DomainClassConverter

  • 사용 전

    @GetMapping("/posts/{id}")
        public String getAPost(@PathVariable Long id) {
            Optional<Post> byId = postRepository.findById(id);
            Post post = byId.get();
            return post.getTitle();
        }
  • 사용

    • 해당아이디 값으 찾아서 Domain객체로 변환해준다.
      @GetMapping("/posts/{id}") public String getAPost(@PathVariable(“id”) Post post) { return post.getTitle(); }
  • Formatter

    • 문자열 기반
    • 문자열 -> 다른타입
    • 어떤타입 -> 문자열 프린팅


Pageable과 Sort 매개변수

스프링 MVC HandlerMethodArgumentResolver

  • 스프링 MVC 핸들러 메소드의 매개변수로 받을 수 있는 객체를 확장하고 싶을 때 사용하는 인터페이스
  • 참고문서

페이징과 정렬 관련 매개변수

  • page: 0부터 시작.
  • size: 기본값 20.
  • sort: property,property(,ASC|DESC)
  • 예) sort=created,desc&sort=title (asc가 기본값)

예제

  • 컨트롤러
    @GetMapping("/posts") public Page<Post> getPost(Pageable pageable){ return postRepository.findAll(pageable); }

  • test

     @Test
     public void getPosts() throws Exception {
         Post post = new Post();
         post.setTitle("jpa");
         postRepository.save(post);
     mockMvc.perform(get("/posts/")
             .param("page", "0")
             .param("size", "10")
             .param("sort", "created,desc")
             .param("sort", "title"))
             .andDo(print())
             .andExpect(status().isOk())
     .andExpect(jsonPath("$.content[0].title", is("jpa")));
    }



HATEOAS

Page를 PagedResource로 변환하기

  • 일단 HATEOAS 의존성 추가 (starter-hateoas)
  • 핸들러 매개변수로 PagedResourcesAssembler

예제

  • 컨트롤러
    @GetMapping("/posts") public PagedResources<Resource<Post>> getPosts (Pageable pageable, PagedResourcesAssembler<Post> assembler){ return assembler.toResource(postRepository.findAll(pageable)); }

  • test

     @Test
     public void getPosts() throws Exception {
         createPost("jpa");
    
     mockMvc.perform(get("/posts/")
             .param("page", "0")
             .param("size", "10")
             .param("sort", "created,desc")
             .param("sort", "title"))
             .andDo(print())
             .andExpect(status().isOk())
     .andExpect(jsonPath("$.content[0].title", is("jpa")));
    } private void createPost(String title) { int postcount = 0;
     while(postcount &lt;= 100){
         Post post = new Post();
         post.setTitle(title);
         postRepository.save(post);
         postcount++;
     }</code></pre></li>
  • 결과
    "_embedded":{ "postList":[ { "id":140, "title":"jpa", "created":null }, ... { "id":109, "title":"jpa", "created":null } ] }, "_links":{ "first":{ "href":"http://localhost/posts?page=0&size=10&sort=created,desc&sort=title,asc" }, "prev":{ "href":"http://localhost/posts?page=1&size=10&sort=created,desc&sort=title,asc" }, "self":{ "href":"http://localhost/posts?page=2&size=10&sort=created,desc&sort=title,asc" }, "next":{ "href":"http://localhost/posts?page=3&size=10&sort=created,desc&sort=title,asc" }, "last":{ "href":"http://localhost/posts?page=19&size=10&sort=created,desc&sort=title,asc" } }, "page":{ "size":10, "totalElements":100, "totalPages":20, "number":2 } }



728x90
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday