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);
 }