본문 바로가기
Dev/Spring Data JPA

[Spring Data JPA] 확장 기능

by dev_jsk 2021. 12. 13.
728x90
반응형

사용자 정의 레포지토리 구현

  • 스프링 데이터 JPA 레포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성
  • 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많다.
    만약 인터페이스의 메소드를 직접 구현하고 싶다면
    (1) JPA 직접 사용(EntityManager)
    (2) 스프링 JDBC Template 사용
    (3) MyBatis 사용
    (4) 데이터베이스 커넥션 직접 사용
    (5) 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", Member.class).getResultList();
    }
    
}
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    ...    
}
  • 명명 규칙(레포지토리 인터페이스 이름 + Impl 또는 사용자 정의 인터페이스 이름 + Impl)에 맞게 클래스를 생성하면 스프링 데이터 JPA가 인식하여 스프링 빈으로 등록한다.
    사용자 정의 인터페이스 이름 + Impl이 가능해지면서 사용자 정의 인터페이스 구현 클래스 명을 인터페이스 이름과 동일하게 할 수 있어 더 직관적이며 추가로 여러 인터페이스를 분리해서 구현하는 것도 가능하다.
  • Impl 대신 다른 이름으로 변경하고 싶다면 XML 또는 JavaConfig 설정을 통해 변경 가능하다.
    - XML 설정
    <repositories base-package="study.datajpa.repository" repository-impl-postfix="Impl" />
    - JavaConfig 설정
    @EnableJpaRepositories(basePackages = "study.datajpa.repository", repositoryImplementationPostfix = "Impl")

※ 참고로 실무에서는 주로 QueryDSL이나 SpringJdbcTemplate를 함께 사용할 때 사용자 정의 레포지토리를 구현한다.

※ 참고로 항상 사용자 정의 레포지토리가 필요한 것은 아니다. 그냥 임의의 레포지토리 클래스를 만들어 스프링 빈으로 등록하여 사용해도 된다. 이 경우엔 스프링 데이터 JPA와는 무관하게 별도로 동작한다.

Auditing

엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하는 기능

순수 JPA 사용

@MappedSuperclass
@Getter
public class JpaBaseEntity {
    
    @Column(updatable = false)
    private LocalDateTime createDate;
    private LocalDateTime updateDate;

    @PrePersist
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        this.createDate = now;
        this.updateDate = now;
    }

    @PreUpdate
    public void preUpdate() {
        LocalDateTime now = LocalDateTime.now();
        this.updateDate = now;
    }

}

public class Member extends JpaBaseEntity {
    ...
}
  • @MappedSuperclass : 실제 상속관계가 아닌 속성만 상속관계를 갖는 것
  • @PrePersist : INSERT 실행 전 동작
    @PreUpdate : UPDATE 실행 전 동작
    @PreDelete : DELETE 실행 전 동작
  • @PostPersist : INSERT 실행 후 동작
    @PreUpdate : UPDATE 실행 후 동작
    @PreDelete : DELETE 실행 후 동작
    @PreLoad : SELECT 실행 후 동작

테스트 코드

@Test
public void jpaEventBaseEntity() throws Exception {
    // given
    Member member = new Member("member1");
    memberJpaRepository.save(member);

    Thread.sleep(100);
    member.setUsername("member2");

    em.flush();
    em.clear();

    // when
    Member findMember = memberJpaRepository.find(member.getId());

    // then
    System.out.println("findMember.createDate" + findMember.getCreateDate());
    System.out.println("findMember.updateDate" + findMember.getUpdateDate());
}

테스트 결과

DDL
테스트 결과

스프링 데이터 JPA 사용

@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataJpaApplication.class, args);
    }
    
    @Bean
    public AuditorAware<String> auditorProvider() {
        // 실무에서는 등록자, 수정자를 세션 정보나 스프링 시큐리티 로그인 정보에서 ID를 받아 사용
        return () -> Optional.of(UUID.randomUUID().toString());
    }
}
@EntityListeners(value = AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createDate;
    @LastModifiedDate
    private LocalDateTime updateDate;

    @CreatedBy
    @Column(updatable = false)
    private String createBy;
    @LastModifiedBy
    private String updateBy;
}
  • 스프링 부트 설정 클래스에 @EnableJpaAuditing 어노테이션 설정을 한다.
  • 저장시점에 등록일, 등록자, 수정일, 수정자 정보가 같이 저장된다. 이렇게 해야 마지막 수정자를 찾기 편리하다. 만약 저장시 수정 정보를 저장하고 싶지 않으면 @EnableJpaAuditing(modifyOnCreate = false)으로 설정하면 된다.
  • 실무에서는 등록자, 수정자를 세션 정보나 스프링 시큐리티 로그인 정보을 받아 사용한다.
  • 등록/수정일, 등록/수정자 엔티티를 분리하여 사용해도 좋다. 대부분의 엔티티에는 등록/수정일은 필수로 사용되지만 등록/수정자는 사용되지 않는 경우도 있어 부모 BaseTimeEntity를 구현하고 자식 BaseEntity를 구현하여 사용해도 된다. (부모 : 등록/수정일, 자식 : 등록/수정자)

테스트 코드

@Test
public void eventBaseEntity() throws Exception {
    // given
    Member member = new Member("member1");
    memberRepository.save(member);

    Thread.sleep(100);
    member.setUsername("member2");

    em.flush();
    em.clear();

    // when
    Member findMember = memberRepository.findById(member.getId()).get();

    // then
    System.out.println("findMember.createDate = " + findMember.getCreateDate());
    System.out.println("findMember.updateDate = " + findMember.getUpdateDate());
    System.out.println("findMember.createBy = " + findMember.getCreateBy());
    System.out.println("findMember.updateBy = " + findMember.getUpdateBy());
}

테스트 결과

DDL
테스트 결과

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

HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아 바인딩 해주는 기능

도메인 클래스 컨버터 사용 전

@GetMapping(value="/members/{id}")
public String findMember(@PathVariable("id") Long id) {
    Member member = memberRepository.findById(id).get();
    return member.getUsername();
}
  • 파라미터로 받은 ID로 엔티티 조회 후 해당 엔티티의 username을 반환

도메인 클래스 컨버터 사용 후

@GetMapping(value="/members/{id}")
public String findMember(@PathVariable("id") Member member) {
    return member.getUsername();
}
  • 파라미터로 ID 값을 받지만 중간에서 도메인 클래스 컨버터가 해당 ID를 가진 엔티티를 바인딩하기 때문에 바로 엔티티의 username을 반환한다.
  • 도메인 클래스 컨버터도 레포지토리를 사용하여 엔티티를 찾는다.

※ 주의할 점으로 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면 이 엔티티는 조회용으로만 사용해야 한다. 왜냐하면 트랜잭션이 없는 범위에서 조회된 엔티티이기 때문에 엔티티를 변경해도 DB에 반영되지 않기 때문이다.

Web 확장 - 페이징과 정렬

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

페이징과 정렬 예제

@GetMapping(value="/members")
public Page<Member> list(Pageable pageable) {
    return memberRepository.findAll(pageable);
}
  • 리턴타입을 Page<T>로 하고 파라미터로 Pageable 인터페이스를 받아 페이징과 정렬을 구현할 수 있다.
  • Pageable은 인터페이스로 실제는 org.springframework.data.domain.PageRequest 객체를 생성한다.

페이징과 정렬 예제 결과

예제 결과로 ID 역순, 한 페이지 3개가 표시된다.

요청 파라미터

  • page : 현재 페이지로 0부터 시작한다.
  • size : 한 페이지에 노출할 데이터 건수를 지정한다.
  • sort : 정렬 조건을 지정한다. sort 파라미터를 추가하여 정렬을 추가할 수 있다. (ex. sort=id, desc&sort=username)

기본값 설정

전역설정 (application.yml)

spring:
  data:
    web:
      pageable:
        default-page-size: 10  /# 기본 페이지 사이즈 /
        max-page-size: 1000  /# 최대 페이지 사이즈 /

개별설정 (@PageableDefault)

@GetMapping(value="/members")
public Page<Member> list(@PageableDefault(size = 3
                                      , sort = "username"
                                      , direction = Direction.DESC) Pageable pageable) {
    return memberRepository.findAll(pageable);
}

접두사

  • 페이징 정보가 둘 이상이면 @Qualifier을 사용하여 접두사를 추가하여 구분한다. (ex. {접두사명}_xxx)
    public String list(
        @Qualifier("member") Pageable memberPageable,
        @Qualifier("order") Pageable orderPageable, ...

Page 내용을 DTO로 변환하기

엔티티를 API로 노출하면 다양한 문제가 발생하기 때문에 반드시 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 m) {
        this.id = m.getId();
        this.username = m.getUsername();
    }
}
@GetMapping(value="/memberdtos")
public Page<MemberDto> listDto(@PageableDefault(size = 3
                                           , sort = "username"
                                           , direction = Direction.DESC) Pageable pageable) {
    return memberRepository.findAll(pageable).map(MemberDto::new);
}
  • 페이징과 정렬도 구현해야 할 경우 Page는 map()을 지원하기 때문에 내부 데이터를 다른 것으로 변경할 수 있어 map()을 사용하여 구현할 수 있다.

Page를 1부터 시작하기

스프링 데이터는 Page 인덱스가 0부터 시작한다. 1부터 시작하도록 변경해보자

  1. Page, Pageable을 파라미터로 사용하지 않고 직접 클래스로 만들어 커슽터마이징하여 처리하고, 직접 PageRequest(Pageable 구현체)를 생성하여 레포지토리에 넘겨 사용한다. 또한 응답 Page도 직접 만들어서 사용해야 한다.
  2. application.ymlspring.data.web.pageable.one-indexed-parameters = true로 설정한다. 하지만 이 방법은 Page 파라미터를 -1 할 뿐이기 때문에 0, 1페이지 응답 Page에서 모두 0페이지 인덱스를 사용하는 한계가 존재한다.
    spring:
      data:
        web:
          pageable:
            one-indexed-parameters: true

결과적으로 위 2가지 방법을 사용하면 1 페이지부터 사용이 가능하지만 한계가 있어 가장 좋은 방법은 0 페이지부터 사용하는 것이다.

728x90
반응형

댓글