실전! 스프링 데이터 JPA 수업을 듣고 정리한 내용입니다.
스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성해준다.
스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많다.
다양한 이유로 인터페이스의 메서드를 직접 구현하고 싶다면?
EntityManager
) JDBC Template
사용 MyBatis
사용 Querydsl
사용등의 방법이 있다.
스프링 JPA는 이런 경우를 위해서 사용자 정의 리포지토리 기능을 제공한다.
MemberRepositoryCustom - 사용자 정의 인터페이스
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
MemberRepositoryImpl - 사용자 정의 인터페이스 구현 클래스
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
✔️ 사용자 정의 구현 클래스
Impl
MemberRepository에 추가 - 사용자 정의 인터페이스 상속
public interface MemberRepository
extends JpaRepository<Member, Long>, MemberRepositoryCustom {}
MemberRepositoryCustom
을 상속받도록 추가해주자!
MemberRepositoryTest에 추가 - 사용자 정의 메서드 호출 코드
@Test
public void callCustom() {
List<Member> result = memberRepository.findMemberCustom();
}
✔️ 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
방식도 지원한다.- 예를 들어서 위 예제의
MemberRepositoryImpl
대신에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();
}
}
기존 방식보다 이 방식이 사용자 정의 인터페이스 이름과 구현 클래스 이름이 비슷하므로 더 직관적이다.
추가로 여러 인터페이스를 분리해서 구현하는 것도 가능하기 때문에 새롭게 변경된 이 방식을 사용하는 것을 더 권장한다.
협업할 때 엔티티 생성, 변경할 때 변경한 사람과 시간 기록을 남기는 것이 좋다.
- 등록일
- 수정일
- 등록자
- 수정자
JpaBaseEntity - 수정일, 등록일 적용
@MappedSuperclass
@Getter
public class JpaBaseEntity {
@Column(updatable = false)
private LocalDateTime createdDate;
private LocalDateTime updatedDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createdDate = now;
updatedDate = now;
}
@PreUpdate
public void preUpdate() {
updatedDate = LocalDateTime.now();
}
}
@MappedSuperclass
: JpaBaseEntity
를 상속받은 클래스는 속성만 상속받아 사용할 수 있다.
Member에 추가
public class Member extends JpaBaseEntity {}
MemberTest에 추가
@Test
public void JpaEventBaseEntity() throws Exception {
// given
Member member = new Member("member1");
memberRepository.save(member); // @PrePersist
Thread.sleep(100);
member.setUsername("member2");
em.flush(); // @PreUpdate
em.clear();
// when
Member findMember = memberRepository.findById(member.getId()).get();
// then
System.out.println("findMember.createDate = " + findMember.getCreatedDate());
System.out.println("findMember.updateDate = " + findMember.getUpdatedDate());
}
실행 결과
✏️ JPA 주요 이벤트 어노테이션
@PrePersist
: 해당 엔티티를 저장하기 이전@PostPersist
: 해당 엔티티를 저장한 이후@PreUpdate
: 해당 엔티티를 업데이트 하기 이전@PostUpdate
: 해당 엔티티를 업데이트 한 이후
(1) 설정
@EnableJpaAuditing
→ 스프링 부트 설정 클래스에 적용해야한다.@EntityListeners(AuditingEntityListener.class)
→ 엔티티에 적용
(2) 스프링 데이터 Auditing 적용 - 등록자, 수정자
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
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;
}
@CreatedDate
: 데이터 생성 날짜 자동 저장 어노테이션@LastModifiedDate
: 데이터 수정 날짜 자동 저장 어노테이션@CreatedBy
: 데이터 생성자 자동 저장 어노테이션@LastModifiedBy
: 데이터 수정자 자동 저장 어노테이션
(3) 등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록
DataJpaApplication
에 추가
@Bean
public AuditorAware<String> auditorProvider() {
return () ->
Optional.of(UUID.randomUUID().toString());
}
실행 결과
- 실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하다.
- 등록자, 수정자는 없을 수 있다.
Base 타입
을 분리하고, 원하는 타입을 선택해서 상속한다.
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
null
일 때 등록 컬럼을 또 찾아야 한다.@EnableJpaAuditing(modifyOnCreate = false)
옵션을 사용하면 된다.
✔️ 전체 적용
@EntityListeners(AuditingEntityListener.class)
를 생략하고 스프링 데이터 JPA 가 제공하는 이벤트를 엔티티 전체에 적용하려면 META-INF/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>
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("/members2/{id}")
public String findMember(@PathVariable("id") Member member) {
return member.getUsername();
}
}
id
를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환한다.
실행 결과
⚠️ 주의
- 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다.
- 트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다.
✔️ 페이징과 정렬 예제
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
Pageable
을 받을 수 있다. Pageable
은 인터페이스, 실제로는 org.springframework.data.domain.PageRequest
객체를 생성한다.
✔️ 요청 파라미터
ex)
/members?page=0&size=3&sort=id,desc&sort=username,desc
page
: 현재 페이지. 0부터 시작한다.size
: 한 페이지에 노출할 데이터 건수default
: 20max
: 2000sort
: 정렬 조건을 정의한다.default
: asc
(오름차순)sort
파라미터 추가한다.
✔️ 기본값
글로벌 설정 : 스프링 부트
default-page-size
: 기본 페이지 사이즈max-page-size
: 최대 페이지 사이즈
개별로 설정,
@PageableDefault
어노테이션을 사용
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = “username”, direction = Sort.Direction.DESC) Pageable pageable) {
...
}
✔️ 접두사
@Qualifier
에 접두사명 추가 "{접두사명}_xxx"
/members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
)
실행 결과
- 엔티티를 API로 노출하면 다양한 문제가 발생한다. 그래서 엔티티를 꼭 DTO로 변환해서 반환해야 한다.
Page
는map()
을 지원해서 내부 데이터를 다른 것으로 변경할 수 있다.
MemberDto
@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.map()
을 사용
@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
Page<MemberDto> pageDto = page.map(MemberDto::new);
return pageDto;
}
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size=5) Pageable pageable) {
return memberRepository.findAll(pageable)
.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
}
Page.map()
코드 최적화
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size=5) Pageable pageable) {
return memberRepository.findAll(pageable)
.map(MemberDto::new);
}
실행 결과
스프링 데이터는 Page
를 0부터 시작한다.
🔔 만약
Page
를 1부터 시작하려면?
(1)Pageable
,Page
를 파리미터와 응답 값으로 사용하지 않고, 직접 클래스를 만들어서 처리한다. 그리고 직접PageRequest
(Pageable
구현체)를 생성해서 리포지토리에 넘긴다. 물론 응답값도Page
대신에 직접 만들어서 제공해야 한다.
(2)
spring.data.web.pageable.one-indexed-parameters
를true
로 설정한다. 그런데 이 방법은 web에서page
파라미터를-1
처리 할 뿐이다. 따라서 응답값인Page
에 모두 0 페이지 인덱스를 사용하는 한계가 있다.
page=1
로 실행했지만, 빨간색 부분은page=0
일 때 나와야 하는 결과로 잘못 출력되고 있다.page=2
때도 똑같이 실행했지만, 빨간색 부분pageable, totalPages, ~
은page=1
일 때 나오는 결과이다. 그러므로n
일 때는 빨간색 부분처럼n-1
결과가 나온다. 이를 감안해야 한다.
왠만하면
page=0
부터 시작하자! (page
에 설정을 안 넣는 것이 좋다.)