개발/Java&Kotlin

[JPA] 스프링 데이터 - 확장 기능

devhooney 2022. 7. 28. 08:53
728x90

김영한님의 실전! 스프링 데이터 JPA 정리

 

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

- 스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성

- 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많음

- 다양한 이유로 인터페이스의 메소드를 직접 구현하고 싶다면?

(1) JPA 직접 사용(EntityManager)

(2) 스프링 JDBC Template 사용

(3) MyBatis 사용

(4) 데이터베이스 커넥션 직접 사용 등등..

(5) Querydsl 사용

 

- 사용자 정의 인터페이스

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

- 사용자 정의 인터페이스 구현 클래스

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
 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>, MemberRepositoryCustom {

}

 

- 사용자 정의 구현 클래스 규칙

(1) 규칙: 리포지토리 인터페이스 이름 + Impl ***

 

(2) 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
참고: 실무에서는 주로 querydsl이나 springjdbctemplate을 함께 사용할 때 사용자 정의 리포지토리 기능 자주 사용
참고: 항상 사용자 정의 리포지토리가 필요한 것은 아니다. 그냥 임의의 리포지토리를 만들어도 된다.
예를 들어 MembewrQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 그냥 직접 사용해도 된다. 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 동작한다.

 

(3) 사용자 정의 리포지토리 구현 최신 방식

스프링 데이터 2.x부터는 사용자 정의 구현 클래스에 리포지토리 인터페이스 이름 + Impl을 적용하는 대신에 사용자 정의 인터페이스명 + Impl방식도 지원한다.

예를 들어 위 예제의 MemberRepositoryImpl 대신에 MemberRepositoryCustomImpl같이 구현해도 된다.

 

2. Auditing

- 엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶으면?

(1) 등록일

(2) 수정일

(3) 등록자

(4) 수정자

 

- 스프링 데이터 JPA 사용

(1) 설정

> @EnableJpaAuditing 스프링 부트 설정 클래스에 적용해야함
> @EntityListeners(AuditingEntityListener.class) 엔티티에 적용

 

(2) 사용 어노테이션

> @CreatedDate

> @LastModifiedDate

> @CreatedBy

> @LastModifiedBy

 

(3) 등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록

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

 

(4) 실무에서는 세션 정보나, 스프링 시큐리티 로그인 정보에서 ID받음

참고: 실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다. 그래서 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;
}

 

(5) 전체 적용

@EntityListeners(AuditingEntityListener.class)를 생략하고 스프링 데이터 JPA가 제공하는 이벤트를 엔티티 전체에 적용하려면 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>

 

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

// 도메인 클래스 컨버터 사용 후
@RestController
@RequiredArgsConstructor
public class MemberController {
 
 private final MemberRepository memberRepository;
 
 @GetMapping("/members/{id}")
 public String findMember(@PathVariable("id") Member member) {
 	return member.getUsername();
 }
}

- HTTP요청은 회원 id를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환

- 도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾음

주의: 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야한다.
(트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)

 

4. Web 확장 - 페이징과 정렬

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

- 페이징과 정렬 예제

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

(1) 파라미터로 Pageable을 받을 수 있다.

(2) Pageable은 인터페이스, 실제는 org.springframework.data.domain.PageRequest 객체 생성

(3) 요청 파라미터

> 예) /members?page=0&size=3&sort=id,desc&sort=username,desc

> page: 현재 페이지, 0부터 시작한다.

> size: 한 페이지에 노출할 데이터 건수

> sort: 정렬 조건을 정의한다. 예) 정렬 속성, 정렬 속성...(ASC|DESC), 정렬 방향을 변경하고 싶으면 sort파라미터 추가(asc 생략가능)

(4) 기본값

> 글로벌 설정: 스프링 부트

spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/

> 개별 설정

@PagealbeDefault 어노테이션을 사용

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

(5) 접두사

> 페이징 정보가 둘 이상이면 접두사로 구분

> @Qualifier에 접두사명 추가 "{접두사명}_xxx"

예제: /members?member_page=0&order_page=1

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

(6) Page 내용을 DTO로 변환하기

> 엔티티를 API로 노출하면 다양한 문제가 발생.

> 엔티티를 꼭 DTO로 변환해서 반환!

> Page는 map()을 지원해서 내부 데이터를 다른 것으로 변경할 수 있다.

(7) Page를 1부터 시작하기

> 스프링 데이터는 Page를 0부터 시작

> 만약 1부터 시작하려면?

# Pageable, Page를 파라미터와 응답 값으로 사용하지 않고, 직접 클래스를 만들어서 처리한다. 그리고 직접 PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘긴다. 물론 응답값도 Page 대신에 직접 만들어서 제공해야 한다.

# spring.data.web.pageable.one-indexed-parameters를 true로 설정
이 방법은 web에서 page파라미터를 -1 처리할 뿐
따라서 응답값인 Page에 모두 0페이지 인덱스를 사용하는 한계가 있다.

728x90