Ian's Archive 🏃🏻

Profile

Ian

Ian's Archive

Developer / React, SpringBoot ...

📍 Korea
Github Profile →
Categories
All PostsAlgorithm19Book1C1CI/CD2Cloud3DB1DesignPattern9ELK4Engineering1Front3Gatsby2Git2IDE1JAVA7JPA5Java1Linux8Nginx1PHP2Python1React9Security4SpatialData1Spring26
thumbnail

실전 스프링 데이터 JPA

JPA
2022.10.11. (수정됨)

Series

  • 1자바 ORM 표준 JPA 프로그래밍 (기본편)
  • 2실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발
  • 3실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
  • 4실전 스프링 데이터 JPA
  • 5Querydsl

공통 인터페이스 설정

@SpringBootApplication위치에 @EnableJpaRepositories설정
(스프링 부트 사용시 생략 가능)

JpaRepository를 상속받아 인터페이스 JpaRepository내 구현된 메소드 사용 필요 시 확장
-> Generic - <엔티티 타입, 식별자 타입>

복사
public interface TeamRepository extends JpaRepository<Team, Long> {
}

쿼리 메소드

메소드 이름으로 쿼리 생성

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

메소드 이름 내에서 지원되는 키워드

  • 필드명이 바뀐 경우 메서드 이름도 함께 변경
  • 짧은 쿼리를 사용하는 경우 주로 사용
  • 식별하기 위한 내용(설명) 추가 가능 ex) findSliceMemberByAge

NamedQuery

쿼리에 이름을 부여하고 호출하는 기능

복사
@Entity
@NamedQuery(
      name="Member.findByUsername",
      query="select m from Member m where m.username = :username")
public class Member {
  ...
}

엔티티에 @NamedQuery로 작성한 SQL 가져와 사용

복사
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
  • 스프링 데이터 JPA는 선언한 “도메인 클래스 + .(점) + 메서드 이름”으로 Named 쿼리를 찾아서 실행
  • 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략 사용

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

// DTO로 직접 조회
@Query(
  "select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
  "from Member m join m.team t")
List<MemberDto> findMemberDto();
  • JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견 가능
  • DTO로 직접 조회 시 new 명령어 사용
  • 파라미터 바인딩, 컬렉션 파라미터 바인딩 가능

반환 타입

유연한 반환 타입 지원

Spring Data JPA return type 문서

복사
//컬렉션
List<Member> findByUsername(String name);

//단건
Member findByUsername(String name);

//단건 Optional
Optional<Member> findByUsername(String name);

컬렉션

  • 결과 없음: 빈 컬렉션 반환

단건 조회

  • 결과 없음: null 반환 -> 예외 발생 시 무시하고 null 반환
  • 결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생

페이징과 정렬

페이징 객체

  • org.springframework.data.domain.Pageable : 패이징 기능 (내부에 Sort객체 포함)

반환 타입

  • Page : count 쿼리 결과를 포함하는 페이징
  • Slice : count 쿼리 없이 다음 페이지만 확인 (limit + 1 조회)
  • List(자바 컬렉션) : count없이 결과만 반환
복사
// repository
Page<Member> findPageByUsername(String name, Pageable pageable);
Slice<Member> findSliceByUsername(String name, Pageable pageable);
List<Member> findListByUsername(String name, Pageable pageable);

count를 포함하는 경우 성능상 이슈 발생 가능 ex) join이 많을 경우
-> @Query를 사용해 count 쿼리 분리 가능

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

벌크성 수정 쿼리

@Modifying 어노테이션 추가, 영속성 컨텍스트 초기화(clearAutomatically true 옵션, em.clear())

복사
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkUpdateAgePlus(@Param("age") int age);
  1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산 먼저 실행
  2. 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트 초기화

@EntityGraph

연관된 엔티티들을 SQL 한번에 조회하는 방법 -> 패치 조인 사용 JPQL 없이 패치 조인

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

// 메서드 이름에서 사용
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)

JPA Hint

@QueryHints 어노테이션 사용해 readOnly 속성 사용

JPA도 @Lock 어노테이션으로 트랜잭션 처리의 순차성을 보장하기 위해 DB Lock 지원

관련된 내용은 추가로 정리 예정

사용자 정의 인터페이스

  1. Custom interface 생성
  2. Custom interface 을 상속받은 사용자 정의 구현 클래스 생성
  3. 기존 Repository Interface에 Custom interface를 상속 받는다.

사용자 정의 구현 클래스

  • 규칙: 리포지토리 인터페이스 이름 + Impl ex) MemberRepositoryImpl
  • 2.x 버전부터는 사용자 정의 인터페이스 명 + Impl 지원 ex) MemberRepositoryCustomImpl
  • 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
    (따로 Bean 관련 어노테이션 붙이지 않아도 된다)

다른 이름으로 적용하고 싶으면 설정 변경

복사
@EnableJpaRepositories(basePackages = "study.datajpa.repository",
                           repositoryImplementationPostfix = "Impl")

Custom 기능은 확장하는 기능으로 관리의 복잡도가 높아질 수 있음
-> 레포지토리를 기능에 따라 분리해 작성

핵심은

  1. 쿼리와 커맨드 분리
  2. 핵심 비즈니스 로직 / 아닌 부분 분리

Auditing

엔티티 생성, 변경할 때 변경한 사람과 시간을 추적하는데 사용 보통 1) 등록일, 2)수정일 -기본 / 추가로 3)등록자, 4)수정자(로그인 한 세션정보 기반)

순수 JPA 사용, Spring JPA 사용 2가지 방법 존재

Spring JPA 사용 방법

  1. @EnableJpaAuditing 어노테이션 스프링 부트 설정 클래스에 적용
  2. @EntityListeners(AuditingEntityListener.class) 어노테이션 엔티티에 적용
  3. 공통 매핑 정보 사용을 위한 @MappedSuperclass 어노테이션 엔티티에 적용

관련 어노테이션

복사
@CreatedDate
@LastModifiedDate
@CreatedBy
@LastModifiedBy

Domain Class Converter

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

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

도메인 클래스 컨버터로 엔티티를 파라미터로 받으면 단순 조회용으로 사용
-> 트랜잭션이 없는 범위에서 엔티티를 조회하기 때문 (권장x)

페이징, 정렬

Pageable객체를 파라미터로 받아 페이징, 정렬 기능 사용 가능

  • org.springframework.data.domain.PageRequest tkdyd
  • 사용 파라미터 : page(0부터 시작), size, sort

설정 방법

글로벌 설정 - 스프링 부트 설정

복사
# Application.yml
data:
  web:
    pageable:
      default-page-size: 10 # 기본 페이지 사이즈
      max-page-size: 2000 # 최대 페이지 사이즈

개별 설정 - @PageableDefault 어노테이션 사용

복사
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "username",
                    direction = Sort.Direction.DESC) Pageable pageable) {
  ...
}

멀티 페이징 시

페이징 정보가 둘이상이면 접두사로 구분 -> @Qualifier에 접두사 추가

ex) /members?member_page=0&order_page=1

복사
public String list (
  @Qualifier("member") Pageable memberPageable,
  @Qualifier("order") Pageable orderPageable,
)

Page 1부터 시작 시

  • 직접 클래스 만들어 변환 후 PageRequest 생성 -> 레포지토리에서 사용
  • 응답도 Page 대신 직접 만들어 제공

스프링 데이터 JPA 구현체

org.springframework.data.jpa.repository.support.SimpleJpaRepository

  • 구현체를 살펴보면 class레벨에 @Repository, @Transactional(readOnly = true) 적용
  • save() 같은 경우 readOnly false (default)

readOnly

기본값은 false 이며 true 로 세팅하는 경우 트랜잭션을 읽기 전용으로 변경

  • 읽기 전용 트랜잭션 내에서 INSERT, UPDATE, DELETE 작업을 해도 반영x
  • 성능 향상을 위해 사용하거나 읽기 외 다른 동작 방지위해 사용

Dirty Checking 무시

개발자가 임의로 UPDATE 쿼리를 사용하지 않아도 트랜잭션 커밋 시에 1차 캐시에 저장되어 있는 Entity 와 스냅샷을 비교해서 변경된 부분이 있으면 UPDATE 쿼리를 날려주는 기능

-> readOnly=true 설정 시 스프링 프레임워크가 Hibernate의 FlushMode를 MANUAL로 설정
-> 플러시 할 때 일어나는 스냅샷 비교와 같은 무거운 로직 수행x

새로운 엔티티 구분

save() 메서드는

  • 새로운 엔티티면 저장 - em.persist()
  • 새로운 엔티티가 아니면 병합 - em.merge()

객체일 때 null로 판단
자바 기본 타입일 때 0으로 판단
-> 식별자 생성 전략이 @Id만 사용해 직접 할당할 때 문제 발생

Persistable 인터페이스 구현해 새로운 엔티티 판단
-> 새로운 엔티티인지 확인하는 isNew 함수 구현 -> @CreatedDate 조합하면 편리하게 확인 가능

복사
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable {
    @Id
    private String id;

    @CreatedDate
    private LocalDateTime createdDate;

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


    @Override
    public Object getId() {
        return id;
    }

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

Tip!!

  • gradle 의존관계 확인 command
복사
./gradlew dependencies --configuration compileClasspath

@Inheritance -> 다형성을 위한 상속 (상속 관계 매핑)
@Embedded, @Embeddable -> 합성 (복합 값 타입 사용 시)
@MappedSuperclass -> 객체의 입장에서 공통 매핑 정보가 필요할 때 사용

Reference

Previous Post
실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화
Next Post
KMP 알고리즘
Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.