[스프링 데이터 JPA] 확장 기능

윤경·2021년 11월 21일
0

JPA

목록 보기
18/22
post-thumbnail

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

실무에서 중요한 내용!!

  • 스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성한다.
  • 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많다.
  • 여러가지 이유로 인터페이스의 메소드를 직접 구현하고 싶다면
    - JPA를 직접 사용(EntityManager)
    - 스프링 JDBC Template 사용
    - MyBatis 사용
    - 데이터베이스 커넥션 직접 사용
    - Querydsl 사용 (할 때 예제처럼 커스텀해 많이 사용함)

사용자 정의 구현 클래스

📍 주의할 점은 MemberRepository 이 부분이 멤버 리포지토리의 이름과 일치해야 한다. 그리고 관례상 그 뒤에 Impl을 붙여줘야 함.

규칙: 리포지토리 인터페이스 이름 + Impl

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

웬만하면 관례를 따라 억지로 수정하지 않는 것이 좋지만^^

Impl 대신 다른 이름으로 변경하고 싶다면

XML 설정을

  <repositories base-package="study.datajpa.repository" repository-impl-postfix="Impl" />

위와 같이 바꾸어주고,

JavaConfig 설정을

@EnableJpaRepositories(basePackages = "study.datajpa.repository", repositoryImplementationPostfix = "Impl")

이와 같이 써주면 된다.

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

그리고

항상 사용자 정의 리포지토리가 필요한 것은 아니다.

그냥 임의의 리포지토리를 만들어도 된다. 예를 들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해 직접 사용해도 된다.

물론 이 경우에는 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다.

즉, 근본적인 고민을 한 번쯤은 해볼 필요가 있다는 것


✔️ 사용자 정의 리포지토리 구현 최신 방식

강의에는 없는 내용이지만 스프링 데이터 2.x 부터는 사용자 정의 구현 클래스에 리포지토리 인터페이스 이름 + Impl을 적용하는 대신 사용자 정의 인터페이스 명 + Impl 방식도 지원한다.

예를 들어 앞선 예제의 MemberRepositoryImpl 대신 MemberRepositoryCustomImpl 같이 구현해도 된다.

기존의 방식보다 이런 방식이 사용자 정의 인터페이스 이름과 구현 클래스 이름이 비슷하므로 더 직관적이다.
추가로 여러 인터페이스를 분리해 구현하는 것도 가능하기 때문에 새롭게 변경된 이 방식을 사용하는 것을 더 권장한다.


[2] Auditing

엔티티를 생성, 변경할 때 기본적으로 등록일, 수정일, 등록자, 수정자를 기록할 수 있는 것이 좋다.

기본적으로 테이블 생성시 남겨놓으면 운영할 때 편하다. 아니 해야한다. 반대로 남겨놓지 않으면 운영할 때 😵

순수 JPA 사용

업데이트 시간을 확인해보면 됨.

JPA 주요 이벤트 어노테이션

  • @PrePersist, @PostPersist
  • @PreUpdate, @PostUpdate

스프링 데이터 JPA 사용

스프링 데이터 JPA를 사용하기 위해서는 꼭 잊지말고 해주어야 하는 설정

  • @EnableJpaAuditing스프링 부트 설정 클래스에 적용 (예제에서는 DataJpaApplication.java)
  • @EntityListeners(AuditingEntityListener.class)엔티티에 적용

사용 어노테이션

  • @CreatedDate
  • @LastModifiedDate
  • @CreatedBy
  • @LastModifiedBy

임의로 랜덤으로 나오도록 했음. 실무에서는 세션 정보나, 스프링 시큐리티 로그인 정보에서 ID를 받으면 된다.

참고로 실무에서 대부분의 엔티티는 등록시간, 수정시간이 필요하지만, 등록자, 수정자는 없을 수도 있다. 그럴 때는 Base 타입을 분리하고 원하는 타입을 선택해 상속하면 된다.

또 참고로 저장시점에 등록일, 등록자는 물론 수정일, 수정자도 같은 데이터가 저장된다.
데이터가 중복 저장되는 것 같지만, 이렇게 해두면 변경 컬럼만 확인해도 마지막 업데이트한 유저를 확인할 수 있으므로 유지보수 관점에서 편리하다.
이렇게 하지 않으면 변경 컬럼이 null일 때 등록 컬럼을 또 찾아야 한다.

저장 시점 저장 데이터만 입력하고 싶으면 @EnableJpaAuditing(modifyOnCreate = false) 옵션을 사용하자.

전체 적용

전체 적용을 위해서는 @EntityListeners(AuditingEntityListener.class를 생략하고 스프링 데이터 JPA가 제공하는 이벤트를 엔티티 전체에 적용하려면 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 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아 바인딩

예를 들어

private final MemberRepository memberRepository;

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

이렇게 했을 때 localhost:8080/members/1 (코드 아래에 멤버를 하나 임시로 생성했기 때문에 가능) 로 접속하면 저장한 userA가 출력된 화면을 얻을 수 있다.

이때, Long id 대신 아래와 같이 Member member로 코드를 바꾸어도 잘 동작한다. (아래 코드의 접속은 members2/1로 접속해야 함)

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

HTTP 요청은 회원 id를 받지만 도메인 클래스 컨버터가 중간에 동작해 회원 엔티티 객체를 반환하기 때문에 가능하다.
도메인 클래스 컨버터도 리포지토리를 사용해 엔티티를 찾는다.

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

즉, 단순 조회용이며 간단간단하게 쓸 용도로만 쓰면 된다!!


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

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

파라미터로 Pageable(인터페이스)을 받는다. 실제로는 org.springframework.data.domain.PageRequest 객체가 생성된다.

포스트맨 GET 결과 ➡️ 전체 꺼내옴 (인덱스는 0부터 시작)

page page=2라고 하면 (다다음 페이지인) 41부터 20개를 꺼냄.

size page=1&size=3으로 바꾸면 그 전에 id 3까지 찾았으니까 4, 5, 6이 출력됨

sort ➡️ 역순으로 정렬시킨 경우. 그리고 id 대신 username을 넣어도 됨. 정렬시키고 기준을 넣어주면 됨.

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

글로벌 설정(스프링부트)

spring:
  data:
    web:
      pageable:
        default-page-size: 10 # 페이징시 default는 20개이므로 한 페이지에 10개씩 나오도록 하고싶을 때
        max-page-size: 2000   # 최대 페이지 값

개별 설정(어노테이션 사용)

    public Page<Member> list(@PageableDefault(size=5, sort="username") Pageable pageable) {   // Page는 인터페이스

이런식으로 @PageableDefault 어노테이션을 사용

접두사

  • 페이징 정보가 둘 이상이면 접두사로 구분
  • @Qualifier에 접두사명 추가 "{접두사명}_xxx"
    Ex) /members?member_page=0&order_page=1

Page 내용 DTO 변환하기

엔티티를 API로 노출시키면 다양한 문제가 발생하므로 엔티티를 반환하는 경우, 꼭 DTO로 변환해 반환해야 한다.

Page는 map()을 지원해 내부 데이터를 다른 것으로 변경할 수 있다.

dto 변환

Page를 1부터 시작하기 위해서는

스프링 데이터 Page는 원래 0부터 시작하는데 이를 1부터 시작하도록 하기 위해서는 두 가지 방법이 있다.

  • 1. Pageable, Page를 파라미터와 응답 값으로 사용하지 않고, 직접 클래스를 만들어 처리한다.
    그리고 직접 PageRequest(Pageable 구현체)를 생성해 리포지토리에 넘긴다. 물론 응답값도 Page 대신 직접 만들어 제공해야 한다.
  • 2. spring.daga.web.pageable.one-indexed-parameters를 true로 설정한다.
    그런데 이 방법은 web에서 page 파라미터를 -1처리할 뿐이라 응답값인 Page에 모두 0 페이지 인덱스를 사용한다는 한계가 있다.

profile
개발 바보 이사 중

0개의 댓글