[Spring Data JPA] 확장 기능

kiteB·2021년 10월 31일
0

Spring Data JPA

목록 보기
6/8
post-thumbnail

[ 사용자 정의 리포지토리 구현 ]

1. 사용자 정의 리포지토리 기능의 필요성

스프링 데이터 JPA는 인터페이스만 구현하면 스프링이 구현체를 자동으로 생성해준다.
스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현해야 하는 경우는 구현해야 하는 기능이 너무 많다!

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

  • JPA 직접 사용 (EntityManager)
  • 스프링 JDBC Template 사용
  • MyBatis 사용
  • 데이터베이스 커넥션 직접 사용
  • Querydsl 사용

등의 방법이 있다.

스프링 JPA는 이런 경우를 위해서 사용자 정의 리포지토리 기능을 제공한다!


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

1) 사용자 정의 인터페이스

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

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

@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

스프링 데이터 JPA가 인식해서 스프링 빈으로 등록해준다.

만약 Impl 대신 다른 이름으로 변경하고 싶으면?
XML, JavaConfig 설정을 통해 바꿀 수 있지만 웬만하면 그냥 Impl로 쓰자^_^

3) 사용자 정의 인터페이스 상속

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom { ... }
  • MemberRepositoryCustom을 상속받도록 추가해주자.

4) 테스트

항상 사용자 정의 리포지토리가 필요한 것은 아니다.
임의의 리포지토리를 만들어서 스프링 빈으로 등록해서
직접 사용해도 된다. (물론 스프링 데이터 JPA와는 무관하게 동작한다.)


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

@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
 
    private final EntityManager em;
 
    @Override
    public List<Member> findMemberCustom() {
        return em.createQuery("select m from Member m")
            .getResultList();
    }
}

스프링 데이터 2.x부터는 리포지토리 인터페이스 이름 + Impl (기존 방식) 대신
사용자 정의 인터페이스 명 + Impl 방식도 지원한다.

즉, MemberRepositoryImpl 대신 MemberRepositoryCustomImpl 처럼 구현해도 된다는 뜻!

기존 방식보다 이 방식이 사용자 정의 인터페이스 이름과 구현 클래스 이름이 비슷하므로
더 직관적이며, 여러 인터페이스를 분리해서 구현하는 것도 가능하므로 이 방식을 추천한다! 😊


[ Auditing ]

협업할 때 엔티티 생성, 변경할 때 변경한 사람과 시간 기록을 남기는 것이 좋다!

  • 등록일
  • 수정일
  • 등록자
  • 수정자

1. 순수 JPA 사용

1) 수정일, 등록일 적용

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

📌 JPA 주요 이벤트 어노테이션

  • @PrePersist: 해당 엔티티를 저장하기 이전
  • @PostPersist: 해당 엔티티를 저장한 이후
  • @PreUpdate: 해당 엔티티를 업데이트 하기 이전
  • @PostUpdate: 해당 엔티티를 업데이트 한 이후

2) MemberJpaBaseEntity 상속받도록 설정

public class Member extends JpaBaseEntity {}

3) 테스트 실행 결과


2. 스프링 데이터 JPA 사용

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 스프링 빈 등록

@Bean
public AuditorAware<String> auditorProvider() {
    return () ->
        Optional.of(UUID.randomUUID().toString());
}
  • 실무에서는 세션 정보나, 스프링 시큐리티 로그인 정보에서 ID를 받음.

4) 실행 결과

🔗 더 효율적으로 개선한 코드
등록일, 수정일 → BaseTimeEntity,
등록자, 수정자 → BaseTimeEntity을 상속한 BaseEntity으로
나누어서 경우에 따라 다양하게 쓸 수 있도록 바꿨다.


[ Web 확장 - 도메인 클래스 컨버터 ]

HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩

1. 도메인 클래스 컨버터

1) 적용 전

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

기존에는 파라미터로 아이디를 받아서 해당 아이디를 가진 엔티티를 조회한다.

실행 결과

2) 적용 후

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;
    
    @GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Member member) {
        return member.getUsername();
    }
}

도메인 컨버터를 사용하면 아이디를 받은 경우
중간에 도메인 클래스 컨버터가 동작하여 해당 엔티티를 객체를 반환해준다.

실행 결과

🚫 주의: 단순히 조회용으로만 사용해야 한다 ❗

트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다!


[ Web 확장 - 페이징과 정렬 ]

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

1. 페이징과 정렬 예제

@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page;
}
  • 파라미터로 Pageable을 받을 수 있다.
  • Pageable은 인터페이스로,
    실제로는 org.springframework.data.domain.PageRequest 객체를 생성한다.

2. 페이징과 정렬

1) 요청 파라미터

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

  • page: 현재 페이지. 0부터 시작한다.
  • size: 한 페이지에 노출할 데이터 건수
    • default: 20
    • max: 2000
  • sort: 정렬 조건을 정의한다.
    • default: asc (오름차순)

→ 기본값 설정을 바꾸고 싶은 경우는 @PageableDefault 어노테이션을 사용하면 된다.

2) 접두사

  • 페이징 정보가 둘 이상이면 접두사로 구분한다.
  • @Qualifier에 접두사명 추가 "{접두사명}_xxx"
  • 예제: /members?member_page&0&order_page=1
public String list(
    @Qualifier("member") Pageable memberPageable,
    @Qualifier("order") Pageable orderPageable, ...
)

3) 실행 결과


3. Page 내용을 DTO로 변환하기

  • 엔티티를 API로 노출하면 모든 정보가 노출되어 다양한 문제가 발생한다. 그래서 꼭 엔티티를 DTO로 변환해서 반환해야 한다!
  • Pagemap()을 지원해서 내부 데이터를 다른 것으로 변경할 수 있다.

1) Member DTO

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

2) Page.map() 사용

@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size=5) Pageable pageable) {
    return memberRepository.findAll(pageable)
            .map(member -> new MemberDto(member.getId(), member.getUsername(), null));
}

3) Page.map() 코드 최적화

@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size=5) Pageable pageable) {
    return memberRepository.findAll(pageable)
        .map(MemberDto::new);
}

4) 실행 결과


참고자료

EntityListener
JPA Auditing 어노테이션

profile
🚧 https://coji.tistory.com/ 🏠

0개의 댓글