Skip to content

Latest commit

 

History

History
340 lines (249 loc) · 10.8 KB

05장-확장-기능.md

File metadata and controls

340 lines (249 loc) · 10.8 KB

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

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

스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많음 다양한 이유로 인터페이스의 메서드를 직접 구현하고 싶다면?

JPA 직접 사용( EntityManager ) 스프링 JDBC Template 사용 MyBatis 사용 데이터베이스 커넥션 직접 사용 등등... 직접 커스텀을 해야할 때 주로 사용한다.

그런데 사용처는 Querydsl 사용에서 거의 90%를 사용한다고 봐도 된다.

내가 만들고 싶은 Repository 인터페이스를 만든다.

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

스프링이 구현체를 이용해 bean을 생성할 수 있도록 네이밍 규칙에 따라야 한다. 내가 커스텀한 인터페이스 +Impl 형식의 네이밍 규칙을 지켜 구현한다.

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

그리고 공통으로 사용하는 MemberRepository에서 JpaRepository와 별개로 커스텀한 Repository로 상속받는다.

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

사용은 다음과 같이 동일하게 할 수 있다.

이는 직접 구현체를 작성한 메서드를 타게 된다. ( + Impl 로 클래스 명을 제대로 지정했을 때의 이야기다. )

List<Member> result = memberRepository.findMemberCustom();

2. Auditing

엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶으면?

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

순수 JPA를 사용하여 Auditing을 구현하려면 다음과 같이 하면 된다.

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

그리고 이를 Entity에서 상속받으면 된다.

public class Member extends JpaBaseEntity {}

JPA 주요 이벤트 어노테이션

  • @PrePersist : persist 직전 발생하는 이벤트
  • @PostPersist : persist 직후 발생하는 이벤트
  • @PreUpdate : update 직전 발생하는 이벤트
  • @PostUpdate : update 직후 발생하는 이벤트
  • @PostLoad: 엔티티를 새로 불러오거나 refresh 한 이후
  • @PreRemove: 엔티티를 삭제하기 이전
  • @PostRemove: 엔티티를 삭제한 이후

그러나 Spring Data JPA를 사용하면 위의 Auditing을 좀 더 간단하게 구현할 수 있다.

설정

  • @EnableJpaAuditing : 스프링 부트 설정 클래스에 적용해야함
  • @EntityListeners(AuditingEntityListener.class) : BaseEntity에 적용한다.

사용하는 어노테이션

  • @CreatedDate : 생성 시각
  • @LastModifiedDate : 마지막 변경 시각
  • @CreatedBy : 누가 만들었는지
  • @LastModifiedBy : 누구에 의해 마지막에 수정됐는지

생성 시각/변경 시각 등록

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
  @CreatedDate
  @Column(updatable = false)
  private LocalDateTime createdDate;
  @LastModifiedDate
  private LocalDateTime lastModifiedDate;
}

위 처럼 BaseEntity를 만들고, 상속 받으면 된다.

public class Member extends BaseEntity {}

등록자/수정자 등록

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
  @CreatedDate
  @Column(updatable = false)
  private LocalDateTime createdDate;
  @LastModifiedDate
  private String lastModifiedBy;   
}

등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록

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

실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다.

그래서 다음과 같이 Base 타입을 분리하고, 원하는 타입을 선택해서 상속한다.

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;
}
  • @EntityListeners(AuditingEntityListener.class) 를 달아주는 것이 귀찮을 수가 있다.

    • 이러한 이벤트 리스너를 등록하는 것이 귀찮다면 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>

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

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

즉, PK값으로 넘어오는 값을 Data-Jpa가 바로 찾아준다.

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

@RestController
@RequiredArgsConstructor
public class MemberController {
  private final MemberRepository memberRepository;
  @GetMapping("/members/{id}")
  public String findMember(@PathVariable("id") Long id) {
    // PK를 이용하여 Repository를 이용해 객체를 찾는 과정이 필요.
    Member member = memberRepository.findById(id).get(); 
    return member.getUsername();
  }
}

근데 값으로 들어오는 id가 Entity의 PK이면서, PathVariable을 바로 엔티티로 매핑한다면, 위와 같이 Repository를 이용하여 객체를 찾는 코드를 작성하지 않아도 된다.

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

@RestController
@RequiredArgsConstructor
public class MemberController {
  private final MemberRepository memberRepository;
  @GetMapping("/members/{id}")
  public String findMember(@PathVariable("id") Member member) {
    return member.getUsername();
  }
}
  • HTTP 요청은 회원 id를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환
  • 도메인 클래스 컨버터도 Repository를 사용해서 엔티티를 찾음

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

영한님은 그렇게 추천하는 방법은 아니다. (PK가 외부에 노출되는 방법이므로 그렇게 좋은 방법이 아님)
간단한 곳에만 사용하길 권장.

4. Web 확장 - 페이징과 정렬

@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
  Page<Member> page = memberRepository.findAll(pageable);
  return page;
}
  • 파라미터로 Pageable
  • Pageable 은 인터페이스인데, 스프링이 이를 파라미터를 받을 수 있는 PageRequest객체를 만들어준다.

요청 파라미터

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

  • page : 현재 페이지, 0부터 시작한다.
  • size : 한 페이지에 노출할 데이터 건수
  • sort : 정렬 조건을 정의한다. 예) 정렬 속성,정렬 속성...(ASC | DESC), 정렬 방향을 변경하고 싶으면 sort 파라미터 추가한다(asc가 기본이라, asc는 생략 가능)

실제로 디폴트 Size는 20으로 설정되어 있는데, 이를 Global로 바꾸려면 application.yml(properties)에서 변경할 수 있다.

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

Global보다 더 우선권이 있는 개별 설정방법은 @PageableDefault 을 이용한다.

// 사용예 : http://localhost:8080/members?page=4&size=3&sort=id,desc
@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 5) Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page;
}

그런데, Page안에 담긴 Member가 Entity이다. 이는 좋지 못한 방법이다. DTO로 변환하여 리턴한다. Page는 map()을 지원하여 다른 데이터로 변경할 수 있다.

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

페이지를 1부터 시작하는 방법

기본적으로 스프링 데이터에서 제공해주는 Page 번호는 0번부터 시작한다.

  1. Pageable, Page를 파리미터와 응답 값으로 사용히지 않고, 직접 클래스를 만들어서 처리한다.

    그리고 직접 PageRequest(Pageable 구현체)를 생성해서 Repository에 넘긴다. 물론 응답값도 Page 대신에 직접 만들어서 제공해야 한다.

  2. spring.data.web.pageable.one-indexed-parameterstrue 로 설정한다. 그런데 이 방법은 web에서 page 파라미터를 -1 처리 할 뿐이다. 따라서 응답값인 Page 에 모두 0 페이지 인덱스를 사용하는 한계가 있다

    • ?page=0 이나, ?page=1이나 둘 다 첫 페이지를 보여준다.

    • http://localhost:8080/members?page=1 로 요청을 했어도, 내부 페이지 객체(Pageable)의 PageNumber는 0을 가리키고 있다.(글로벌 설정이 적용되지 않음)

      • ...
        "pageable": {
        "offset": 0,
        "pageSize": 10, "pageNumber": 0 //0 인덱스
        },
        ...