확장 기능
📌 사용자 정의 리포지토리 구현
스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동으로 생성한다.
스프링 데이터 JPA는 인터페이스로 되어있기 때문에 상속받아 구현을 하게 되면 모든 기능을 다 구현해야 한다. → 현실적으로 불가능
특정 기능에 JPA가 아닌 다른 데이터베이스 커넥션 방법을 사용할 수 있게 사용자 정의 리포지토리 기능을 JPA가 제공한다.
- JPA 직접 사용(EntityManager)
- 스프링 JDBC Template 사용
- Mybatis 사용
- 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 {
// ...
}
- 사용자 정의 인터페이스를 스프링 데이터 JPA 인터페이스에 상속해 주면 스프링 데이터 JPA가 인식하여 구현체를 스프링 빈으로 자동 등록해 준다.
사용자 정의 구현 클래스명 규칙
- 사용자 정의 구현클래스명에는 규칙이 존재한다.
- 리포지토리 인터페이스 명 + Impl
만약 Impl 대신 다른 이름으로 변경하고 싶다면
<!-- XML -->
<repositories base-package="study.datajpa.repository" repository-impl-postfix="Impl" />
// JavaConfig
@EnableJpaRepositories(basePackages = "study.datajpa.repository",
repositoryImplementationPostfix = "Impl")
가급적 설정을 변경하지 않고, 관례를 따라 사용자 정의 구현 클래스명 규칙을 지키는 것이 좋다. 유지보수 측면에서 혼란이 생길 가능성이 있다.
참고 : 실무에서는 주로 QueryDSL이나 SpringJdbcTemplate을 함께 사용할 때 사용자 리포지토리 기능을 자주 사용
참고 : 항상 사용자 정의 리포지토리가 필요한 것은 아니다. 그냥 임의의 리포지토리를 만들어도 된다.
예를 들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링빈으로 등록해서 직접 사용해도 된다.
물론 이 경우 스프링 데이터 JPA와는 관계없이 별도로 동작한다.
✔️사용자 정의 인터페이스 구현 최신 방식
: 스프링 데이터 2.x부터는 사용자 정의 구현 클래스에 리포지토리 인터페이스 명 + Impl을 적용하는 대신 사용자 정의 인터페이스 명 + Impl 방식도 지원한다.
예를 들어 위 예제의 MemberRepositroyImpl 대신 MemberRepositoryCustomImpl 같이 구현해도 된다.
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
- 기존 방식보다 이 방식이 사용자 정의 인터페이스 이름과 구현 클래스 이름이 비슷하므로 더 직관적이다.
- 추가로 여러 인터페이스를 분리해서 구현하는 것도 가능하기 때문에 새롭게 변경된 이 방식을 더 권장한다.
📌 Auditing
실무에서는 데이터의 추적을 위해 로그 데이터를 필수로 저장한다.
데이터베이스의 테이블이라면 등록·수정일, 등록·수정자와 같은 로그 데이터를 모든 컬럼을 추가해야 하지만 객체에서는 모든 엔티티에 필드를 추가하지 않고 상속받아 자동화할 수 있다.
스프링 데이터 Auditing 적용 - 등록일, 수정일
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
- 등록일 필드에 @CreateDate 적용 및 수정 시 데이터가 변경되지 않도록 @Colume(updatable = false) 설정
- 수정일 필드에 @LastModifiedDate적용
- @EntityListeners(AuditingEntityListener.class)를 사용할 @MappedSuperClass에 적용한다.
→ 추가로 스프링부트 설정 클래스에 @EnableJpaAuditing적용이 필요. 적용 안 하면 동작하지 않음
@EnableJpaAuditing // <<< 적용 필요
@SpringBootApplication
public class DataJpaApplication {
//...
}
스프링 데이터 Auditing 적용 - 등록자, 수정자
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime updatedDate;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
@Column
private String updatedBy;
}
- 등록자 필드에 @CreatedBy 적용 및 수정 시 데이터가 변경되지 않도록 @Colume(updatable = false) 설정
- 수정자 필드에 @LastModifiedBy 적용
- config 클래스에 는 AuditorAware 스프링 빈 등록 필요
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
//...
@Bean
public AuditorAware<String> auditorProvider(){
return () -> Optional.of(UUID.randomUUID().toString());
}
}
- 실무에서는 세션 정보나 스프링 시큐리티 로그인 정보에서 ID를 받는다.
등록·수정일, 등록·수정자을 분리 해서 사용
실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다. 그래서 다음과 같이 Base 타입을 분리하고, 원하는 타입을 선택해서 상속한다.
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createBy;
@LastModifiedBy
private String lastModifiedBy;
}
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
- 자주 사용하는 등록·수정일을 BaseTimeEntity와 등록·수정자를 BaseEntity로 분리하고 BaseEntity에서 BaseTimeEntity를 상속받도록 설계한다.
- 최초 등록 시 수정일, 수정자를 null로 설정하고 싶을 경우 config 클래스에 @EnableJpaAuditing(modifyOnCreate = false) 옵션을 사용하면 된다.
📌 Web 확장 - 도메인 클래스 컨버터
스프링 데이터 JPA를 이용하면 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩할 수 있다.
도메인 클래스 컨버터 사용 전
@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
Member findMember = memberRepository.findById(id).get();
return findMember.getUsername();
}
도메인 클래스 컨버터 사용 후
@GetMapping("/members2/{id}")
public String findMember2(@PathVariable("id") Member member) {
return member.getUsername();
}
- 도메인 클래스 컨버터 사용 전과 후의 결과는 같다.
- 기존에는 request 파라미터로 엔티티 id를 받으면 id를 통해 DB에서 조회한 엔티티를 반환했다.
- 도메인 클래스 컨버터 기능을 이용하면, HTTP 요청은 회원 id를 받지만 중간에 도메인 클래스 컨버터가 동작해서 회원 엔티티 객체를 반환한다.
- 도메인 클래스 컨버터도 리포지토리를 사용해서 엔티티를 찾는다.
주의 : 도메인 클래스 컨버터로 받은 엔티티는 트랜잭션이 없는 범위에서 조회했기 때문에 엔티티를 변경해도 DB에 반영되지 않는다. 이 엔티티는 단순 조회용으로만 사용해야 한다.
📌 Web 확장 - 페이징과 정렬
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다.
페이징과 정렬 예제
@GetMapping("/members")
public Page<MemberDTO> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page.map(MemberDTO::new);
}
- 파라미터로 Pageable을 받을 수 있다.
- Pageable 은 인터페이스, 실제는 org.springframework.data.domain.PageRequest 객체 생성
요청 파라미터
- ?page=0&size=3&sort=id,desc&sort=username,desc
- page : 현재 페이지, 0부터 시작
- size : 한 페이지에 노출할 데이터 건수
- sort : 정렬 조건 desc or asc(asc 생략 가능)
페이징 글로벌 설정
<!-- XML -->
spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
페이징 개별 설정
@GetMapping("/members")
public Page<MemberDTO> list(@PageableDefault(size=5) Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page.map(MemberDTO::new);
}
- @PageableDefault 어노테이션을 사용해 개별 설정할 수 있다.
- size = value (한 페이지에 노출할 데이터 건수)
- sort = value (정렬 데이터)
- direction = Sort.Direction.DESC or ASC (정렬 기준)
접두사
- 페이징 정보다 둘 이상하면 접두사로 구분한다.
- @Qualifier에 접두사명 추가 "{접두사명}_xxx"
- 예제: /members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...){
// ...
}
Page 내용을 DTO로 변환하기
- 엔티티를 API로 노출하면 다양한 문제가 발생한다. 그래서 엔티티를 꼭 DTO로 변환해서 반환해야 한다.
- Page는 map()을 지원해서 내부 데이터를 다른 것으로 변경할 수 있다.
@GetMapping("/members")
public Page<MemberDTO> list(@PageableDefault(size=5) Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page.map(MemberDTO::new);
}
@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;
}
public MemberDTO(Member member) {
this.id = member.getId();
this.username = member.getUsername();
}
}
Page를 1부터 시작하기
- 스프링 데이터는 Page를 0부터 시작한다.
- 1부터 시작하는 방법은 2가지가 있다.
- Pageable, Page를 파라미터와 응답 값으로 사용하지 않고, 직접 클래스를 만들어서 처리한다. 그리고 직접 PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘긴다.
물론 응답 값도 Page 대신 직접 만들어서 제공해야 한다. - spring.data.web.pageable.one-indexed-parameters를 true로 설정한다.
이 방법은 web에서 page 파라미터를 -1 처리할 뿐이다. 따라서 응답값인 Page에 모두 0 페이지 인덱스를 사용하는 한계가 있다.
- 실제로 page 파라미터가 +1 되어 동작하지만 반환되는 페이지 인덱스는 변경되지 않는 것을 확인할 수 있다.
해당 글은 인프런의 [실전! 스프링 데이터 JPA] 강의를 정리한 내용입니다.
실전! 스프링 데이터 JPA - 인프런 | 강의
스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.
www.inflearn.com
'Spring > Spring Data JPA' 카테고리의 다른 글
[Spring Data JPA] 5. 스프링 데이터 JPA 분석 (0) | 2023.05.22 |
---|---|
[Spring Data JPA] 3. 쿼리 메소드 기능 (0) | 2023.05.19 |
[Spring Data JPA] 2. 공통 인터페이스 기능 (0) | 2023.05.17 |
[Spring Data JPA] 1. 예제 도메인 모델 (0) | 2023.05.17 |
댓글