Last update: @5/22/2023
주의
본 포스팅은 인프런 강의를 통해 학습한 내용을 임의로 요약한 것으로 일부 내용의 오류 및 누락, 링크 숨김 등이 존재합니다.
Spring Data JPA는 JPA 사용 시 반복되는 코드를 공통화하고 페이징 등 기타 기능들을 편리하게 사용할 수 있도록 지원해주는 기술
기본 사용법
•
JpaRepository<Entity, IdType> 인터페이스를 상속한 인터페이스 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
}
Java
복사
◦
Spring Data JPA는 위 인터페이스의 구현체를 만들어 빈으로 등록해줌
◦
등록된 빈을 통해 기본적인 쿼리들 사용 가능
•
스프링 부트 사용하지 않을 시 아래 설정
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}
Java
복사
•
◦
save(S)
▪
새로운 엔티티일 경우 EntityManager.save()
▪
새로운 엔티티가 아닐 경우 merge()
◦
delete(T)
▪
EntityManager.delete()
◦
findById(ID)
▪
EntityManager.find()
◦
getOne(ID)
▪
EntityManager.getReference()
◦
findAll([Sort], [Pageable])
쿼리 메서드
•
인터페이스의 메서드 이름을 분석해 JPQL 쿼리문을 생성하고 실행
•
예시
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
Java
복사
Named Query
•
쿼리를 미리 정의해 이름을 붙여놓은 것
•
실무에서 거의 쓸 일 없음
@Entity
@NamedQuery(
name = "Member.findByUserName",
query = "select m from Member m where m.username = :username"
)
public class Member {...}
Java
복사
public interface MemberRepository extends JpaRepository<Member, Long>
@Query(name = "Member.findByUsername") // 메서드 이름이 같으면 생략 가능
List<Member> findByUsername(@Param("username") String username);
}
Java
복사
@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);
}
Java
복사
•
동적 쿼리는 QueryDSL 사용 권장
•
단순 값 조회
@Query("select m.username from Member m")
List<String> findUsernameList();
Java
복사
◦
JPA 값 타입(@Embedded)도 이 방법으로 조회 가능
•
DTO 조회 (프로젝션)
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
List<MemberDto> findMemberDto();
Java
복사
◦
DTO에 아래처럼 순서가 맞는 생성자 필수
@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;
}
}
Java
복사
파라미터 바인딩
•
@Param 애노테이션을 통해 파라미터를 바인딩할 수 있음
import org.springframework.data.repository.query.Param
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :name")
Member findMembers(@Param("name") String username);
}
Java
복사
◦
이름 기반 파라미터 바인딩 사용 권장
•
컬렉션 파라미터 바인딩
◦
Collection 타입으로 in절 지원
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") Collection<String> names);
Java
복사
반환 타입
•
스프링 데이터 JPA는 반환 타입을 유연하게 제공
List<Member> findListByUsername(String username); // 컬렉션
Member findMemberByUsername(String username); // 단건
Optional<Member> findOptionalByUsername(String username); // 단건 Optional
Java
복사
•
결과가 없거나 예상보다 더 많은 경우
◦
컬렉션
▪
결과 없음: 빈 컬렉션 반환
◦
단건 조회
▪
결과 없음: null 반환 (순수 JPA는 NoResultException 예외가 터지는데, 이를 변환)
▪
결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생
페이징 및 정렬 지원
•
Pageable 및 Sort 객체를 파라미터로 받는 메서드 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable); //count 쿼리 사용
Slice<Member> findSliceByAge(int age, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort);
}
Java
복사
•
사용 예제
◦
PageRequest 객체를 만들어서 페이지(0부터 시작) 및 페이지 사이즈, 정렬 기준을 넘겨 파라미터로 전달
PageRequest pageRequest =
PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findByAge(age, pageRequest);
// 엔티티 직접 꺼내기
List<Member> content = page.getContent();
// DTO로 매핑해서 꺼내기
// Page 객체를 그대로 JSON으로 반환하면 내부 페이징 관련 정보도 JSON으로 전달되어 좋음
Page<MemberDto> memberDtos =
page.map(member -> new MemberDto(member.getId(), member.getUsername(), member.getTeam().getName()));
Java
복사
•
Slice
◦
Slice는 무한 스크롤 구현 시 사용하는 페이징 객체로, getTotalPages()와 getTotalElements() 기능이 없음
◦
limit + 1개의 객체를 조회함
•
count 쿼리 분리 (복잡한 SQL에서 count 시 불필요한 JOIN을 줄이기 위해 사용)
@Query(value = “select m from Member m”,
countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);
Java
복사
•
Page.map()을 이용해서 DTO로 변환하기 예제
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
Java
복사
벌크 수정 쿼리
•
대량의 데이터를 수정하기 위해 DB에 SQL을 보내는 경우 @Modifiying 애노테이션 부착
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
Java
복사
◦
수정된 데이터의 개수를 반환함
◦
쿼리 실행 시 영속성 컨텍스트는 무시되기 때문에 영속성 컨텍스트 초기화 필요
▪
clearAutomatically = true 옵션 권장 (default false)
@EntityGraph
•
지연로딩을 설정한 경우, 페치 조인을 간편하게 도와주는 애노테이션
//공통 메서드 오버라이드
@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);
Java
복사
쿼리 힌트
•
JPA 구현체에게 힌트를 제공해서 추가 기능을 사용할 수 있음
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
Java
복사
◦
위처럼 가져온 엔티티는 수정이 불가
Lock
•
DB 락 모드 설정 가능
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
Java
복사
사용자 정의 리포지토리 구현 (스프링 데이터 2.x 이후부터)
1.
사용자 정의 리포지토리 인터페이스 생성
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
Java
복사
2.
구현 클래스 생성
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
Java
복사
•
인터페이스 이름 + Impl을 클래스 이름으로 설정
3.
스프링 데이터 JPA 클래스에서 사용자 정의 인터페이스 상속
public interface MemberRepository
extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
Java
복사
•
사용
List<Member> result = memberRepository.findMemberCustom();
Java
복사
•
사실 위처럼 인터페이스를 상속시켜 엮는 것 보다는 확실히 분리해서 만드는 것을 권장
Auditing
•
@CreatedDate와 @LastModifiedDate 등으로 엔티티 생성 및 수정 시 날짜와 수정한 사람 정보 저장
@EntityListeners(AuditingEntityListener.class)
@Getter
@MappedSuperclass
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;
}
Java
복사
◦
엔티티 클래스에 @EntityListeners(AuditingEntityListener.class) 부착 필수
◦
등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록
@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());
}
}
Java
복사
▪
예제는 UUID이지만 실무에서는 로그인 정보에서 ID 등을 받아서 설정
▪
메인 스프링 어플리케이션에 @EnableJpaAuditing 애노테이션 부착 필수
•
수정자가 필요 없는 테이블도 많기 때문에 분리가 필요함
◦
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;
}
Java
복사
@Entity
public class Member extends BaseTimeEntity {...}
Java
복사
•
위처럼 하면 저장 시점에 수정 정보도 등록 정보와 똑같이 저장되어 나중에 유용함
◦
수정 정보를 최초에 null로 저장하고 싶으면 @EnableJpaAuditing(modifyOnCreate = false) 사용
도메인 클래스 컨버터
•
id를 요청 파라미터로 받을 때 파라미터 타입의 엔티티를 DB에서 찾아주는 기능
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/members2/{id}")
public String findMember(
@PathVariable("id") Member member
) {
return member.getUsername();
}
}
Java
복사
◦
거의 사용하지 않음
Web 페이징 지원
•
컨트롤러 파라미터로 Pageable을 받아 사용 가능
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
Java
복사
•
요청 URL 예시
https://localhost:8080/members?page=0&size=3&sort=id,desc&sort=username,desc
Java
복사
◦
page: 0부터 시작
◦
size: 한 페이지 당 데이터 개수
▪
기본값 20
▪
글로벌 기본값 변경 시 설정파일에 아래 추가
spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
Java
복사
▪
개별 설정
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = “username”,
direction = Sort.Direction.DESC) Pageable pageable
) {...}
Java
복사
◦
sort: 정렬 조건. asc 생략 가능
Persistable
•
스프링 데이터 JPA의 save는 새로운 엔티티일 경우 save, 아니면 merge를 하는데, 이를 id 필드가 null 또는 0이면 새로운 엔티티로 봄. 이 검사 로직을 변경할 때 Persistable 인터페이스를 구현
package study.datajpa.entity;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.domain.Persistable;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.time.LocalDateTime;
@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
@Id
private String id;
@CreatedDate
private LocalDateTime createdDate;
public Item(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public boolean isNew() {
return createdDate == null;
}
}
Java
복사
◦
등록 시간을 조합해 새로운 엔티티 여부를 편리하게 판단하는 예시
기타 잘 사용하지 않는 기능들
•
Specifications
◦
Criteria 사용 → 복잡해서 사용 불가
•
Query By Example
◦
Entity로 검색 → 조금만 복잡해져도 사용 불가
•
Projections
◦
쓸만하지만 조금만 복잡해져도 QueryDSL 쓰는 것이 나음
•
네이티브 쿼리
◦
동적쿼리 불가 등 제약이 많음 → 가급적 사용 자제