본문 바로가기
JPA

JPA 를 공부하면서 알게 된 내용 정리 3

by 성건희 2022. 2. 2.
반응형

Spring Data JPA - 2편

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

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

위처럼 Custom 인터페이스를 만들어 두고, 기존의 Repository 에 extends 하면 된다.

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
  ...
}

사용자 정의 리포지토리를 만들어두지 않으면 impl 을 만들 때 JpaRepository 의 모든 기능을 구현해야하는 문제가 있다.
보통 복잡한 동적쿼리로 QueryDSL 을 쓸 때 사용자 정의 리포지토리를 많이 사용한다.

참고로 항상 사용자 정의 리포지토리가 필요한 것은 아니다.
그냥 임의의 리포지토리를 만들어도 된다. 예를 들어 MemberQueryRepository 를 인터페이스가 아닌 클래스로 만들고
스프링 빈으로 등록해서 그냥 직접 사용해도 된다. 물론 이 경우에는 스프링 데이터 JPA 와는 아무런 관계 없이 별도로 동작한다.

주의할점

사용자 정의 리파지토리의 구현체는 리파지토리 명 뒤에 Impl 을 붙여야한다.
그래야 Spring Data JPA 가 impl 을 찾아서 넣어준다. (안붙이면 에러남)
Impl 대신 다른 이름을 사용하는 방법이 있기는 하지만.. 그냥 왠만해서는 관례상 Impl 작성을 따르자.

Auditing

엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적할 때 사용.

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

순수 JPA 방식

@Getter
@MappedSuperclass
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();
    }
}
@Entity
public class Member extends JpaBaseEntity {
   ...
}

위처럼 등록해두면 등록일, 수정일 처리를 공통 클래스로 처리할 수 있다.
Spring Data JPA 에서는 이것을 더 편하게 만들 수 있도록 제공한다.

Spring Data JPA 방식

먼저 이걸 사용하려면 스프링 부트 설정 클래스에 @EnableJpaAuditing 어노테이션을 적용해야한다. (이거 안하면 적용안되니 주의)

@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(DataJpaApplication.class, args);
    }
}

그 후 아래와 같이 등록하면 된다.

@Getter
@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;

}

@CreatedBy, @LastModifiedBy 는 어떤 이름을 넣어줄지 처리를 해주어야한다.

@Bean
public AuditorAware<String> aditorProvider() {
    // 스프링 시큐리티를 쓰면 해당 세션 정보 ID 를 가져와서 넣어주면 됨
}

@EntityListeners(AuditingEntityListener.class)

자동으로 넣기 위해서는 해당 어노테이션을 붙여줘야하는데,, 매번 넣어주기 번거롭다면 xml 로 처리할 수 있다.

경로 : 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>

JPA 스펙에서 entity-listener 를 읽어서 자동으로 실행함.

등록 시 update 시간에 null 을 넣어야 할까?

update 에 null 을 넣게되면 null 처리해야 할 코드가 늘어난다.
보통 관례 상 insert 시 update 도 등록 시간을 넣어주는것이 일반적이다.

실무에서는 어떻게 활용할까

보통 실무에서는 등록시간, 수정시간 만 사용하는 페이지가 있고
등록시간, 수정시간, 등록자, 수정자 를 사용하는 페이지가 있을 수 있다. (보통 시간은 거의 다 쓴다.)

이런 경우 아래와 같이 처리 할 수 있다.

@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseTimeEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}
@Getter
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity extends BaseTimeEntity {

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;
}
  • 등록시간, 수정시간 필요 -> BaseTimeEntity 사용
  • 등록시간, 수정시간, 등록자, 수정자 필요 -> BaseEntity 사용

도메인 클래스 컨버터

보통 유저 정보를 조회할 때 아래와 같이 @PathVariable 을 통해 id 를 가져와서 컨트롤러를 만든다.

@GetMapping("/members/{id}")
public String findMember(@PathVariable("id") Long id) {
    Member member = memberRepository.findById(id).get();
    return member.getUsername();
}

하지만 클래스로 받아서 처리할 수 도 있다.

@GetMapping("/members2/{id}")
public String findMember2(@PathVariable("id") Member member) {
    return member.getUsername();
}

HTTP 요청은 회원 id 를 받지만 스프링의 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 조회해 반환한다.
이 때, 도메인 클래스 컨버터도 리파지토리를 사용해 엔티티를 조회한다.

주의 : 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 해당 엔티티는 단순 조회용으로만 사용해야 한다.
(트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)

영한님은 잘 사용하지는 않는다고 하심.

컨트롤러에서 페이징 처리

@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
    return memberRepository.findAll(pageable);
}

스프링 부트가 자동으로 PageRequest 객체를 생성해서 Pageable 인터페이스에 DI 해준다.

따라서 클라이언트에서는 아래와 같이 사용하면 된다.

http://localhost:8080/members?page=0&size=3&sort=username,desc
  • page : 현재 페이지, 0 부터 시작
  • size : 한 페이지에 노출할 데이터 건수
  • sort : 정렬 조건

page default size 변경

기본적으로 페이징 데이터는 20개씩 가져오도록 세팅되어있다.
이것을 변경하려면 아래와 같이 지정하면 된다.

글로벌 설정
application.yml

spring:
  data:
    web:
      pageable:
        default-page-size: 10

단건 설정
@PageableDefault

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

API 에서는 반환을 Entity 로 하면 안된다.

API 스팩이 변하기 때문에 DTO 로 변환해서 반환해야한다.

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

}

DTO 와 Entity 의존 관계

DTO 는 Entity 의 의존을 가져도 된다.
하지만 Entity 는 가급적 DTO 의 의존을 가지면 안된다.

@Data
public class MemberDto {

    private Long id;

    private String username;

    private String teamName;

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

Page 를 1부터 시작하려면?

스프링 데이터는 Page 를 0 부터 시작한다.
1부터 시작하게 하려면 아래와 같이 처리하면 된다.

방법 1

Pageable, Page 를 파라미터와 응답 값으로 사용하지 않고, 직접 클래스를 만들어서 처리한다.
그리고 직접 PageRequest 를 생성해서 리포지토리에 넘긴다.
물론 응답값도 Page 대신에 직접 만들어서 제공해야 한다.

방법 2

spring.data.web.pageable.one-indexed-parameterstrue 로 설정한다.
하지만 이 방법은 web 에서 page 파라미터를 -1 처리 할 뿐이다.
(page=0, page=1 입력 모두 동일한 데이터를 노출함 - 모두 0 페이지 인덱스를 사용)
이것의 단점은 같이 넘어가는 Page 데이터는 현재 페이지 정보를 보여주기 때문에 데이터가 일치하지 않는 문제가 있다.

그냥 Page 를 0 부터 시작하는 것으로 쓰는게 가장 깔끔하다.

참고

  • 실전! 스프링 데이터 JPA
반응형

댓글