Web/JPA

[Data JPA] EP 4. 확장 기능

목차 (클릭시 해당 목차로 이동)


     

     

     

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

     

    Data JPA를 사용하다보면 인터페이스의 메소드를 직접 구현해야할 때가 있습니다.

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

     

    이때 Data JPA의 인터페이스를 직접 구현하는 것은 너무 많기때문에 Data JPA에서 사용자 정의 리포지토리 구현 기능을 제공합니다.

     

     

    사용자 정의 리포지토리 구현 기능 사용법

    1. 직접 구현할 메소드가 있는 인터페이스 (아무이름) (ex. MemberRepositoryCustom) 을 만듭니다.
    2. 구현클래스 MemberRepository...Impl 로 이름을 짓고 MemberRepositoryCustom를 상속받아 메소드를 구현합니다.
    3. MemberRepository 클래스에서 JpaRepository와 함께 MemberRepositoryCustom을 상속받습니다.

     

    그러면 MemberRepository에서 구현클래스에서 구현한 메소드를 사용할 수 있습니다.

     

    자바여서 가능한 것이 아니라 Data JPA에서 해주는 것입니다.

     

    구현 클래스의 이름은 MemberRepository ... Impl 이것만 맞춰주면 됩니다.

     

     

    MemberRepositoryCustom

    package study.datajpa.repository;
    
    import study.datajpa.entity.Member;
    
    import java.util.List;
    
    public interface MemberRepositoryCustom {
        List<Member> findMemberCustom();
    }
    

     

    MemberRepositoryQueryImpl

    package study.datajpa.repository;
    
    import lombok.RequiredArgsConstructor;
    import study.datajpa.entity.Member;
    
    import javax.persistence.EntityManager;
    import java.util.List;
    
    @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

    package study.datajpa.repository;
    
    import org.springframework.data.domain.Page;
    import org.springframework.data.domain.Pageable;
    import org.springframework.data.domain.Slice;
    import org.springframework.data.jpa.repository.*;
    import org.springframework.data.repository.query.Param;
    import study.datajpa.dto.MemberDto;
    import study.datajpa.entity.Member;
    
    import javax.persistence.LockModeType;
    import javax.persistence.QueryHint;
    import java.util.Collection;
    import java.util.List;
    import java.util.Optional;
    
    public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    ...

     

     

     

     

     

    주의점

    • 항상 사용자 정의 리포지토리가 필요한건 아닙니다. 오히려 복잡해질 수 있기 때문에 서비스를 살펴보고 필요한 경우에 사용합니다.
    • 화면에 맞춘 쿼리와 핵심비즈니스 로직을 구분해야합니다. 구분할 때는 그냥 임의의 리포지토리를 만들어서 구분해도 됩니다.

     

     

    임의의 리포지토리 구분해 사용하기

     

    package study.datajpa.repository;
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Repository;
    import study.datajpa.entity.Member;
    
    import javax.persistence.EntityManager;
    import java.util.List;
    
    @Repository
    @RequiredArgsConstructor
    public class MemberQueryRepository {
    
        private final EntityManager em;
    
        List<Member> findAllMembers() {
            return em.createQuery("select m from Member m")
                    .getResultList();
        }
    }
    

     

     

    Auditing

     

     

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

    1. 등록일
    2. 수정일
    3. 등록자
    4. 수정자

     

    순수 JPA로 해결

     

    사용법

    Entity에서 JpaBaseEntity를 extends만 하면 됩니다.

     

    package study.datajpa.entity;
    
    import lombok.Getter;
    
    import javax.persistence.Column;
    import javax.persistence.MappedSuperclass;
    import javax.persistence.PrePersist;
    import javax.persistence.PreUpdate;
    import java.time.LocalDateTime;
    
    @MappedSuperclass
    @Getter
    public class JpaBaseEntity {
    
        @Column(updatable = false)
        private LocalDateTime createDate;
        private LocalDateTime updateDate;
    
        @PrePersist
        public void prePersist() {
            LocalDateTime now = LocalDateTime.now();
            createDate = now;
            updateDate = now;
        }
    
        @PreUpdate
        public void preUpdate() {
            updateDate = LocalDateTime.now();
        }
    }
    

     

    ...
    public class Member extends JpaBaseEntity{
    ...

     

     

    주의점

    persist시에도 update에 값을 넣어놓습니다. 왜냐하면 나중에 쿼리할때 null있으면 불편하기 때문입니다.

    JpaBaseEntity에 @MappedSuperclass를 해야합니다. @MappedSuperclass의 의미는 진짜 상속관계는 아니고 데이터만 공유하는 상속관계라는 의미입니다.

     

     

     

     

     

     

    스프링 데이터 JPA 사용 (실무)

     

    1. 순수 JPA 사용때와 달리 어노테이션을 이용하면 됩니다.
    2. 추가로 @EntityListeners(AuditingEntityListener.class)를 꼭 해주어야 합니다.
    3. Application의 @SpringBootApplication와 같이 @EnableJpaAuditing을 해줍니다.

     

    실무에서는 어느곳에선 등록일, 수정일만 필요, 등록자 수정자가 필요하지 않는 경우가 있을 수 있습니다.

    이럴경우 BaseEntity에 시간, 사람을 다 넣으면 필요없는 데이터가 DB에 들어갑니다.

     

    시간만 필요한 (거의 모든경우)에는 BaseTimeEntity를 최상위에 만들고 그 바로 아래 등록자 수정자를 담은 BaseEntity를 만들면 됩니다.

     

    • 시간, 사람 필요한 경우에는 BaseEntity를 상속
    • 시간 필요한 경우에는 BaseTimeEntity를 상속

     

    또한, String을 넣어줄 때는 시간과 달리 string을 넣어줘야 하므로 Application main 함수 아래에 AuditorAware<String>으로 아무 아이디를 넘기도록 합니다.

     

    실무에서는 세션에서 ID를 받아 넘겨주면 됩니다.

     

    package study.datajpa.entity;
    
    import lombok.Getter;
    import org.springframework.data.annotation.CreatedBy;
    import org.springframework.data.annotation.LastModifiedBy;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import javax.persistence.Column;
    import javax.persistence.EntityListeners;
    import javax.persistence.MappedSuperclass;
    
    @EntityListeners(AuditingEntityListener.class)
    @MappedSuperclass
    @Getter
    public class BaseEntity extends BaseTimeEntity{
        @CreatedBy
        @Column(updatable = false)
        private String createdBy;
    
        @LastModifiedBy
        private String lastModifiedBy;
    }
    
    package study.datajpa.entity;
    
    import lombok.Getter;
    import org.springframework.data.annotation.CreatedBy;
    import org.springframework.data.annotation.CreatedDate;
    import org.springframework.data.annotation.LastModifiedBy;
    import org.springframework.data.annotation.LastModifiedDate;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import javax.persistence.Column;
    import javax.persistence.EntityListeners;
    import javax.persistence.MappedSuperclass;
    import java.time.LocalDateTime;
    
    @EntityListeners(AuditingEntityListener.class)
    @MappedSuperclass
    @Getter
    public class BaseTimeEntity {
    
        @CreatedDate
        @Column(updatable = false)
        private LocalDateTime createDate;
    
        @LastModifiedDate
        private LocalDateTime lastModifiedDate;
    }
    

     

     

     

     

     

    Web 확장 - 페이징과 정렬

     

     

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

     

    이 전에 사용했던 Pageable을 사용하면 쉽게 사용이 가능합니다.

     

    pageable의 page, size를 스프링 부트가 pageRequest를 생성해서 injection 해줍니다.

     

     

     

    size의 기본값은 20으로 되어있습니다.

    Pageable의 page, size 사용자 지정하기

     

    1. 기본 값 바꾸기

    글로벌 설정으로 모든 pageable의 size를 설정할 수 있습니다.

     

    application.yml

      data:
        web:
          pageable:
            default-page-size: 10
            max-page-size: 2000

     

     

    2. 해당 요청에 대해서만 바꾸기

    @PageableDefault로 size를 지정할 수 있습니다. (아래 코드 예시)

     

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

     

     

    그리고 실제 위 @GetMapping에 Postman으로 요청을 보내고 Json 응답을 받아보았습니다.

     

    지정한 size, sort 대로 결과가 잘 나왔습니다.

     

    근데 마지막 부분에 "pageable" 데이터 부분과 그 아래에 다른 데이터가 반환되는 것을 볼 수 있습니다.

    반환타입이 Page라서 해당하는 정보들이 나가는 것을 볼 수 있습니다.

     

     

    {
        "content": [
            {
                "id": 1,
                "username": "user0",
                "age": 0,
                "team": null
            },
            {
                "id": 2,
                "username": "user1",
                "age": 1,
                "team": null
            },
            {
                "id": 11,
                "username": "user10",
                "age": 10,
                "team": null
            },
            {
                "id": 12,
                "username": "user11",
                "age": 11,
                "team": null
            },
            {
                "id": 13,
                "username": "user12",
                "age": 12,
                "team": null
            }
        ],
        "pageable": {
            "sort": {
                "sorted": true,
                "unsorted": false,
                "empty": false
            },
            "offset": 0,
            "pageSize": 5,
            "pageNumber": 0,
            "paged": true,
            "unpaged": false
        },
        "last": false,
        "totalPages": 20,
        "totalElements": 100,
        "number": 0,
        "size": 5,
        "sort": {
            "sorted": true,
            "unsorted": false,
            "empty": false
        },
        "first": true,
        "numberOfElements": 5,
        "empty": false
    }

     

     

    Page의 totalCount 계산을 위해 count 쿼리가 나가는 것도 볼 수 있습니다.

     

     

     

     

     

    추가로 페이징 정보가 둘 이상일때 접두사로 구분이 가능합니다.

     

     

    @Qualifier에 접두사명을 추가합니다. "{접두사명}_xxx"

     

    ex) /members?member_page=0%order_page=1

    public String list(
    	@Qualifier("member") Pageable memberPageable,
    	@Qualifier("order") Pageable orderPageable, ...

     

     

     

     

     

     

    주의할 점

    여기서 주의할 점이 있습니다.

     

    컨트롤러에서는 클라이언트에 절대 엔티티를 그대로 반환하면 안됩니다.

    Page도 마찬가지 입니다.

    2021.04.05 - [Web/JPA] - EP1. 회원관리 API 만들기

     

    엔티티를 직접 반환하지 않고 DTO로 변환한 뒤 반환합니다.

     

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

     

     

     

     

    참고

    Page<MemberDto> map = page.map(MemberDto::new); 사용방법

     

    DTO는 엔티티를 봐도 되지만 엔티티는 DTO를 보면 안되는 원리를 이용해서 아래와 같이 DTO 생성자를 만들 수 있습니다.

    package study.datajpa.dto;
    
    import lombok.Data;
    import study.datajpa.entity.Member;
    
    @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();
        }
    }
    

     

     

     

     

     

     

     

    page를 1부터 시작하기

     

    현재는 page가 0부터 시작합니다.

    page가 1부터 시작하도록 하는 방법이 2가지가 있습니다.

     

    1. 직접 PageRequest 만들기

     

    2. one-indexed-parameters : true 하기

      data:
        web:
          pageable:
            default-page-size: 10
            max-page-size: 2000
            one-indexed-parameters: true


    위 방식의 한계는 반환되는 json의 page 정보는 원래와 똑같이 반환됩니다.

     

     

    추천하는 방식은 그냥 Page를 0부터 시작하는 것 입니다.