본문 바로가기

Web Sever 개발과 CS 기초/스프링

Spring Data JPA 이해와 사용법

관련 내용

<Spring Data JPA와 Querydsl 완전히 이해하기>

  • (현재글)SpringDataJPA 이해와 사용법

Spring Data JPA 이해와 사용법

  • Querydsl 이해와 사용법

Querydsl 이해와 사용법

  • Querydsl JOIN((INNER, LEFT, RIGHT, THETA, FETCH) 사용법

Querydsl 의 JOIN (INNER, LEFT, RIGHT, THETA, FETCH) 사용법

개요 목적

이번 시간에는 JPA를 편리하기 사용할 수 있는 Spring Data JPA 사용법에 대해서 알아본다.

먼저 Spring Data JPA 인터페이스의 구성과 역할에 대해 먼저 이해를 하고,

Spring Data JPA 다양한 편리 기능을 코드를 통해서 알아본다.

Spring Data JPA 인터페이스 이해하기

KIMHWANG\data-jpa\src\main\java\study\datajpa\repository\MemberJpaRepository.java 에는,

JPA의 entityManger를 사용하여, save, delete,findAll, findById, count 메소드를 만들었다.

이런 공통으로 많이 사용하는 CRUD 직접 작성하지 않고, Spring Data JPA 인터페이스에서 제공하는 메소드로 대신할 수 있다.

//
public interface MemberRepository extends JpaRepository<Member, Long> {
}
//Spring Data JPA 인터페이스 작동 테스트
@SpringBootTest
public class MemberRepositoryTest {
    @Autowired
    MemberRepository memberRepository;
    @Test
    public void testMember() {
        Member member = new Member("memberA");
        Member savedMember = memberRepository.save(member);
        Member findMember =
            memberRepository.findById(savedMember.getId()).get();
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
		}
}

메소드를 가진 실제 구현체가 없는데도, 작동할 수 있는 이유는, Spring Data JPA가 JpaRepository<Member, Long>를 확인하고, 메소드가 구현되어 있는 프록시 클래스를 만들어 주기 때문이다.

추가로 @Repository(스프링 런타임 예외로 변환 역할)도 알아서 적용해준다.

@SpringBootTest
public class MemberRepositoryTest {
    @Autowired
    MemberRepository memberRepository;

    @Test
    void test() {
        System.out.println("memberRepository class= " + memberRepository.getClass());
    }
    //프록시 클래스가 출력되는 것을 확인할 수 있다.
    //memberRepository class= class com.sun.proxy.$Proxy117

공통 인터페이스 이해와 메소드 알아보기

우리가 사용하는 JPARepository 인터페이스와 JPARepository 상위 인터페이스 개념 그리고 가지고 있는 메소드들에 대해서 알아보자.

Spring Data JPA 주요 메서드

  • save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
  • delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
  • findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
  • getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
  • findAll(…) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.

구현체 SimpleJpaRepository

SimpleJpaRepository.class가 위에서 말한 프록시 구현체 클래스이다.

@Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환

@Transactional 어노테이션으로 따로 트랜잭션을 주입 받지 않아도, 메소드 실행 시 자동 트랜잭션 안에서 동작한다.

<SimpleJpaRepository에서 save() 메서드 사용을 주의해야 하는 이유>

(@Id를 @GeneratedValue설정하지 않으면 persist가 아니라 merge로 save되는 문제 발생)

//SimpleJpaRepository save 메소드
@Transactional
@Override
public <S extends T> S save(S entity) {
	
    Assert.notNull(entity, "Entity must not be null.");
    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

//직접 작성한 Id를 가진 엔티티로 save를 하게 되면,
@Entity
public class Item {
    @Id
    private String id;
}
Item item = new Item();
item.setId("가");
itemRepository.save(item)
//if (entityInformation.isNew(entity)) 부분에서 이미 id가 존재하기 때문에
//em.persist(entity); 가 아닌
//else {return em.merge(entity); 로 병합 -> DB조회 -> 값 없네 -> 인서트하게 된다.

해결 법으로는 entityInformation.isNew(entity)를 Id가 아닌 새로운 값으로 다시 설정 해줘야 한다.

//Persistable<String>를 직접 구현해서, 초기의 null인 값으로 
//entityInformation.isNew(entity) 판별을 할 수 있도록 설정한다.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Item implements Persistable<String> {
    @Id
    private String id;
    @CreatedDate
    private LocalDateTime createdDate;
    @Override
    public boolean isNew() {
        return createdDate == null;
    }
}

쿼리 메소드 기능

SpringDataJpa 인터페이스가 기본적으로 제공하는 CRUD 기능 외에도 다양한 쿼리 메소드 기능을 제공한다.

이 아래 내용부터 SpringDataJpa의 기능과 사용 방법에 대해서 알아보자.

앞으로 나올 예제 쿼리들은 밑에 그림 테이블, 엔티티 구조를 사용한다.

메소드 이름으로 쿼리 생성

스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행할 수 있다.

이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.

이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.

예시를 하나 들어보자. 회원의 이름과 나이를 기준으로 조회하려고 하면,

//순수 JPA에서는 createQuery 직접 작성 필요
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) {
        return em.createQuery("select m from Member m where m.username = :username and m.age > :age")
            .setParameter("username", username)
            .setParameter("age", age)
            .getResultList();
    }
//반면에, 이럭식으로 작성하면 분석해서 위와 같은 구현체를 만들어 준다.
public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}

스스로 분석해서 쿼리를 만들어주는 다양한 매칭 요소들은

Spring Data JPA - Reference Documentation에서 확인할 수 있다.

(true false innotnull like orderBy 다양한 매칭을 지원한다.)

다양한 쿼리 메소드

1 조회 조건 LIst나 단건 조회가 가능하고, 반환 타입에 관한 것은 좀 있다 나온다.

find…By ,read…By ,query…By get…By 모두 사용, …에서 분석에 영향은 주지 않지만, 식별하기 위한 내용이 들어갈 수 있다. ex)findHelloBy

2 COUNT 해당 조건 수

count…By 반환타입 long

3 EXISTS 해당 조건 값 존재하는지

exists…By 반환타입 boolean

4 삭제

delete…By, remove…By 반환타입 long

5 DISTINCT 중복 값 제거

findDistinct, findMemberDistinctBy

6 LIMIT 위에서 3개 가져오기

findFirst3, findFirst, findTop, findTop3

//숫자를 생략하면 결과 크기가 1로 간주
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();

Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);

Spring Data JPA - Reference Documentation 여기서 LImitting에 대해 추가적 설명을 볼 수 있다.

JPA NamedQuery

엔티티 클래스 안에 @NamedQuery 어노테이션으로 Named 쿼리 정의

정의한 Name쿼리를 해당 엔티티 spring data jpa 인터페이스에 적용 사용 가능하다.

그러나 잘 사용하지 않고, 아래에 나오는 @Query 방법을 사용한다.

//엔티티 클래스 적용
@Entity
@NamedQuery(
    name="Member.findByUsername",
    query="select m from Member m where m.username = :username")
public class Member {
}

//인터페이스에 적용
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query(name = "Member.findByUsername")
    List<Member> findByUsername(@Param("username") String username);
}

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

@Query 어노테이션을 사용하여 메소드 위에 JPQL 쿼리를 작성한다.

JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있는 큰 장점이 있다.

실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 매우 지저분해진다. 따라서 @Query 기능을 자주 사용하게 된다

public interface MemberRepository extends JpaRepository<Member, Long> {

    @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, 값, DTO 조회하기

1 단순 값 하나

JPA 값 타입( @Embedded )도 이 방식으로 조회할 수 있다.

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

2 DTO로 직접 조회

DTO로 직접 조회 하려면 JPA의 new 명령어를 사용해야 한다. 그리고 다음과 같이 생성자가 맞는 DTO가 필요하다.

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

@Data
public class MemberDto {

    private Long id;
    private String username;
    private String teamName;

    public MemberDto(Long id, String username, String teamName) {
        this.id = id;
        this.username = username;
        this.teamName = teamName;
    }
}

파라미터 바인딩

//위치기반
select m from Member m where m.username = ?0 
//이름기반
select m from Member m where m.username = :name 
@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);

Collection 타입으로 in절 지원

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

반환 타입

리턴 타입에 원하는 반환 타입을 입력할 수 있다.

List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional

컬렉션

결과 없음: 빈 컬렉션 반환

단건 조회

1 결과 없음: null 반환 (단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의Query.getSingleResult() 메서드를 호출한다. 이 메서드를 호출했을 때 조회 결과가 없으면 avax.persistence.NoResultException 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null 을 반환한다.)

2 결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생

추가 다른 반환 타입 알아보기 링크

Spring Data JPA - Reference Documentation

페이징과 정렬

순수 JPA로 하면 복잡한 페이징 작업을 spring data jpa가 만들어 놓은 메소드를 활용해 쉽게 해보자.

검색 조건: 나이가 10살

정렬 조건: 이름으로 내림차순

페이징 조건: 첫 번째 페이지, 페이지 당 보여줄 데이터는 3건

순수 JPA 페이징과 정렬

offset - 몇 번 데이터부터 시작할 것이냐(0번부터 시작), limit- offset부터 몇 개의 데이터를 가져올 것이냐

public List<Member> findByPage(int age, int offset, int limit) {
        return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
                .setParameter("age", age)
                .setFirstResult(offset)
                .setMaxResults(limit)
                .getResultList();
    }
@Test
    public void paging() throws Exception {
        //given
        memberJpaRepository.save(new Member("member1", 10));
        memberJpaRepository.save(new Member("member2", 10));
        memberJpaRepository.save(new Member("member3", 10));
        memberJpaRepository.save(new Member("member4", 10));
        memberJpaRepository.save(new Member("member5", 10));
        int age = 10;
        int offset = 0;
        int limit = 3;
        //when
        List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
        long totalCount = memberJpaRepository.totalCount(age);
        //페이지 계산 공식 적용...
        //전체 카운트도 가져와서 따로 공식을 사용해서 페이지 결과를 가져와야 한다.
}

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

페이징과 정렬 파라미터 제공

관계형 DB든 몽고 DB든 해당 클래스로 페이징(offset, limit, count함께 처리)과 정렬 파라미터 통합 시켰다.

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

특별한 반환 타입

해당 반환 타입을 작성하면, 파라미터는 같은데, 나가는 쿼리를 다르게 해서 필요한 데이터를 제공한다.

  • org.springframework.data.domain.Page : 추가 토탈 count 쿼리 결과를 포함하는 페이징 페이징 처리하는 많은 정보들 가지고 있다(몇 번째 페이지인지 등등)
  • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능 요즘 많이 나오는 더 보기 버튼 클릭하면 더 보여주는 곳에 사용(내부적으로 limit + 1조회) (자료가 더 있는 지 등 정보를 가지고 있다)
  • List (자바 컬렉션): 같은 페이징 소팅 파라미터 입력해도 추가 count 쿼리 없이 결과만 반환

Page 사용하기

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom{
    //Pageable 파아미터를 넘기고 반환 타입으로 Page 설정하면 된다.
    Page<Member> findByAge(int age, Pageable pageable);
}
@Test
public void page() throws Exception {
    //given
    memberRepository.save(new Member("member1", 10));
    memberRepository.save(new Member("member2", 10));
    memberRepository.save(new Member("member3", 10));
    memberRepository.save(new Member("member4", 10));
    memberRepository.save(new Member("member5", 10));
    //when
    //넘길 파라미터 설정한다
    //해당 페이지, 페이지 갯수, 그리고 소팅조건(설정 안해도 된다.)
    PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,
        "username"));
    Page<Member> page = memberRepository.findByAge(10, pageRequest);
    //then
    //Page 반환 타입에 페이지를 위한 다양한 정보가 저장되어 있다.
    List<Member> content = page.getContent(); //조회된 데이터
    assertThat(content.size()).isEqualTo(3); //조회된 데이터 수
    assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
    assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
    assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
    assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
    assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
}

PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다.

Slice 사용하기

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom{
    //파라미터는 같고, 반환 타입만 Slice로 변경
    Slice<Member> findByAge(int age, Pageable pageable);
}
//슬라이스 처리하는데 다양한 정보를 준다.
public interface Slice<T> extends Streamable<T> {
    int getNumber(); //현재 페이지
    int getSize(); //페이지 크기
    int getNumberOfElements(); //현재 페이지에 나올 데이터 수
    List<T> getContent(); //조회된 데이터
    boolean hasContent(); //조회된 데이터 존재 여부
    Sort getSort(); //정렬 정보
    boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부
    boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부
    boolean hasNext(); //다음 페이지 여부
    boolean hasPrevious(); //이전 페이지 여부
    Pageable getPageable(); //페이지 요청 정보
    Pageable nextPageable(); //다음 페이지 객체
    Pageable previousPageable();//이전 페이지 객체
    <U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}

만약 줄 수 있는 정보가 6개인데, 10개를 요청하게 되면, 10개를 요청하고 6개 데이터를 반환해서 문제 없다. 다만 hasNext() 다음 값이 있는 지는 false가 된다.

List 사용하기

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom{
    //파라미터는 같고, 반환 타입만 Slice로 변경
    List<Member> findListByAge(int age, Pageable pageable);
}
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC,
            "username"));
//count 쿼리 없이 데이터 정보만 받을 수 있다.
List<Member> list = memberRepository.findListByAge(10, pageRequest);

JPA 페이징과 정렬 성능 높이기(count 쿼리 분리)

이건 복잡한 sql에서 사용, 데이터는 left join, 카운트는 left join 안 하게 사용할 수 있다.

//메소드를 설정해서 count 쿼리는 join을 하지 않게 설정할 수 있다.
@Query(value = “select m from Member m”,
 countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);

아니면 TOP 설정을 통해서 카운트 없이 원하는 위 데이터 수만큼 가져올 수 있다. .

List<Member> findTop2By();

페이징 결과 → DTO 반환

아래 Page의 Map 메소드를 사용해서 바로 원하는 Dto로 변환이 가능하다.

public interface Page<T> extends Slice<T> {
		<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());

벌크성 수정 쿼리

@Modifying 어노테이션을 사용해서 벌크성 쿼리를 할 수 있다.

@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
//when
int resultCount = memberRepository.bulkAgePlus(20);
->
//하나의 쿼리로 여러개의 데이터 한번에 수정 가능
update member set age=age+1  where age>=?

벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화를 하자 @Modifying(clearAutomatically =true)

(이 옵션의 기본값은 false, 이 옵션 없이 회원을 findById 로 다시 조회하면 영속성 컨텍스트에 과거 값이 남아서 문제가 될 수 있다. 만약 다시 조회해야 하면 꼭 영속성 컨텍스트를 초기화 하자)

벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.

권장하는 방안

1 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.

2 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.

@EntityGraph - fetch join 역할

@EntityGraph을 사용하여, 연관된 엔티티들을 SQL 한번에 조회하는 방법에 대해서 알아보자. LEFT OUTER JOIN 사용한다

페치조인에 대한 내용을 확인하려면 링크

//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

//메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)

JPA Hint & Lock

JPA Hint

하이버네이트 기능을 좀 더 활용하기 위해, 하이버네이트에게 알려주는 힌트이다.

아래 예제의 경우는 해당 메소드로 읽어온 데이터는 읽기만 할 수 있고, 변경감지를 통한 update가 되지 않는다.

그러면 해당 메소드를 실행하면, 변경 감지 기능을 위한 메모리 사용을 줄여서, 성능이 좋아진다. 그러나, 정말 읽기만 하고, 정말 자주 쓰고 정말 성능을 많이 잡아 먹는 메소드에게만 붙이는 것이 좋다.

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

Lock

락 어노테이션을 붙이면 읽기를 위한 락이 걸린다. 해당 데이터에 다른 세션이 접근하지 못하게 된다.

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);

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

Spring Data Jpa 인터페이스의 메서드를 직접 구현하고 싶다면, 사용자 정의 리포지토리를 구현하면 된다.

코드를 통해서 사용자 정의 리포지 토리 구현 방법에 대해서 바로 알아보자.

//1 구현체의 틀인 사용자 정의 인터페이스를 만든다.
public interface MemberRepositoryCustom {
		List<Member> findMemberCustom();
}
//2 해당 인터페이스를 받아서 구현 클래스를 만든다.
//!!여기서 주의 이름 만드는 방법은
//MemberRepositoryImpl(리포지토리 인터페이스 이름 + Imp)을하거나
//MemberRepositoryCustomImpl(사용자 정의 인터페이스 명 + Impl)로 지정해야 한다.
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
    private final EntityManager em;
    @Override
    public List<Member> findMemberCustom() {
        return em.createQuery("select m from Member m")
            .getResultList();
    }
}

//3 사용자 정의 인터페이스를 jpa인터페이스에 상속한다.
public interface MemberRepository extends JpaRepository<Member, Long>,
 MemberRepositoryCustom{
}

Auditing - 변경 사람과 시간 자동 추적

엔티티를 생성, 변경할 때 변경한 사람시간을 자동 추적하고 싶을 때 Auditing 기능을 사용하면 된다.

예시 코드를 보면서 알아보자.

//1 먼저 스프르링 부트 설정 클래스에 @EnableJpaAuditing 등록
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataJpaApplication.class, args);
    }
}

//2 @CreatedDate @LastModifiedDate @CreatedBy @LastModifiedBy 등록하여
// 등록일 수정일 등록자 수정자 필드 지정하기.
//참고로 저장시점에 저장데이터만 입력하고 싶으면 
//@EnableJpaAuditing(modifyOnCreate = false) 옵션을 사용하면 된다.
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;
    @LastModifiedDate
    private LocalDateTime lastModifiedDate;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;
    @LastModifiedBy
    private String lastModifiedBy;
}

//3 등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록
//실무에서는 세션 정보나, 스프링 시큐리티 로그인 정보에서 ID를 받음
@Bean
public AuditorAware<String> auditorProvider() {
		return () -> Optional.of(UUID.randomUUID().toString());
}

//3 적용하고 싶은 엔티티에 상속
public class Member extends BaseEntity{ 
}

//4 결과 확인하기.
memberRepository.save(new Member("memberName", 11, null));
//이름 나이 팀아이디만 입력햇는데 알아서 등록일 수정일 생성자 수정자가 입력된다.

전체 적용 하기

@EntityListeners(AuditingEntityListener.class) 를 생략하고 스프링 데이터 JPA 가 제공하는 이벤트를 엔티티 전체에 적용하려면 orm.xml에 다음과 같이 등록하면 된다

///META-INF/orm.xml

http://xmlns.jcp.org/xml/ns/persistence/orm”>
 xmlns:xsi=“<http://www.w3.org/2001/XMLSchema-instance”>
 xsi:schemaLocation=“<http://xmlns.jcp.org/xml/ns/persistence/>
orm <http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd”>
 version=“2.2">
 
 
 
 

Web 확장 - 도메인 클래스 컨버터

HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩할 수 있다.

//적용 전
@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;
    @GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Long id) {
        Member member = memberRepository.findById(id).get();
        return member.getUsername();
    }
}

//적용 후
@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;
    @GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Member member) {
        //HTTP 요청은 회원 id 를 받지만 도메인 클래스 컨버터가 
        //중간에 동작해서 회원 엔티티 객체를 반환
        return member.getUsername();
    }
}

이 두개 좀있다 머리아픈데.

Web 확장 - 페이징과 정렬

기본 사용 법

스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용해보자.

/members**?page=0&size=3&sort=id,desc&sort=username,desc** 이렇게 날라온 URL을 페이징 파라미터로 받을 수 있다.

sort: 정렬 조건을 정의한다. 예) 정렬 속성,정렬 속성...(ASC | DESC), 정렬 방향을 변경하고 싶으면 sort 파라미터 추가 ( asc 생략 가능) - &sort=id 이런식으로 생략 가능

@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;
		
    //테스트를 위한 데이터 추가.
    @PostConstruct
    public void init() {
        for (int i = 0; i < 100; i++) {
            memberRepository.save(new Member("user" + i, i));
        }
    }
    @GetMapping("/members")
    public Page<Member> list(Pageable pageable) {
        Page<Member> page = memberRepository.findAll(pageable);
        return page;
    }
}

자동 size 설정 사용법 - page만 입력해주면 된다.

기본 설정은 size=20로 설정되어 있다.

http://127.0.0.1:8080/members?page=1&sort=id 이런 식으로 페이지만 입력해주면, size가 20개인 id로 오름 차순 정렬된 1페이지 데이터를 보여준다. (페이지는 0부터 시작이라 id가 21번인 데이터부터 출력)

기본 사이즈 글로벌 설정하기

spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/

개별 설정하기

@PageableDefault 어노테이션을 사용

@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 12, sort = "username",
    direction = Sort.Direction.DESC) Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page;
}
//http://127.0.0.1:8080/members 이렇게만 입력해도 기본 설정대로 페이징 정보를 준다.

페이징 정보가 두 개 라면

@Qualifier 에 접두사명 추가 "{접두사명}_xxx” 예제: /members?member_page=0&order_page=1

public String list(
 @Qualifier("member") Pageable memberPageable,
 @Qualifier("order") Pageable orderPageable, ... 따로 설정하면 된다.

Page 내용을 DTO로 변환하기

엔티티를 외부로 노출하면 안되기 때문에 Page.map() 사용해서 dto로 바꿔서 보낼 수 있다.

@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
		Page<Member> page = memberRepository.findAll(pageable);
		//map을 사용
		Page<MemberDto> pageDto = page.map(MemberDto::new);
		return pageDto;
}

Projections

전체 엔티티가 아니라, 필드 하나(username)만 조회하고 싶을 때나,

엔티티 대신에 DTO로 조회하고 싶을 때 사용하면 된다.

실무에서는 단순할 때만 사용하고, 복잡한 상황의 경우 Querydsl을 사용하자.

필드 하나만 조회

가져올 하나의 데이터 인터페이스 지정

public interface UsernameOnly {
    String getUsername();
}

스프링 데이터 jpa 인터페이스 반환 값에 해당 인터페이스 지정

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    //만들어 놓음 인터페이스 지정
	List<UsernameOnly> findProjectionsByUsername(String username);
}

사용해보기

@Test
public void projections() throws Exception {
    //given
    Team teamA = new Team("teamA");
    em.persist(teamA);
    Member m1 = new Member("m1", 0, teamA);
    Member m2 = new Member("m2", 0, teamA);
    em.persist(m1);
    em.persist(m2);
    em.flush();
    em.clear();
    //when
    List<UsernameOnly> result =
        memberRepository.findProjectionsByUsername("m1");
    for (UsernameOnly usernameOnly : result) {
        //해당 이름을 보고 값을 넣어준다. 맞춰야 한다.
        System.out.println(usernameOnly.getUsername());
        //m1을 잘 출력한다.
    }
}
//SQL 성능 최적화 select에 username만 선택해서 조회한다.
select
    member0_.username 
from
    member member0_ 
where
    member0_.username=?

DTO Class 조회

DTO 클래스 지정

public class UsernameOnlyDto {
    private final String username;
    public UsernameOnlyDto(String username) {
        this.username = username;
    }
    public String getUsername() {
        return username;
    }
}

스프링 데이터 jpa 인터페이스 반환 값에 해당 클래스 지정

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

    List<UsernameOnlyDto> findProjectionsByUsername(String username);

}

추가, 동적 Projections

하나의 메소드로 번갈아 끼우며 사용할 수 있다.

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

    List<UsernameOnlyDto> findProjectionsByUsername(String username);

}
List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1",
UsernameOnly.class);
//이렇게 번갈아 사용이 가능하다
List<UsernameOnlyDto> result = memberRepository.findProjectionsByUsername("m1",
UsernameOnlyDto.class);