Spring/인프런_JPA
[Spring]스프링 데이터 JPA - 확장 기능
빙응이
2024. 7. 23. 22:45
📝 사용자 정의 리포지토리
JPA에서 기본적인 기능은 JpaRepository 인터페이스를 상속받아 사용한다.
그러나 복잡한 쿼리나 특정 기능 구현을 위해 사용자 정의 인터페이스를 사용할 수 있다.
- 스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동으로 생성한다
- 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많다.
- 다양한 이유로 인터페이스 메서드를 직접 구현하고 싶다면 사용하면 된다.!!
사용자 정의 인터페이스
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 {
}
📌사용 규칙
- 구현 클래스의 이름은 사용자 정의 인터페이스 이름 + Impl 을 지켜야한다.
- 예)MemberRepositoryImpl
- 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록하기 때문이다.
만약 변경하고 싶다면
JavaConfig 설정을 바꾸면 된다.
@EnableJpaRepositories(basePackages = "study.datajpa.repository",
repositoryImplementationPostfix = "Impl")
참고: 실무에서는 주로 QueryDSL이나 SpringJdbcTemplate를 사용할 때 사용자 정의 기능을 사용한다.
참고 : 항상 사용자 정의 리포지토리를 만들 필요는 없다. 그냥 임의의 리포지토리를 만들어 빈에 등록해도 된다.
📝 Auditing
Auditing은 엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적할 때 사용한다.
- 필요한 정보
- 등록일
- 수정일
- 등록자
- 수정자
📌 순수 JPA로 구현해보기
@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();
}
}
위처럼 ProPersist는 생성될 때 진행되며, ProUpdate는 업데이트 시에 진행되어 날짜를 갱신한다.
이것을 엔티티에 상속하여 같이 쓰게만 할 수 있게 하면 된다.
public class Member extends JpaBaseEntity {}
📌 스프링 데이터 JPA로 구현해보기
설정 어노테이션
- @EnableJpaAuditing : 스프링 부트 설정 클래스에 적용
- @EntityListeners(AuditingEntityListener.class) : 엔티티에 적용
사용 어노테이션
- @CreatedDate : 생성 시 갱신
- @LastModifiedDate : 마지막 업데이트에 갱신
- @CreatedBy : 생성자 이름
- @LastModifiedBy : 갱신자 이름
@EntityListeners(AuditingEntityListener.class)
@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;
}
@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());
참고! : 실무에서는 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있기에 따로 만드는 것이 좋다.
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;
}
참고 : 저장시점에 등록일, 등록자는 물론이고, 수정일, 수정자도 같은 데이터가 저장된다. 데이터가 중복 저장되는 것 같지만, 이렇게 해두면 변경 컬럼만 확인해도 마지막에 업데이트한 유저를 확인 할 수 있으므로 유지보수 관점에서 편리하다. 이렇게 하지 않으면 변경 컬럼이 null일때 등록 컬럼을 또 찾아야 한다.
참고로 저장시점에 저장데이터만 입력하고 싶으면 @EnableJpaAuditing(modifyOnCreate = false)옵션을 사용하면 된다
📝 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에 반영되지 않는다.
📝Web 확장 - 페이징과 정렬
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편하게 사용할 수 있다.
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
- 파라미터로 Pageable을 받을 수 있다.
- Pageable은 인터페이스, 실제는 org.springframework.data.domain.PageRequest 객체를 생성하는 것이다.
📌 예제
요청 파라미터
- 예) /member?page=0&size=3&sort=id, desc&sort=username, desc
- page : 현재 페이지는 0부터 시작
- size : 한 페이지의 노출할 건수는 3개
- sort : 정렬 조건을 정의
개별 설정은 어노테이션을 통해 구현할 수 있다.
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "username",
direction = Sort.Direction.DESC) Pageable pageable) {
}
📌 Page 내용을 DTO로 변환하기
- 엔티티를 API로 노출하면 다양한 문제가 발생한다. 그래서 엔티티를 DTO로 변환해야 한다.
- Page는 map을 지원해서 내부 데이터를 다른 것으로 변경 가능하다.
@Data
public class MemberDto {
private Long id;
private String username;
}
@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(Pageable pageable) {
return memberRepository.findAll(pageable).map(MemberDto::new);
}