확장 기능

2024. 8. 26. 23:49·JPA/Spring Data Jpa
목차
  1. 사용자 정의 리포지토리 구현
  2. Auditing
  3. Web 확장 - 도메인 클래스 컨버터
  4. Web 확장 - 페이징과 정렬

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

스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성한다.

그렇게 되면 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현해야 하는 경우 구현해야 하는 기능이 너무 많다

그래서 다양한 스타일로 인터페이스의 메서드를 직접 구현하고 싶은 경우가 있다.

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

사용자 정의 인터페이스

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

사용자 정의 인터페이스 상속

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

    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);

    List<Member> findHelloBy();

    List<Member> findTop3HelloBy();

	...
}

사용자 정의 메서드 호출 코드

@Test
public void callCustom() {
    List<Member> memberCustom = memberRepository.findMemberCustom();
}

사용자 정의 구현 클래스에 규칙이 있다.

  • 규칙: 리포지토리 인터페이스 이름 + `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 같이 구현해도 된다.

최신 사용자 정의 인터페이스 구현 클래스 예제

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

기존 방식보다 이 방식이 사용자 정의 인터페이스 이름과 구현 클래스 이름이 비슷하므로 더 직관적이다.

추가로 여러 인터페이스를 분리해서 구현하는 것도 가능하기 때문에 새롭게 변경된 이 방식을 사용하는 것을 더 권장한다.

Auditing

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

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

순수 JPA 사용

우선 등록일, 수정일 적용

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

확인 코드

@Test
public void jpaEventBaseEntity() throws Exception {
    //given
    Member member = new Member("member1");
    memberRepository.save(member); // @PrePersist 발생

    Thread.sleep(100);
    member.setUsername("member2");

    em.flush(); // @PreUpdate 발생
    em.clear();

    //when
    Member findMember = memberRepository.findById(member.getId()).get();

    //then
    System.out.println("findMember.createdDate = " + findMember.getCreatedDate());
    System.out.println("findMember.updatedDate = " + findMember.getUpdatedDate());
}

JPA 주요 이벤트 어노테이션

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

스프링 데이터 JPA 사용

설정
@EnableJpaAuditing -> 스프링 부트 설정 클래스에 적용해야함

@EntityListeners(AuditingEntityListener.class) -> 엔티티에 적용함

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

주의: DataJpaApplication에 @EnableJpaAuditing도 함께 등록해야 한다.

@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {

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

    @Bean
    public AuditorAware<String> auditorProvider() {
       return () -> Optional.of(UUID.randomUUID().toString());
    }
}

위 설정에서는 UUID 값을 사용했지만 실제는 예를 들어 스프링 시큐리티를 사용하면 시큐리티 컨텍스트 같은데서 세션 정보를 가져와서 아이디를 꺼내야한다. 다른 경우라면 HB 세션에서 유저 아이디를  꺼내야 한다. 그러면 등록되거나 수정될때마다 auditorProvider()를 호출해서 리턴값을 가져가 아래 필드에 자동으로 값을 저장한다.

정리 : 실무에서는 세션 정보나, 스프링 시큐리티 로그인 정보에서 ID를 받음

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

    @LastModifiedBy
    private String lastModifiedBy;

사용 어노테이션

  • @CreatedDate
  • @LastModifiedDate
  • @CreatedBy
  • @LastModifiedBy
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
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;
}

참고

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

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

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

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

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

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

    @LastModifiedBy
    private String lastModifiedBy;
}

참고

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

데이터가 중복 저장되는 것 같지만, 이렇게 해두면 변경 컬럼만 확인해도 마지막에 업데이트한 유저를 확인 할 수 있으므로 유지보수 관점 에서 편리하다. 이렇게 하지 않으면 변경 컬럼이 `null` 일때 등록 컬럼을 또 찾아야 한다. 참고로 저장시점에 저장데이터만 입력하고 싶으면 @EnableJpaAuditing(modifyOnCreate = false)옵션을 사용하면 된다.

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

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;

	// 도메인 클래스 컨버터 사용 전
    @GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Long id) {
        Member member = memberRepository.findById(id).get();
        return member.getUsername();
    }

	// 도메인 클래스 컨버터 사용 후
    @GetMapping("/members/{id}")
    public String findMember2(@PathVariable("id") Member member) {
        return member.getUsername();
    }
    
    @PostConstruct
    public void init() {
        memberRepository.save(new Member("userA"));
    }
}

HTTP 요청은 회원 `id` 를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환

도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾는다.

주의: 

도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다.

(트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)

Web 확장 - 페이징과 정렬

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

Pageable : 페이징에 쓰일 파라미터 정보
Page : 페이징 처리 후 결과 정보

스프링 데이터에서 제공하는 페이징 기능을 웹에서는 바로 바인딩이 되도록 지원을 한다.  

페이징과 정렬 예제

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

컨트롤러에서 파라미터로 Pageable을 받을 수 있다. 그래서 컨트롤러에서 바인딩 될때 파라미터에  Pageable이  있으면 PageRequest라는 객체를 생성해서 객체에 값을 채운 후 Pageable에 주입한다.

Pageable은 인터페이스이고 실제는 org.springframework.data.domain.PageRequest 객체이다.

요청 파라미터

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

요청 파라미터에 조건이 없으면 조회하는 데이터 개수는 기본값이 20개이다.

이러한 기본값을 두가지 방식으로 설정할 수 있다.

기본값 설정 방법

글로벌 설정: 스프링 부트

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

개별 설정

@PageableDefault 어노테이션을 사용

@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public Page<Member> list2(@PageableDefault(size = 12, sort = "username",
        direction = Sort.Direction.DESC) Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page;
}

접두사
페이징 정보가 둘 이상이면 접두사로 구분

@Qualifier에 접두사명 추가 "{접두사명}_xxx"
예제: `/members?member_page=0&order_page=1`

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

Page 내용을 DTO로 변환하기

엔티티를 API로 노출하면 다양한 문제가 발생한다. 그래서 엔티티를 꼭 DTO로 변환해서 반환해야 한다.

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

Member DTO

@Data
 public class MemberDto {
 
     private Long id;
     private String username;
     
     public MemberDto(Member m) {
         this.id = m.getId();
         this.username = m.getUsername();
     } 
}
@GetMapping("/page/members")
public Page<MemberDto> pageDtoList(Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page.map(member -> new MemberDto(member));
}

Page를 1부터 시작하기

스프링 데이터는 Page를 0부터 시작한다. 만약 1부터 시작하려면?

방법1

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

그리고 직접 PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘긴다.

물론 응답값도 Page 대신에 직접 만들어서 제공해야 한다.

@GetMapping("/page/members")
public CustomPage<MemberDto> customPageDtoList(Pageable pageable) {
    PageRequest request = PageRequest.of(1, 2);
    CustomPage<MemberDto> map = memberRepository.findAll(request)
            .map(member -> new MemberDto(member));
    return map;
}

방법2

spring.data.web.pageable.one-indexed-parameters를 true로 설정한다.

그런데 이 방법은 web에서 `page` 파라미터를 `-1` 처리 할 뿐이다.

따라서 응답값인 `Page` 에 모두 0 페이지 인덱스를 사용하는 한계가 있다.

one-indexed-parameters` Page 1요청 ( `http://localhost:8080/members?page=1)

{
    "content": [
    ...
    ],
    "pageable": {
    "offset": 0,
    "pageSize": 10, "pageNumber": 0 //0 인덱스
    },
    "number": 0, //0 인덱스 
    "empty": false
}

 

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

나머지 기능들  (0) 2024.08.27
스프링 데이터 JPA 분석  (0) 2024.08.27
쿼리 메소드 기능  (1) 2024.08.26
공통 인터페이스 기능  (0) 2024.08.25
  1. 사용자 정의 리포지토리 구현
  2. Auditing
  3. Web 확장 - 도메인 클래스 컨버터
  4. Web 확장 - 페이징과 정렬
'JPA/Spring Data Jpa' 카테고리의 다른 글
  • 나머지 기능들
  • 스프링 데이터 JPA 분석
  • 쿼리 메소드 기능
  • 공통 인터페이스 기능
an_jjin
an_jjin
공부한 내용을 정리하는 개발 기록 블로그
an_jjin
An Devlog
an_jjin
전체
오늘
어제
  • 분류 전체보기
    • JAVA
      • 초급
      • 중급1
      • 중급2
      • 고급1
    • Spring
      • 핵심 원리 기본
      • MVC1
      • MVC2
      • DB1
      • 락
      • 디자인 패턴
      • AOP
    • JPA
      • JPA 기본
      • JPA 활용 1
      • JPA 활용 2
      • Spring Data Jpa
      • JPA 정리
    • 프로젝트
      • Filmeet
      • FitTrip
      • Kidsping
    • CS
      • 기술 면접 대비 CS 전공 핵심요약집
    • 네트워크
      • HTTP
      • WebSocket
    • 데이터베이스
    • 운영체제
      • 뇌를 자극하는 윈도우즈 시스템 프로그래밍
    • Git
    • Kafka
    • Docker
    • [LG유플러스] 유레카
    • 회고

블로그 메뉴

  • 홈
  • 태그
  • 방명록

링크

공지사항

인기 글

태그

자바
Websocket
lg 유레카
spring websocket
transactional outbox pattern
queryplan
캐시
lg 유플러스 유레카 sw 교육
동시성
프록시 팩토리
객체지향
after_commit
STOMP
db부하
선착순 응모 시스템
트랜잭션 아웃박스 패턴
before_commit
Transactional Outbox
lg 유플러스 유레카
redis 분산락
redlock
이벤트 응모 시스템
스레드
lg 유플러스 유레카 sw 교육 후기
도커
분산락
redis
AOP
lg 유플러스 유레카 후기
빈 후처리기

최근 댓글

최근 글

hELLO· Designed By정상우.v4.6.1
an_jjin
확장 기능

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.