쌩로그

실전! 스프링 데이터 JPA (인프런 - 김영한) 본문

Spring/JPA

실전! 스프링 데이터 JPA (인프런 - 김영한)

.쌩수. 2024. 2. 18. 05:42
반응형

목록

  1. 포스팅 개요
  2. 본론
      2-1. 공통 인터페이스 기능
      2-2. 쿼리 메소드 기능
      2-3. 확장 기능
      2-4. 스프링 데이터 JPA 분석
      2-5. 나머지 기능들
  3. 요약

1. 포스팅 개요

인프런에서 영한님의 실전! 스프링 데이터 JPA를 학습하고 정리한 포스팅이다.

실전! 스프링 데이터 JPA도 실전 활용 2편 강의처럼 책과 겹치는 부분이 딱 떨어지도록 구성되어있는 것이 아니라 겹치는 부분도 있고, 연관된 부분도 있다.

책에는 12장, 14장의 리스너엔티티 그래프, 16장 부분이다.

따라서 먼저는 강의 내용을 포스팅 하고, 이후 책 내용을 포스팅한다.

이번 포스팅은 스프링 데이터 JPA의 강의 내용이고, 다음 포스팅에서 책의 12, 14, 16 챕터에 대해 다뤄보고자 한다.

2. 본론

2-1. 공통 인터페이스 기능

스프링 데이터 JPA를 사용하려면 아래와 같이 Config 설정을 다음과 같이 해줘야 한다.

@SpringBootApplication
@EnableJpaRepositories(basePackages = "spring.datajpa.repository") // 부트 쓰면 생략가능

하지만 스프링 부트를 사용하면 생략가능하다.

그리고 package spring.datajpa; 이렇게 패키지가 선언되어있다면 해당 패키지로부터 하위 패키지까지 스프링 데이터 JPA가 Repository를 다 scan한다.

만약 위치가 달라지면 @EnableJpaRepositories(basePackages = "spring.datajpa.repository") 과 같이 애너테이션이 필요하다.

그런데, 다음과 같이 MemberRepository는 구현체가 없는 인터페이스인데,
어떻게 동작이 가능할까?

// 인터페이스

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

//테스트
@SpringBootTest
@Transactional
@Rollback(false)
class MemberJpaRepositoryTest {

    @Autowired
    MemberJpaRepository memberJpaRepository;

    @Test
    void test() {
        System.out.println("memberJpaRepository = " + memberJpaRepository.getClass());
    ...
    }
}

출력결과는 다음과 같다.

memberJpaRepository = class spring.datajpa.repository.MemberJpaRepository$$SpringCGLIB$$0

스프링 데이터 JPA스프링을 통해서 인터페이스 구현체를 만들어준다.
(테스트코드에서는 구현체를 만들어서 인젝션 해준다.)

또한 @Repository 애노테이션을 생략 가능하다.
인터페이스만으로도 스프링 데이터 JPA가 인식하여 구현체를 만들어준다.

참고로 @Repository의 기능은 다음과 같다.

  • 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리
  • JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리
@Test
void basicCRUD() {
    Member member1 = new Member("member1");
    Member member2 = new Member("member2");
    memberRepository.save(member1);
    memberRepository.save(member2);

    Member findMember1 = memberRepository.findById(member1.getId()).get();
    Member findMember2 = memberRepository.findById(member2.getId()).get();
    assertThat(findMember1).isEqualTo(member1);
    assertThat(findMember2).isEqualTo(member2);


    // 리스트 조회 검증
    List<Member> all = memberRepository.findAll();
    assertThat(all.size()).isEqualTo(2);

    // 카운트 검증
    long count = memberRepository.count();
    assertThat(count).isEqualTo(2L);


    // 삭제 검증
    memberRepository.delete(member1);
    memberRepository.delete(member2);

    long deletedCount = memberRepository.count();
    assertThat(deletedCount).isEqualTo(0L);
}

테스트 결과는 다음과 같다.

해당 테스트가 사용하는 Repository는 다음과 같다.

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

그런데 어떻게 이것이 가능할까?

구조는 다음과 같다.

상속받고 있는 JpaRepository의 패키지는 다음과 같다.
org.springframework.data.jpa.repository

스프링 데이터 JPA 프로젝트는 스프링 데이터 프로젝트와 공통적인 프로젝트가 있는데,
스프링 데이터 프로젝트는 공통의 CRUD를 제공하고 JPA에 특화된 기능들이 org.springframework.data.jpa.repository에서 구현되어 있다.

스프링 데이터 JPA는 상상할 수 있는 것을 다 제공한다.

스프링 데이터 JPA 커스텀 기능

구현하지 않아도 기능을 제공한다. (쿼리 메서드)

2-2. 쿼리 메소드 기능

  1. 메소드 이름으로 쿼리 생성
  2. 메소드 이름으로 JPA NamedQuery 호출
  3. @Query 어노테이션을 사용해서 리파지토리 인터페이스에 쿼리 직접 정의

쿼리 메소드 기능 1. 메소드 이름으로 쿼리 생성

이름과 나이를 기준으로 회원을 조회하려면?

순수 JPA 리포지토리는 다음과 같이 할 것이다.

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();

이를 스프링 데이터 JPA를 사용하면 다음과 같다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}

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

쿼리 메소드 필터 조건은 스프링 데이터 JPA 공식 문서에서 참고할 수 있다.

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능은 다음과 같다.

  • 조회 : find…By ,read…By ,query…By get…By,
  • COUNT : count…By 반환타입 long
  • EXISTS : exists…By 반환타입 boolean
  • 삭제 : delete…By, remove…By 반환타입 long
  • DISTINCT : findDistinct, findMemberDistinctBy
  • LIMIT : findFirst3, findFirst, findTop, findTop3

참고

이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.

쿼리 메소드 기능 2. JPA NamedQuery

참고로 실무에선 쓰지 않는다.

  • Entity에서 @NamedQuery에서 name과 query를 지정해야함.
@Entity
@NamedQuery(
    name="Member.findByUsername",
    query="select m from Member m where m.username = :username")
public class Member {
...
}
  • JPA 직접 사용시
public class MemberRepository {
public List<Member> findByUsername(String username) {
    ...
List<Member> resultList =
        em.createNamedQuery("Member.findByUsername", Member.class)
            .setParameter("username", username)
            .getResultList();
}
}
  • 스프링 데이터 JPA의 Repository에서는 @Query(name = "Member.findByUsername") 로 사용한다.
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username)

스프링 데이터 JPA로 Named 쿼리를 호출하려면 다음과 같이 하면 된다.

public interface MemberRepository extends JpaRepository<Member, Long>  { //** 여기 선언한 Member 도메인 클래스
    List<Member> findByUsername(@Param("username") String username);
}

참고로 스프링 데이터 JPA 쪽에서 주석처리를 해도 잘 동작한다.(단 Entity 에 선언되어있어야 한다.)

우선순위는 다음과 같다.

    1. @NamedQuery를 Entity에서 찾는다.
    1. @NamedQuery가 없으면, 메서드 이름으로 쿼리 생성

@NamedQuery의 장점 : 문법오류가 있을 때 애플리케이션 실행시 문법오류를 알려준다.

다시 말하지만, 참고로 실무에선 잘 쓰지 않는다.

쿼리 메소드 기능 3. @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에 jpql을 선언하고, 메서드를 사용하면 된다.
  • 오타 발생시 애플리케이션 실행 시점에 걸러준다.

@Query로 값, DTO 조회하기

username을 다음과 같이 뽑아올 수 있다.

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

참고로 DTO로 조회시 new 연산자를 사용해서 반환하도록 해야 한다.

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

참고로 파라미터들은 DTO 클래스 생성자의 형식에 맞게 해야한다.

파라미터 바인딩

두 종류가 있다.

  • 위치 기반
  • 이름 기반

위치 기반과 이름 기반이 있는데, 그냥 이름 기반을 사용하면 된다.

  • 컬렉션 파라미터 바인딩
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);

위와 같이 in절을 통해서 넣을 수 있다.

쿼리 실행시 where 절에 in절을 통해서 파라미터가 들어간다.

반환 타입

스프링 데이터 JPA는 유연한 반환타입을 지원한다.
컬렉션 / 단건 / Optional 등등이 있다.

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

스프링 데이터 JPA에서 단건 조회시 null을 반환하게 되면 알아서 try-catch를 통해서 null을 반환한다.

참고로 자바 8 이후로는 그냥 Optional을 반환하게 하면 된다.

만약 Optional로 반환하는데, 2건 이상이라면 org.hibernate.NonUniqueResultException이 터지는데, 이 예외를 스프링 데이터 JPA가 해당 예외를 스프링의 예외로 변환해서 반환한다.
org.springframework.dao.IncorrectResultSizeDataAccessException 이게 그것이다.

반환타입은 공식문서에서 자세히 볼 수 있다.

순수 JPA 페이징과 정렬

page를 파라미터나 offset, limit을 지정해주면, DB가 달라도 방언에 따라 페이징 쿼리를 알아서 실행해준다.

예를 들어 다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.

  • 검색 조건: 나이가 10살
  • 정렬 조건: 이름으로 내림차순
  • 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건

JPA 페이징 리포지토리 코드

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();
}

public long totalCount(int age) {
    return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)
            .setParameter("age", age)
            .getSingleResult();
}

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

JPA는 페이징을 Pageable(페이지기능) 와 Sort(정렬)로 추상화했다.

페이징과 정렬 파라미터

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

특별한 반환 타입

  • org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징(totalCount와 쿼리 결과를 포함한다.)
  • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit + 1조회)(totalCount는 없이 결과를 포함한다.)
  • List (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환

페이징과 정렬 사용 예제

Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort);

다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자

  • 검색 조건: 나이가 10살
  • 정렬 조건: 이름으로 내림차순
  • 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건

Page 사용 예제 정의 코드

public interface MemberRepository extends Repository<Member, Long> {
 Page<Member> findByAge(int age, Pageable pageable);
}

Page 사용 예제 실행 코드

//페이징 조건과 정렬 조건 설정
@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
    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(); //다음 페이지가 있는가?
}
  • Pageable을 넣을 때 구현체로 PageRequest를 넣어주고, of() 메서드를 통해서 Sort조건을 넣어주었다.

  • 두 번째 파라미터로 받은 Pageable인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다.

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

Page 인터페이스

public interface Page<T> extends Slice<T> {
    int getTotalPages(); //전체 페이지 수
    long getTotalElements(); //전체 데이터 수
    <U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}

Slice 인터페이스

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); //변환기
}

@Query를 통해서 쿼리와 countQuery를 분리할 수 있다.

@Query(value = "select m from Member m",
        countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);

페이지를 유지하면서 엔티티를 DTO로 변환하기

Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());

참고로 slice에는 PageRequest를 통해 넣은 pageSize에 1개를 더 추가해서 불러온다.
요청보다 하나 더 추가해서 가져온다.

@Test
void sliceTest() {
    // 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));

    int age = 10;
    PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

    // when
    Slice<Member> slice = memberRepository.findByAge(age, pageRequest);

    // then
    List<Member> content = slice.getContent();

    assertThat(content.size()).isEqualTo(3);
    assertThat(slice.getNumber()).isEqualTo(0); // 페이지 번호
    assertThat(slice.isFirst()).isTrue();
    assertThat(slice.hasNext()).isTrue();
}

벌크성 수정 쿼리

변경감지(dirty checking)는 한 건 한 건이지만, 벌크성 수정 쿼리는 한번의 SQL로 전체 데이터를 업데이트 하는 것을 의미한다.

SQL로는 쉽지만, JPA는 엔티티를 중심으로 하기 때문에 조금 우회해야 한다..

JPA를 사용한 벌크성 수정 쿼리는 다음과 같다.

public int bulkAgePlus(int age) {
    int resultCount = em.createQuery(
        "update Member m set m.age = m.age + 1" +
            "where m.age >= :age")
        .setParameter("age", age)
        .executeUpdate();
    return resultCount;
}

스프링 데이터 JPA를 사용한 벌크성 수정 쿼리는 다음과 같다.

@Modifying
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
  • @Modifying : executeUpdate() 역할을 한다.

  • @Query()의 JPQL에 update문을 넣어야 한다.

  • 벌크성 수정, 삭제 쿼리@Modifying 어노테이션을 사용힌다.

    • 사용하지 않으면 다음 예외가 발생한다.
    • org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations
  • 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화: @Modifying(clearAutomatically = true)(이 옵션의 기본값은 false )

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

참고로 벌크 연산 후엔 반드시 영속성 컨텍스트를 clear해야한다.
기본편과 책에 있다. (정리도 했다.)

@Modifying 의 속성에서

clearAutomatically = true
flushAutomatically = true

이 두 옵션을 통해서 영속성 컨텍스트의 flush()와 clear()를 호출할 수 있다.

@EntityGraph

연관된 엔티티들을 SQL 한번에 조회하는 방법이다.

  • fetch join을 해야할 때 JPQL이 아니라 메서드 이름으로만 해결할 때 사용한다.
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

위와 같이 사용한다.
Lazy 로딩으로 인한 N + 1이 나오지 않는다.

아래와 같이 JPQL과 같이 사용도 가능하다.

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

메서드 이름(스프링 데이터 JPA가 메서드 이름을 추론해서 만들어주는 SQL)으로만 할 때는 다음과 같이 그냥 붙여주면 된다.

@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)

참고로 @EntityGraph스프링 데이터 JPA가 아니라 JPA에서 제공하는 기능이다.

EntityGraph 정리하자면 다음과 같다.

  • 사실상 페치 조인(FETCH JOIN)의 간편 버전이다.
  • LEFT OUTER JOIN 사용한다.

JPA의 NamedEntityGraph

아래의 애너테이션을 Entity에 선언하고,

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

Repository에서 다음과 같이 사용할 수 있다.

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

(참고로 영한님은 NamedEntityGraph 잘 안 쓰신다고 한다.
EntityGraph 혹은, JPQL을 사용한다고 하신다.)

JPA Hint & Lock

JPA Hint 는 JPA의 쿼리 힌트로써 JPA의 구현체(하이버네이트)에게 제공하는 힌트다.
변경 감지를 할 때는 객체를 2개 관리해야한다.
최적화가 되어있더라도 비용이 발생한다.
JPA는 객체를 조회해서 영속성 컨텍스트에서 관리하는 순간 동일한 객체를 2개 관리한다.

dirty checking 없이 단순히 조회만 할 경우 Hint를 제공하는데, Hibernate는 제공하지만, JPA 표준은 이를 제공하지 않는다.

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

QueryHint성능 최적화를 하려면, 테스트를 해보고, 이점이 있을 때 넣어야 한다.
전체적으로 무조건 튜닝이 아니라, 테스트를 해보고 이점이 명확할 때 하는 것이 좋다.
대부분의 성능은 복잡한 쿼리에서 좌지우지 된다.

Lock

select for update같dl 데이터베이스에서는 select 할 때 락을 걸수 있다.
(책에서는 비관적 락이라고 번역)

@Lock(LockModeType.PESSIMISTIC_WRITE) // JPA에서 Lock을 지원한다.
List<Member> findLockByUsername(String username);

이를 사용하면,

List<Member> result = memberRepository.findLockByUsername("member1");

해당 쿼리 끝에 for update가 붙는다.

select
m1_0.member_id,
m1_0.age,
m1_0.team_id,
m1_0.username
from
member m1_0
where기준
m1_0.username=? for update

(참고로 SQL은 방언에 따라 달라짐.)

실시간 트래픽이 많은 곳에는 Lock을 걸면 안 된다.
(책 기준 15, 16장이다.)

2-3. 확장 기능

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

  • 실무에서 중요하다.

JPA와 함께 다른 기능도 함께 사용할 때,
예를 들어, JPA를 EntityManager를 통해 직접 사용 하거나, 스프링 JDBC Template, 혹은 MyBatis, 데이터베이스 커넥션 직접 사용, QueryDSL 등을 사용할 때 직접 리포지토리를 구현해서 사용할 수 있다.

사용자가 정의한 인터페이스다.

public interface MemberRepositoryCustrom {
    List<Member> findMemberCustom();
}

다음은 사용자가 정의한 인터페이스를 구현한 클래스다.

@RequiredArgsConstructor
public class MemberRepositoryCustromImpl implements MemberRepositoryCustrom {

    private final EntityManager em;

    @Override
    public List<Member> findMemberCustom() {
        return em.createQuery("select m from Member m")
                .getResultList();
    }

}

다음은 사용자 정의 인터페이스를 상속하는 경우다.

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustrom { ... }

사용자 정의 메서드를 호출하는 코드다.

List<Member> result = memberRepository.findMemberCustom();

사용자 정의 리포지토리를 구현할 때 규칙이 있다.

  • 커스텀 인터페이스를 구현한 클래스이름을 맞춰야 한다.
  • 커스텀 인터페이스는 아무렇게나 해도되지만, 스프링 데이터 JPA 인터페이스 이름 + Impl을 붙여야 한다.

그러나 관례를 안 따르려면 다음과 같이 할 수 있다.
(그냥 관례를 따르자.)

@EnableJpaRepositories(basePackages = "spring.datajpa.repository", repositoryImplementationPostFix = "Impl : 변경할 명명 규칙")

관례를 따르자.

참고

  • 실무에서는 주로 QueryDSL이나 SpringJdbcTemplate을 함께 사용할 때 사용자 정의 리포지토리 기능을 자주 사용한다.

참고

항상 사용자 정의 리포지토리가 필요한 것은 아니다. 그냥 임의의 리포지토리를 만들어도 된다. 예를 들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 그냥 직접 사용해도 된다. 물론 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다.

참고

개발할 때 화면에 맞춘 쿼리와 핵심 비즈니스 로직을 분리할 필요가 있음.

Auditing

등록일, 수정일, 등록자, 수정자 를 추적하고 싶을 때 사용한다.

먼저 순수 JPA이다.

@MappedSuperclass // Entity에서 속성만 상속받을 때
public class JpaBaseEntity {

    @Column(updatable = false)
    private LocalDateTime createDate;
    private LocalDateTime updateDate;

    @PrePersist // Persist 하기 전 이벤트 발생
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        createDate = now;
        updateDate = now;
    }

    @PreUpdate // update 전 이벤트
    public void preUpdate() {
        updateDate = LocalDateTime.now();
    }

}

이걸 클래스를 상속받으면 된다.
그런데 앞의 내용은 JPA 이벤트로 처리했지만, 스프링 데이터 JPA에서는 더욱 깔끔하게 해결하게 한다.

@EnableJpaAuditing를 스프링 애플리케이션 실행시 동작한다.

@EntityListeners(AuditingEntityListener.class) // 이벤트 기반으로 동작하게한다.
@MappedSuperclass
@Getter
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime creatdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;

    // 등록자
    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    // 수정자
    @LastModifiedBy
    private String lastModifiedBy;

}

등록자와 수정자를 처리하려면 스프링 애플리케이션에서 다음과 같이 한다.
(비슷하게 함. 실제는 세션이나, 시큐리티를 통해서 아이디를 주도록 해야한다.)

@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataJpaApplication.class, args);
    }

    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.of(UUID.randomUUID().toString());
    }
}

그러면 스프링 데이터 JPA가 BaseEntity에 해당하는 항목이 등록되거나, 수정될 때, 애플리케이션에서 등록한 auditorProvider를 호출해서 얻은 결과물을 가져가기 때문에, 등록자와 수정자에 값이 자동으로 들어갈 수 있다.

참고

실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다. 그래서 다음과 같이 Base 타입을 분리하고, 원하는 타입을 선택해서 상속한다.

public class BaseTimeEntity {
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;
    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}
public class BaseEntity extends BaseTimeEntity {
    @CreatedBy
    @Column(updatable = false)
    private String createdBy;
    @LastModifiedBy
    private String lastModifiedBy;
}

참고

저장시점에 등록일, 등록자는 물론이고, 수정일, 수정자도 같은 데이터가 저장된다.
데이터가 중복 저장되는 것 같지만, 이렇게 해두면 변경 컬럼만 확인해도 마지막에 업데이트한 유저를 확인 할 수 있으므로 유지보수 관점에서 편리하다.

이렇게 하지 않으면 변경 컬럼이 null 일때 등록 컬럼을 또 찾아야 한다.
참고로 저장시점에 저장데이터만 입력하고 싶으면 @EnableJpaAuditing(modifyOnCreate = false) 옵션을 사용하면 된다.

전체 적용

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

// META-INF/orm.xml

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="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">

    <persistence-unit-metadata>
        <persistence-unit-defaults>
            <entity-listeners>
                <entity-listener
                class="org.springframework.data.jpa.domain.support.AuditingEntityListener"/>
                </entity-listeners>
            </persistence-unit-defaults>
        </persistence-unit-metadata>
</entity-mappings>

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();
    }

}

위의 코드는 도메인 클래스 컨버터를 사용하기 전 코드다.
여기서 id에 pk가 들어갔다.
도메인 클래스 컨버터(Domain Class Converter) 기능을 사용히면 다음과 같다.

@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;

    @GetMapping("/members2/{id}")
    public String findMember(@PathVariable("id") Member member) {
        return member.getUsername();
    }
}

스프링이 중간에서 컨버팅하는 과정을 끝내고 Member를 바로 파라미터의 결과로 인젝션한다.
이것이 도에인 클래스 컨버터 기능이다.

참고로 잘 쓰지 않는다.
사실 PK를 가지고, 외부에 공개해서 찾는 경우도 있지만, 그리 많지 않다.
또한 쿼리가 단순한 경우도 많지 않고, 복잡해지면 못 쓴다.

  • HTTP 요청은 회원 id를 받지만, 도메인 클래스 컨버터가 동작해서 회원 엔티티 객체를 반환한다.

  • 도메인 클래스 컨버터도 Repository를 사용해서 엔티티를 찾는다.

  • 주의 : 도메인 클래스 컨버터로 엔티티를 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다.

    • 트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.

Web 확장 - 페이징과 정렬

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

@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page;

}

Pageable은 인터페이스인데, 스프링 부트스프링 데이터 JPA의 기능을 사용해서 자동으로 구현체를 세팅한다.
HTTP parameter가 컨트롤러에서 바인딩될 때 pageable이 있으면, org.springframework.data.domain.PageRequest라는 객체를 생성해서 값을 채운 후, 파라미터에 인젝션을 해준다.

그리고 다음과 같이 요청할 수 있다.

http://localhost:8080/members?page=0&size=3&sort=id,desc&sort=username,desc

만약 페이지의 설정을 바꾸고 싶다면 다음과 같은 경우들로 바꿀 수 있다.

  1. Global 설정

application.yml에 설정할 수 있따.

spring:
  data:
    web:
      pageable:
        default-page-size: 10
        max-page-size: 2000

위와 같이 설정하면 10개의 pageSize를 설정할 수 있다.
또한 최대 페이지 사이즈를 설정할 수도 있다.(max-page-size)

  1. 개별 설정

개별 설정(특별한 설정)이 가능한데 Global보다 우선순위가 높다.

아까 한 부분 중,

@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 5) Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page;
}

위와 같이 @PageableDefault 설정울 통해서 기본 값을 변경할 수 있다.
참고로 그냥 기본 값은 page는 0, size는 20이다.

접두사

  • 페이징 정보가 둘 이상이면 접두사로 구분한다.
  • @Qualifier에 접두사명 추가 "{접두사명}_xxx"
    • 예시 : `/members?member_page=0&order_page=1
public String list{
    @Qualifier("member") Pageble memberPageable,
    @Qualifier("order") Pageable orderPagable
}

위와 같이 하면 member_page는 memberPageable에,
order_page는 orderPagable에 들어간다.

page 내용을 DTO로 변환하기

요청에 대한 응답을 반환을 할 떄 엔티티를 반환하는 것이 아니라, DTO를 변환해서 넘기도록 한다.

다음과 같이 하면 된다.


@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    Page<MemberDto> map = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));
    return map;
}

이 떄 DTO를 반환할 때 DTO에 다음과 같이 생성자를 두고 사용할 수 있다.

public MemberDto(Member member) {
    this.id = member.getId();
    this.username = member.getUsername();
    this.teamName = member.getUsername();
}

// 컨트롤러
    @GetMapping("/members")
    public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable) {
        Page<MemberDto> map = memberRepository.findAll(pageable).map(MemberDto::new);
        return map;
    }

page 1부터 시작하기

  • Page를 커스터마이징 구현을 하면 된다.
  • spring.data.web.pageable.one-indexed-parameterstrue로 설정하면 된다.
    • 그런데 이 방법은 한계가 있다.
    • page를 1로 줘도, 다른 pageable 데이터와 맞지 않다. 다른 데이터들은 0부터 시작한다고 가정하고, 데이터를 뿌려준다.

2-4 스프링 데이터 JPA 분석

스프링 데이터 JPA 구현체 분석

org.springframework.data.jpa.repository.support.SimpleJpaRepository스프링 데이터 JPA의 구현체이다.
그런데 이 구현체의 코드를 보면 순수 JPA를 사용해서 구현함을 알 수 있다.

@Repository애너테이션은 두 가지 기능을 제공한다.

  1. 컴포넌트 스캔의 대상이 된다.
  2. 스프링 데이터 JPA에서 예외가 발생하더라도, 스프링의 예외로 변환해서 처리해준다.

JPA의 데이터 변경은 한 트랜잭션에서 일어나야 한다.
스프링 데이터 JPA를 통해서 데이터가 변경되는 이유는 구현체에서 @Transactional을 사용하기 때문이다.

@Transactional

@Transactional은 다음과 같은 기능들이 있다.

  • JPA의 모든 변경은 트랜잭션 안에서 동작
  • 스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션 안에서 처리한다.
  • 서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션을 시작한다.
    • 서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용한다.
    • 그래서 스프링 데이터 JPA를 사용할 때 트랜잭션이 없어도 데이터 등록, 변경이 가능했던 것이다.(사실은 트랜잭션이 리포지토리 계층에 걸려있다.)

@Transactional(readOnly = true)

  • 데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있음.
  • 자세한 내용은 JPA 책 15장읽기 전용 쿼리의 성능 최적화를 참고할 수 있다.

새로운 엔티티를 구별하는 방법

매우 중요!!!

  • save() 메서드
    • 새로운 엔티티면 저장( persist )
    • 새로운 엔티티가 아니면 병합( merge )

새로운 엔티티를 판단하는 기본 전략

새로운 엔티티를 판단하는 기본 전략은 다음과 같다.

  • 식별자가 객체일 때 null 로 판단
  • 식별자가 자바 기본 타입일 때 0으로 판단
  • Persistable 인터페이스를 구현해서 판단 로직 변경 가능

참고로 Persistable 인터페이스는 다음과 같이 정의되어 있다.

package org.springframework.data.domain;

import org.springframework.lang.Nullable;

public interface Persistable<ID> {
    @Nullable
    ID getId();

    boolean isNew();
}

만약 다음과 같이 @GenerateValue가 없이 Entity가 정의되었다고 가정해보자.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {

    @Id
    private String id;


    public Item(String id) {
        this.id = id;
    }

}

// 테스트
@Test
void save() {
    Item item = new Item("A");
    itemRepository.save(item);
}

테스트 코드를 디버깅해보면, 해당 Entity를 새로운 Entity로 판단하지 않는다.
그래서 merge()가 동작한다.
merge()는 우선 DB에서 select로 조회를 하고, DB에 값이 없으면 새로운 엔티티로 인지하여 insert쿼리를 실행한다. DB에서 select를 조회하기 때문에 비효율적이다.
그래서 기본적으로는 merge를 사용하지 않는 방향으로 해야한다.

따라서 Persistable를 사용해서 새로운 엔티티 확인 여부를 직접 구현하는 것이 효과적이다.
참고로 등록시간( @CreatedDate ) 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다.(@CreatedDate에 값이 없으면 새로운 엔티티로 판단)

정리하면 @GenerateValue사용하지 못할 경우 Persistable 인터페이스구현해서 새로운 Entity 판단 로직 변경을 할 수 있다.

예를 들면, 다음과 같이 사용할 수 있다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {

    @Id
    private String id;


    public Item(String id) {
        this.id = id;
    }

    @CreatedDate
    private LocalDateTime createdDate;

    @Override
    public String getId() {
        return id;
    }
    @Override
    public boolean isNew() {
        return createdDate == null;
    }

}

@CreatedDate 는 결과적으로 jpa에서 persist가 되기 전에 호출된다.
그렇기 때문에, createdDate가 있는지 여부로, 새로운 엔티티인지 아닌지 구분할 수 있다.

2-5. 나머지 기능들

Specifications (명세)

도메인 주도 설계(Domain Driven Design)SPECIFICATION(명세)라는 개념을 소개한다.
스프링 데이터 JPAJPA Criteria를 활용해서 이 개념을 사용할 수 있도록 지원한다.
(참고로 Criteria 는 실무에서 잘 사용하지 않는다.)

Specifications는 술어(predicate) 로 이루어져 있다.

  • 참 또는 거짓으로 평가
  • AND OR 같은 연산자로 조합해서 다양한 검색조건을 쉽게 생성(컴포지트 패턴)
    • 예) 검색 조건 하나하나
  • 스프링 데이터 JPA는 org.springframework.data.jpa.domain.Specification클래스로 정의한다.

JPA Criteria의 기능을 사용하는데, QueryDSL을 사용하자..!!

Query By Example

스프링 데이터 JPA의 최신 기술로서 specification과 비슷하게 쿼리를 할 때 Example에 의해서 하겠다는 것이다.
(참고로 실무에서 쓰지 않는다.)

다음은 테스트 코드의 when/then 대한 부분이다.

// when
// Probe 생성
Member member = new Member("m1");
Team team = new Team("teamA"); //내부조인으로 teamA 가능
member.setTeam(team);

//ExampleMatcher 생성, age 프로퍼티는 무시
ExampleMatcher matcher = ExampleMatcher.matching()
    .withIgnorePaths("age");

Example<Member> example = Example.of(member, matcher);

List<Member> result = memberRepository.findAll(example);

// then
assertThat(result.get(0).getUsername()).isEqualTo("m1");

org.springframework.data.repository.queryQueryByExampleExecutor 에서 제공하는 기능이다.
"new Member("m1");"을 통해서 where 조건을 만든다.

참고

  • Probe : 필드에 데이터가 있는 실제 도메인 객체
  • ExampleMatcher : 특정 필드를 일치시키는 상세한 정보 제공, 재사용 가능
  • Example : Probe와 ExampleMatcher로 구성, 쿼리를 생성하는데 사용

Query By Example의 장단점은 다음과 같다.

장점

  • 동적 쿼리를 편리하게 처리
  • 도메인 객체를 그대로 사용
  • 데이터 저장소를 RDB에서 NOSQL로 변경해도 코드 변경이 없게 추상화 되어 있음 => 패키지가 org.springframework.data.repository.query
  • 스프링 데이터 JPA JpaRepository 인터페이스에 이미 포함

단점

  • 조인은 가능하지만 내부 조인(INNER JOIN)만 가능하다. 외부 조인(LEFT JOIN)이 안 된다.
  • 다음과 같은 중첩 제약조건 안됨
    • firstname = ?0 or (firstname = ?1 and lastname = ?2)
  • 매칭 조건이 매우 단순함
    • 문자는 starts/contains/ends/regex
    • 다른 속성은 정확한 매칭( = )만 지원

쿼리는 다음과 같다.

select
    m1_0.member_id,
    m1_0.age,
    m1_0.team_id,
    m1_0.username
from
    member m1_0
where
    and m1_0.username=?

username을 검색 조건에 넣었다.
또한 참고로 age는 Entity 클래스에서 기본형이다.
null이 들어갈 수 있으면, 조건에서 제외되지만 age는 기본형(Primitive Type)으로 값이 0이기 때문에 검색조건에 들어가기 때문에 age를 제외해줬다.

또한 sql 쿼리를 보면 조인에 대한 해결이 되지 않는다.
inner 조인만 가능하고 outer join이 되지 않는다.

만약에 코드를 다음과 같이 작성했다면, 연관관계까지 고려해서 select를 해준다.
team에 있는 team의 name도 검색조건에 넣어준다.

Member member = new Member("m1");
Team team = new Team("teamA");
member.setTeam(team);

결과는 다음과 같다.

select
    m1_0.member_id,
    m1_0.age,
    m1_0.team_id,
    m1_0.username
from
    member m1_0
join
    team t1_0
        on t1_0.team_id=m1_0.team_id
where
    t1_0.name=?
    and m1_0.username=?

정리

  • 실무에서 사용하기에는 매칭 조건이 너무 단순하고, LEFT 조인이 되지 않는다.
  • 실무에서는 QueryDSL을 사용하자!

Projections

쿼리의 select 절에 들어갈 데이터를 의미한다.
엔티티 대신에 DTO를 편리하게 조회할 때 사용한다.

만약 전체 엔티티가 아니라 만약 회원 이름만 딱 조회하고 싶으면?

인터페이스 기반 Closed Projections

다음과 같이 프로퍼티 형식(getter)의 인터페이스를 제공하면, 구현체는 스프링 데이터 JPA가 제공한다.

public interface UsernameOnly {
    String getUsername();
}

JPA 레포지토리에서의 메서드는 다음과 같을 것이다.

List<UsernameOnly> findProjectionByUsername(@Param("username") String username);

실행된 쿼리는 다음과 같다.

select m1_0.username from member m1_0 where m1_0.username='m1';

인터페이스를 정의하면 실제 구현체를 스프링 데어터 JPA가 인터페이스를 통해 username만 인식해서 구현체까지의 데이터를 담아 반환을 해준 것이다.

Open Projection

일단 Entity를 조회해서 애플리케이션에서 데이터를 처리하는 것을 의미한다.
사용법은 다음과 같다.

import org.springframework.beans.factory.annotation.Value;

public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age}")
String getUsername();
}

위에서 봤던 close projection과의 차이는 단순히 @Value 값 여부이다.

참고로 단 이렇게 SpEL문법을 사용하면, DB에서 엔티티 필드를 다 조회해온 다음에 계산한다. 따라서 JPQL SELECT절 최적화가 되지 않는다.

클래스 기반 Projection

여기서는 생성자가 중요하다.

public UsernameOnlyDto(String username) {
    this.username = username;
}

생성자 안의 파라미터 명을 통해서 JPA가 분석한다.

Repository의 코드는 다음과 같다.

List<UsernameOnlyDto> findProjectionByUsername(@Param("username") String username);

이를 디버깅 해보면 Proxy가 아니라 구현 클래스가 있음을 알 수 있다.

동적 프로젝션

다음과 같이 제너릭도 적용할 수 있다.

<T> List<T> findProjectionByUsername(@Param("username") String username, Class<T> type);

사용코드는 다음과 같다.

List<UsernameOnlyDto> result = memberRepository.findProjectionByUsername("m1", UsernameOnlyDto.class);

중첩 구조 처리

public interface NestedClosedProjections {

    String getUsername();
    TeamInfo getTeam();

    interface TeamInfo {
        String getName();
    }

}

유저 이름과 유저가 속한 팀의 이름을 가져오도록 했다.
그런데 쿼리를 보면, Member 쪽에서는 username을 가져올 수 있지만,
Team 같은 경우 Team 엔티티의 모든 내용을 가져온다.

select
    m.username as col_0_0_,
    t.teamid as col_1_0_,
    t.teamid as teamid1_2_,
    t.name as name2_2_
from
    member m
left outer join
    team t
        on m.teamid=t.teamid
where
    m.username=?

주의

  • 프로젝션 대상이 root 엔티티면, JPQL SELECT절dmf 최적화 가능gkek.
  • 프로젝션 대상이 ROOT가 아니면
    • LEFT OUTER JOIN 처리
    • 모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산한다.

정리

  • 프로젝션 대상이 root 엔티티면 유용하다.
  • 프로젝션 대상이 root 엔티티를 넘어가면 JPQL SELECT 최적화가 안된다!
  • 실무의 복잡한 쿼리를 해결하기에는 한계가 있다.
  • 실무에서는 단순할 때만 사용하고, 조금만 복잡해지면 QueryDSL을 사용하자

네이티브 쿼리

가급적 네이티브 쿼리는 사용하지 않는게 좋다.
정말 어쩔 수 없을 때 사용하자.

최근에 나온 궁극의 방법 -> 스프링 데이터 Projections를 활용하는 것이다.

사용법은 다음과 같다.

// Repository
@Query(value = "select * from member where username =?", nativeQuery = true)
Member findByNativeQuery(String username);

// 사용
Member result = memberRepository.findByNativeQuery("m1");

다음은 실행된 sql이다.

select \* from member where username ='m1';

스프링 데이터 JPA 기반 네이티브 쿼리

네이티브 쿼리는 다음과 같은 기능을 제공한다.

  • 페이징 지원
  • 반환 타입
    • Object[]
    • Tuple
    • DTO(스프링 데이터 인터페이스 Projections 지원)
  • 제약
    • Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있다.
      • 믿지 말고 직접 처리하자.
    • JPQL처럼 애플리케이션 로딩 시점에 문법 확인이 불가
    • 동적 쿼리 불가

JPA 네이티브 SQL 지원

JPA 네이티브 SQL을 지원한다.

@Query(value = "select * from member where username =?", nativeQuery = true)
Member findByNativeQuery(String username);
  • JPQL은 위치 기반 파리미터를 1부터 시작하지만 네이티브 SQL은 0부터 시작
  • 네이티브 SQL을 엔티티가 아닌 DTO로 변환을 하려면

Projections 활용

다음은 스프링 데이터 JPA 네이티브 쿼리와** 인터페이스 기반 Projections**를 활용한 코드다.

public interface MemberProjection {

    Long getId();
    String getUsername();
    String getTeamName();

}

// Repository
@Query(value = "select m.member_id as id, m.username, t.name as teamName " +
        "from member m left join team t",
        countQuery = "select count(*) from Member",
        nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);

이렇게 하면 페이징 처리도 할 수 있다.

동적 네이티브 쿼리

동적 네이티브 쿼리를 사용하려면 다음과 같은 방법을 활용해야 한다.

  • 하이버네이트를 직접 활용
  • 스프링 JdbcTemplate, myBatis, jooq같은 외부 라이브러리 사용

3. 요약

  • 공통 인터페이스 기능을 통해서 스프링 데이터 JPA가 인터페이스로 선언한 Repository 인터페이스를 컴포넌트 스캔하여 알아서 구현체를 만들어 주는 것을 알아보았다.
  • 쿼리 메소드 기능을 통해서 메서드 이름의 규칙에 따라 쿼리문을 스프링 데이터 JPA가 알아서 만들어주는 것을 알아보았다.
  • 확장 기능에서는 Auditing, 도메인 컨버터 클래스, 페이징 등을 알아보았다.
  • 스프링 데이터 JPA 분석쪽에서 구현체를 알아보았고, 새로운 엔티티를 어떻게 구별할 수 있는지 알아보았고, 알아보는 과정에서 merge()에 대해서도 알아보았다.
  • 나머지 기능들에서는 잘 쓰지 않는 명세, Query By Example 그리고 네이티브 쿼리를 소개해주셨고, Entity를 DTO로 반환할 수 있는 Projections 기능에 대새허도 알아보았다.
728x90
Comments