※ 본문은 김영한 선생님의 인프런 '실전! 스프링 데이터 JPA' 강의를 듣고 정리한 내용임을 알립니다.
▶ 스프링 데이터 JPA 공통 인터페이스
- JpaRepository 인터페이스
- 공통 CRUD 제공
- 제너릭 <Entity 타입, 식별자 타입> 설정
- 제너릭 타입
- T : Entity
- ID : Entity의 식별자 타입
- S : Entity와 그 자식 타입
- 주요 메소드
- save(S) : 새로운 Entity는 persist / 이미 있는 Entity는 merge
- delete(T) : Entity를 하나 삭제함 / 내부에서 em.remove() 호출
- findById(ID) : Entity 하나를 조회 / 내부에서 em.find() 호출
- getOne(ID) : Entity를 Proxy로 조회 / 내부에서 em.getReference() 호출
- findAll(...) : 모든 Entity 조회 / 정렬(Sort)이나 페이징(Pageable) 조건을 파라미터로 제공 가능
※ 메소드를 더 알고 싶다면 JpaRepository 인터페이스의 메소드들을 살펴보도록 하자.
★ save(S) 주의사항 : 실무에서 merge를 사용할 일은 거의 없음
→ 모종의 이유로 @Id @GeneratedValue를 사용하지 않고 @Id만 사용해야 할 때 merge하는 문제가 발생
→ Persistable을 상속받고, @CreatedDate를 활용해서 isNew() 메소드를 직접 만들어주면 해결 가능
▶ 쿼리 메소드
- 쿼리 메소드의 3가지 방법
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
- @Query 어노테이션을 사용해서 Repository Interface에 쿼리 직접 정의
1. 메소드 이름으로 쿼리 생성
: 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
- 조회 : find...By, read...By, query...By, get...By
- COUNT : count...By 반환타입 long
- EXISTS : exists...By 반환타입 boolean
- 삭제 : delete...By, remove...By 반환타입 long
- DISTINCT : findDistinct, find MemberDistinctBy
- LIMIT : findFirst3, findFirst, findTop, findTop3
※ 메소드 이름으로 조회 쿼리 :
docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
※ Entity의 필드명이 변경되면 인터페이스에 정의한 메소드 이름도 꼭 변경해야 함
→ 그렇지 않으면 컴파일 시점에 오류가 발생
→ 이렇듯 컴파일 에러 단계에서 오류를 잡아낼 수 있다는 것이 스프링 데이터 JPA의 장점!
2. JPA NamedQuery
★ 실무에서 쓸 일이 없음
- 사용 방법 : Entity에 @NamedQuery 어노테이션 사용
→ JpaRepository에서 @Query의 name 속성에 @NamedQuery의 name 입력 / 혹은 메소드 이름만으로 호출
→ 쿼리들을 미리 파싱해보기 때문에 컴파일 에러를 잡아낼 수 있음
3. @Query 어노테이션으로 Repository에 쿼리 직접 정의
@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);
- JPQL을 바로 쓰면서 메소드 이름을 줄일 수 있음
→ 첫번째 방법의 단점을 보완하면서, 두번째 방법보다 편리함
→ 두번째 방법과 같이 컴파일 에러를 잡아낼 수 있음
→ 값 하나를 조회하거나 DTO를 조회하는 방식도 가능
→ 컬렉션 파라미터 바인딩 : Collection 타입으로 in절 지원
※ 이 밖에 동적쿼리 같은 경우는 Querydsl 사용
※ 반환 타입
: 스프링 데이터 JPA는 유연한 반환 타입을 지원
→ List<Member> / Member / Optional<Member>, Page<Member> 등
→ 컬렉션의 경우, 결과가 없으면 빈 컬렉션 반환
→ 단건 조회의 경우, 결과가 없으면 null 반환 / 결과가 2건 이상이면 NonUniqueResultException 예외 발생
▶ 페이징과 정렬
- 페이징과 정렬 파라미터
- 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 조회)
※ Slice를 사용하는 경우의 예시 : 모바일에서 리스트 밑바닥에 닿으면 다음 리스트를 불러오는 기능
- Controller에서 Page 사용
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
- 파라미터로 Pageable을 받을 수 있음
- Pageable은 인터페이스이며, 실제로는 org.springframework.data.domain.PageRequest 객체 생성
- 요청 파라미터 예시 : /members?page=0&size=3&sort=id,desc&sort=username,desc
- page : 현재 페이지 / 0부터 시작
- size : 한 페이지에 노출할 데이터 건수
- sort : 정렬 조건을 정의
- 사이즈 기본값 : spring.data.web.pageable.default-page-size=20
- 최대 페이지 기본값 : spring.data.web.pageable.max-page-size=2000
- 글로벌 설정 방법 : application.yaml에서 default-page-size와 max-page-siez 수정
- 개별 설정 방법 : @PageableDefault 어노테이션 사용
- 접두사
- 페이징 정보가 둘 이상이면 접두사로 구분 가능
- @Qualifier에 접두사명 추가
- URI 예시 : /members?member_page=0&order_page=1
- 코드 예시 : @Qualifier("member") Pageable memberPageable, @Qualifier("order") Pageable orderPageable
- Page 내용을 DTO로 변환
★ Entity를 외부에 노출하는 것은 매우 위험하므호 반드시 DTO로 변환해야 함
- Page를 1부터 시작하기
방법1. Pageable, Page 직접 정의
→ 직접 PageRequest(Pageable 구현체)를 생성해서 Repository에 넘기기
→ 응답값도 Page 대신 직접 만들어서 제공해야 함
방법2. yml에서 spring.data.web.pageable.one-indexed-parameters: true 설정
→ 하지만 이 방법은 web에서 page 파라미터를 -1 처리를 할 뿐
=> 페이지를 0부터 시작하는게 깔끔하다.
▶ 벌크성 수정 쿼리
→ Repository에서 @Modifying 어노테이션을 사용해야 함
★ 벌크 연산은 영속성 컨텍스트를 무시하고 실행
→ 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화해야 함
→ @Modifying(clearAutomatically = true) 설정
▶ @EntityGraph
: 연관된 Entity들을 SQL 한번에 조회하는 방법
- 사실상 fetch join의 간편 버전 → JPQL로 처리하지 않고 사용할 수 있음
- left outer join 사용
- 적용 예시 : @EntityGraph(attributePaths = {"team"})
▶ JPA Hint & Lock
- JPA Hint
: JPA 쿼리 힌트 (SQL 힌트가 아닌 JPA 구현체에게 제공하는 힌트)
: @QueryHints 어노테이션 활용
※ 사실 성능 상에서 크게 이점을 볼 수 없다.
- JPA Lock
: @Lock 어노테이션 활용
예시) 고객이 주문 완료 하고 사장님이 접수를 했지만 고객이 주문을 취소하려는 경우에 Lock이 필요할지도 모른다.
→ 하지만 밀리초 단위에 동시에 같은 데이터를 변경할 확률은 매우 낮으며 실시간 애플리케이션에서 위험이 따른다.
+ 락을 풀어두어서 발생하는 이슈보다 락을 걸어서 발생하는 이슈가 더 많다.
※ 내용이 복잡해서 책을 참고하자. 마찬가지로 실무에서 크게 쓸 일이 없다.
▶ 사용자 정의 Repository
- 스프링 데이터 JPA의 Repository는 인터페이스만 정의하고 구현체는 스프링이 자동 생성
→ 하지만 여러가지 사정으로 인터페이스의 메소드를 직접 구현하고 싶다면?
- JPA 직접 사용 (EntityManager)
- Spring JdbcTemplate
- MyBatis 사용
- DB Connection 직접 사용
- Querydsl 사용
- 사용자 정의 구현 클래스
- 규칙 : 사용자 정의 인터페이스 이름 + Impl
- 규칙대로 생성한 클래스를 기존 JpaRepository에 추가적으로 extends
- JpaRepository에서 인터페이스 이름이나 쿼리만으로 해결되지 않을 때 사용
- Querydsl / Spring JdbcTemplate을 함께 사용할 때 자주 사용
★ 사용자 정의 Repository는 항상 필요하지는 않다.
※ 실무에서는 핵심 비즈니스와 간단한 화면 비즈니스, 두 Repository를 분리하는 게 낫다.
▶ Auditing
Q. Entity를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶다면? (등록일 / 수정일 / 등록자 / 수정자)
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
- 실무에서는 모든 테이블에 다 깔아두는 것이 좋음 (문제가 발생하면 추적하기 위해서)
- Application 클래스에 @EnableJpaAuditing이 있어야 함
- 실무에서 사용 시, 생성 시간 / 변경 시간은 다 필요하지만 등록자 / 수정자는 애매할 수 있음
→ BaseTimeEntity를 최상위에 두고 BaseEntity가 상속받는 식으로 사용하는 것이 좋다.
▶ Projections
: Entity 대신에 DTO를 편리하게 조회할 때 사용
: query의 select 절에 들어갈 필드들이라고 생각하면 쉬움
ex) 전체 Entity가 아닌 회원 이름만 조회하고 싶을 때
- 조회할 Entity의 필드를 getter 형식으로 지정하면 해당 필드만 선택해서 조회
- 프로젝션 대상이 root Entity면 유용하게 사용하는 기능
- 실무의 복잡한 쿼리를 해결하기에는 한계가 있음
- 정적 쿼리를 Native로 짜야 할 때는 @Query를 조합해서 사용할 수도 있음
'JVM > JPA' 카테고리의 다른 글
Querydsl (0) | 2021.05.01 |
---|---|
객체지향 쿼리 언어 (JPQL) (0) | 2021.04.18 |
값 타입 (0) | 2021.04.16 |
Proxy와 연관관계 관리 (0) | 2021.04.16 |
Entity Mapping (0) | 2021.04.15 |