Spring/Spring Data JPA

확장 기능

민철킹 2021. 6. 15. 13:14

1. 사용자 정의 Repository

 

스프링 데이터 JPA가 제공하는 기능을 사용하기 위해 우리는 JpaRepository 인터페이스를 상속받아 사용하고 구현체는 스프링이 자동으로 생성하여 주입시켜준다. 만약 이를 개발자가 직접 구현하려한다면(implements) 구현해야 하는 메서드들이 굉장히 많기 때문에 매우 번거롭고 까다롭다.

아래와 같은 이유로 만약 인터페이스의 메서드를 직접 구현하려한다면?

  •  JPA 직접 사용( EntityManager )
  • 스프링 JDBC Template 사용
  • MyBatis 사용
  • Querydsl 사용
  • 데이터베이스 커넥션 직접 사용 등등...

 

커스텀 메서드를 딱 하나만 구현하고 싶을 때 사용하는 방법이다.

먼저 커스텀 인터페이스를 만들고 그 안에 메서드를 추가한다.

그 뒤에 인터페이스 구현체를 통해 메서드를 구현한다.

이제 JpaRepository와 커스텀 인터페이스를 같이 상속받아 주면 스프링 데이터 jpa가 구현체를 찾아 같이 엮어주어 우리가 구현한 커스텀 메서드를 사용할 수 있게 된다.

구현 메서드 사용

동적 쿼리를 위해 querydsl을 사용할 때 위와 같이 커스텀 메서드를 방식을 많이 사용한다. 간단한 기능은 스프링 데이터 jpa가 제공하는 기능으로 거의 해결이 가능하다.

추가로 이 방법을 사용할 때 지켜야하는 규칙이 존재한다. 이 규칙은 커스텀 인터페이스를 구현한 구현체에 해당되는 내용인데, "사용할 Repository클래스명 + Impl"로 구현체 클래스의 이름을 작성해야한다.

이 메서드를 사용할 MemberRepository + Impl

Caused by: java.lang.IllegalArgumentException: Failed to create query for method public abstract java.util.List study.datajpa.repository.MemberRepositoryCustom.findMemberCustom()! No property findMemberCustom found for type Member!

이 규칙을 지키지 않으면 위와 같은 오류가 발생한다.

 

 

Impl 대신 다른 규칙을 사용하고 싶을 땐 수동으로 규칙을 변경하고 싶다.(왠만하면 관례를 따르는 것이 좋지만..)

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

//-------------------------------------------------------------
// JavaConfig 설정
@EnableJpaRepositories(basePackages = "study.datajpa.repository",
repositoryImplementationPostfix = "Impl")

Impl 대신에 다른 postfix를 사용할 수도 있긴함.

 

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

 

스프링 데이터 2.x 부터는 사용자 정의 구현 클래스에 리포지토리 인터페이스 이름 + Impl 을 적용하는 대신에 사용자 정의 인터페이스 명 + Impl 방식도 지원한다. 예를 들어서 위 예제의 MemberRepositoryImpl 대신MemberRepositoryCustomImpl 같이 구현해도 된다. 이 방식이 사용자 정의 인터페이스 이름과 구현 클래스 이름이 비슷하므로 더 직관적이다. 

 


2. Auditing

 

엔티티를 생성, 변경할 때 변경한 사람과 시간을 추적하고 싶을 때 사용하는 기능이다.

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

 

먼저, 순수 JPA를 사용하여 등록일, 수정일을 관리하도록 해보자.

@PrePersist, @PreUpdate 애노테이션을 통해 등록일, 수정일을 관리할 수 있다.

@MappedSuperclass 애노테이션을 통해 속성만 내려받아 사용할 수 있도록 한다. 이 애노테이션이 있는 클래스는 엔티티가 아니므로 테이블과 매핑도 되지 않는다. 

이렇게 상속받아 사용하게 되면 Member 엔티티에 createdDate, updatedDate라는 컬럼을 따로 만들어주지 않아도 사용 가능하다. 각 엔티티에 공통으로 사용되는 속성들을 분리하는 용도로 사용될 수 있다.

 

@MappedSuperclass를 상속하는 것 만으로 Member 엔티티에 정의해주지 않은 칼럼이 생성되는 것을 확인할 수 있다.

 

save가 되는 시점에 @PrePersist에 의해 createdDate와 updatedDate에 현재 시각이 저장된다.

그 후 Thread.sleep을 통해 강제로 텀을 준 뒤에 member의 값을 바꾸었다. flush()되며 값이 수정될 때 @PreUpdate가 발생하여 updatedDate의 값이 다시 현재 시각으로 저장된다. 따라서 예상한 테스트 결과는 createdDate보다 updateDate가 더 이후여야 한다.

예상한 결과와 동일하다.

참고로 @PostPersist, @PostUpdate도 존재한다. 스프링 MVC의 @PreConstruct, @PostConstruct와 동일한 개념이다.

이러한 @MappedSuperclass를 사용하면 공통 필드의 중복 코드 없이 상속만으로 해결할 수 있다는 장점이 존재한다.

 


이번에는 스프링 데이터 JPA를 사용하여 동일한 기능을 사용해보자. 훨씬 더 깔끔하게 구현이 가능하다.

먼저 스프링 부트 설정 클래스에 @EnableJpaAuditing이라는 애노테이션을 추가한다.(추가안하면 동작 안해요)

동일하게 @MappedSuperclass 애노테이션을 작성하고 @EntityListeners 애노테이션을 통해 엔티티가 이벤트에 의해 동작함을 알려준다.

내부에는 앞서 이야기한 방식과 다르게 메서드를 직접 구현하는 것이 아니라 @CreatedDate, @LastModifiedDate 라는 애노테이션을 통해 간단하게 사용할 수 있다. 동일하게 실제 사용할 엔티티에서 Extends를 하여 사용이 가능하다.

 

 

등록자, 수정자를 관리하려면 어떻게 해야할까?

이는 스프링 데이터 jpa가 제공하는 @CreatedBy, @LastModifiedBy 애노테이션을 사용하여 해결이 가능하다.

위와 같이 애노테이션을 사용하면 명시적으로 표시하기 때문에 어떤 기능을 하는지 알아보기에도 매우 좋다.

그럼 등록자와 수정자에는 값이 어떤 방식으로 넣어야하는가?

등록자, 수정자를 처리해주는 AuditorAware 스프링 빈을 등록하여 사용하는데 이해하기 쉽도록 람다식을 풀어보면

getCurrentAuditor를 구현하는 것이다. 현재는 UUID를 사용하여 random값이 들어가도록 임시로 만들어 놓았지만 실제 사용할 때에는 세션 정보나, 스프링 시큐리티 로그인 정보에서 id를 제공받아 값을 넣어주는 식으로 사용한다.

 

 

저장시점에 등록일, 등록자는 물론이고, 수정일, 수정자도 같은 데이터가 저장된다.

데이터가 중복 저장되는 것 같지만, 이렇게 해두면 변경 컬럼만 확인해도 마지막에 업데이트한 유저를 확인 할 수 있으므로 유지보수 관점에서 편리하다.

이렇게 하지 않으면 변경 컬럼이 null 일때 등록 컬럼을 또 찾아야 한다. 

참고로 저장시점에 저장데이터만 입력하고 싶으면 @EnableJpaAuditing(modifyOnCreate = false) 옵션을 사용하면 된다.(이 옵션을 사용하게 되면 데이터를 저장할 때 created에만 값이 들어가고 updated에는 null이 들어간다.)

 

만약 @EntityListeners 애노테이션을 생략하고 싶으면 "META-IMF/orm.xml"이라는 파일을 만들어 아래와 같이 작성하면된다. 이는 JPA 기본 스펙에서 제공되는 기능이다.

<?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>

 

 

등록일, 수정일은 거의 모든 엔티티에서 필요하지만 등록자, 수정자는 테이블에 따라 필요하지 않은 경우도 다수 존재한다.

따라서 BaseTimeEntity에는 등록일, 수정일을 만들어 놓고 이를 상속받은 BaseEntity에 등록자, 수정자를 만들어 분리하여 필요에 따라 사용하도록 하자!

 

 

 

 

항상 느끼지만 스프링mvc가 제공하는 기능, 스프링 데이터 jpa가 제공하는 기능 등등 모두 편리하게 사용이
가능하도록 만들어주는 Tool, 도구에 불과하므로 그것의 핵심 기능을 잘 알고 있는 것이 중요하다.
스프링 데이터 JPA도 JPA를 잘 이해하면 굉장히 쉬운 것 같다.

 


3. Web 확장 기능

 

3-2. Domain Class Converter

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

 

먼저 일반적으로 흔히 사용하는 Controller를 하나 만들었다. @RestController 이기 때문에 바로 값을 반환할 수 있고 GetMapping을 통해 요청을 매핑시켰다. 이 때 PathVariable을 통해 /members/1 과 같이 id가 http 파라미터로 넘어온다면 finById를 통해 일치하는 회원을 찾고 그 회원의 이름을 화면에 출력한다.

 

여기서 스프링 데이터 JPA를 사용한다면 위와 같이 작성하여 바로 엔티티와 매핑시킬 수 있다.

도메인 클래스 컨버터가 중간에서 동작하여 회원 엔티티 객체를 리포지토리에서 찾는 것이다. 하지만 이렇게 쿼리가 단순하게 동작하는 경우는 사실상 잘 없고, PK를 저렇게 외부에 보여주는 식으로 사용하는 것은 좋지 않다. 또한, 도메인 클래스 컨버터를 사하여 엔티티를 파라미터로 받는다면 단순 조회용으로만 사용해야한다.트랜잭션 범위 바깥에서 엔티티를 조회한 것이기 때문에, 엔티티를 변경하여도 DB에 실제로 반영되지 않기 때문이다.


페이징과 정렬

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

 

현재 회원 저장소에 100명의 회원이 저장되어 있다.

postman을 통해 요청을 전달하면

 

이와 같이 페이징이 완료되어 현재 0번부터 20명의 회원이 페이징된 것을 확인할 수 있다. 아래의 pageable을 보면 전체는 100명의 데이터가 존재하고, 전체 페이지는 5페이지이며 현재가 첫번째 페이지임을 알려준다.

쿼리 파라미터로 페이지의 번호를 추가로 전송하면 해당 페이지의 내용이 반환된다.

"?page=0&size=3" 과 같이 size도 지원하므로 이와 같이 입력하면 페이지의 size가 3이므로 총 34개의 페이지가 생성되고 그 중 0번째 페이지가 반환되는 것이다. 만약 "?page=2&size=3"과 같이 입력한다면 size가 3인 총 34개의 페이지 중에서 

2페이지에 해당하는 3개의 데이터를 가져오는 것이다.

 

 

컨트롤러에서 바인딩될 때 Pageable 인터페이스를 보고, PageableRequest 객체를 생성해서 값을 채워 injection해준다.

 

사용하는 파라미터들 

- sort=id,desc ==> id를 기준으로 내림차순 정렬, 정렬 조건 여러개 추가 가능

- page ==> 현재 페이지, 0부터 시작

- size ==> 한 페이지에 노출할 데이터 건수

 

 

글로벌 설정

페이지 사이즈를 지정해주지 않으면 default값으로 20개가 설정되어있고, 최대 페이지 사이즈는 2000개로 설정되어 있다.

이를 변경하고 싶으면 application.yml에서 값을 변경해주면 된다.

이와 같이 글로벌 설정을 원하는 대로 변경할 수 있다.

 

 

개별 설정

@PageableDefault를 사용하여 size나 정렬 기본값을 개별로 설정할 수 있다. 당연히 개별 설정이 글로벌 설정보다 우선 순위가 높다.

 

접두사

페이징 정보가 둘 이상이면 접두사로 구분할 수 있다.

@Qualifier에 접두사명을 추가한다. ex) /members?member_page=0?order_page=1

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

 

Page 내용을 DTO로 변환

앞에서 공부한 내용이지만 항상 엔티티를 그대로 사용하지말고 DTO로 변환해야한다.

공부를 위해 잠깐 null 사용함..

 

 

페이지 1부터 시작하기

Pageable, Page를 파리미터와 응답 값으로 사용히지 않고, 직접 클래스를 만들어서 처리하고 PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘겨 custom하게 사용할 수 있다. 물론 응답값도 Page 대신에 직접 만들어서 제공해야 한다.

 

두번째 방법으로 "spring.data.web.pageable.one-indexed-parameters"를 true로 설정하여 페이지를 0이 아닌 1부터 시작하게 할 수 있다. 하지만 이 방법을 사용하게 되면 pageable 객체 안의 pageNumber, number 등의 정보가 맞지 않는다.이 정보들은 모두 0을 기준으로 동작하는 것이기 때문이다. 근데 굳이 이 방법을 쓸 일이 있을까 싶음 ㅋㅋ

 

 

반응형

'Spring > Spring Data JPA' 카테고리의 다른 글

나머지 기능들  (0) 2021.06.17
Spring Data JPA 분석  (0) 2021.06.16
JPA Hint, Lock  (0) 2021.06.14
@EntityGraph  (0) 2021.06.14
벌크성 수정 쿼리  (0) 2021.06.14