Search

[강의 요약] 김영한 - 스프링 데이터 JPA

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
복사
메서드 키워드
주요 키워드
COUNT: count...By
반환타입 long
EXISTS: exists...By
반환타입 boolean
삭제: delete...By, remove...By
반환타입 long
DISTINCT: findDistinct, findMemberDistinctBy

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 쓰는 것이 나음
네이티브 쿼리
동적쿼리 불가 등 제약이 많음 → 가급적 사용 자제