백엔드/스프링 데이터 JPA

[Spring Data JPA] [5] 확장 기능

RE-Heat 2023. 9. 10. 23:33

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard

 

실전! 스프링 데이터 JPA - 인프런 | 강의

스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.

www.inflearn.com

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

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

스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동으로 생성한다. 그런데 스프링 데이터 JPA가 제공하는 기능 외에 추가로 필요한 기능이 있다면 어떻게 해야 할까?

인터페이스를 상속받아 직접 구현하는 건 구현해야 할 기능이 너무 많아 배보다 배꼽이 큰 일이다.

해결책 : 직접 사용자 정의 인터페이스를 구현한 뒤 JPA 리포지토리를 상속받아 사용하면 된다.

 

MemberRepositoryCutom - 사용자 정의 인터페이스

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

 

MemberRepositoryCutomImpl - 사용자 정의 인터페이스 구현 클래스

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom{
    private final EntityManager em;

    @Override
    public List<Member> findMemberCustom() {
        return em.createQuery("select m from Member m")
                .getResultList();
    }
}
  •  MemberRepositoryCutom을 상속한 뒤 findMemberCutom()을 구현하면 된다.

 

MemberRepository - 기존 MemberRepository에 사용자 정의 인터페이스 추가

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {}
  • extends에 MemberRepositoryCutom 추가

 

MemberRepositoryTest

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

실제로 쿼리문이 잘 날아가는 걸 확인할 수 있다.

◎ 지켜야 할 규칙

사용자 정의 구현 클래스의 이름은 리포지토리 인터페이스의 이름 + Impl로 만들어야 한다. 이 규칙을 맞춰야 스프링 데이터 JPA가 인식해 스프링 빈으로 등록한다.

예시) MemberRepositoryImpl

 

■Impl 대신 다른 이름으로 변경하는 방법

① XML 설정

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

② JavaConfig 설정

@EnableJpaRepositories(basePackages = "study.datajpa.repository", 
                       repositoryImplementationPostfix = "Impl"
  • 위 방법으로 이름을 바꿀 수 있지만, 다른 사람이 쉽게 알아볼 수 있도록 관례를 따르는 게 좋다. 

 

※ 바뀐 부분

스프링부트 2.X부터는 사용자 정의 구현 클래스에 리포지토리 인터페이스 이름 + Impl 대신

사용자 정의 인터페이스 명 + Impl 방식도 지원한다.

예시) MemberRepositoryCutomImpl도 가능하다는 의미다.

 

[2] Auditing

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

■ 순수 JPA 사용

JpaBaseEntity

@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();
    }
}
  • @MappedSuperClass - 매핑 정보 상속용 애노테이션 
    • 공통 정보가 필요할 때 사용하는 방법이다.
    • 이 애노테이션을 붙이지 않으면 테스트 코드 실행 시 JpaEntity의 필드 값이 들어가지 않는다.

@MappedSuperClass 쓰기 전과 후(우)

JPA 주요 이벤트 애노테이션

  • @PrePersist : 해당 엔티티를 저장하기 이전
  • @PostPersist : 해당 엔티티를 저장한 이후
  • @PreUpdate : 해당 엔티티를 업데이트하기 이전
  • @PostUpdate : 해당 엔티티를 업데이트 한 이후

 

Member

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

테스트 결과 현재 시각 확인 가능

 

참고] 콘솔창 확인 후 DB에서 값을 확인했는데, 없으면 Test에 @Rollback(value = false)을 붙였는지 확인 필요

 

■ 스프링 데이터 JPA 사용

① 설정

  • @EnableJpaAuditing → 스프링 부트 클래스에 적용
  • @EntityListeners(AuditingEntityListener.class) → 엔티티에 적용 업데이트 한 이후

 

BaseEntity

@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;
}
  • @EntityListeners: 엔티티의 변화를 감지하여 엔티티와 매핑된 테이블 데이터를 조작한다.
    • AuditingEntityListener 클래스는 엔티티의 영속·수정 이벤트를 감지하는 역할을 한다.
  • @CreatedDate: 데이터 생성 날짜 자동 저장 어노테이션
    • @Column(updatable = false)를 적용해 생성 날짜가 수정되지 않도록 한다.
  • @LastModifiedDate: 데이터 수정 날짜 자동 저장 어노테이션
  • @CreatedBy: 데이터 생성자 자동 저장 어노테이션
  • @LastModifiedBy: 데이터 수정자 자동 저장 어노테이션

 

등록자·수정자를 처리하는 AuditorAware 스프링 빈 등록

@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());
    }
}
  • 등록 수정될 때마다 AuditorAware를 호출해 결과물을 꺼내간다.
  • 세션 정보나 스프링 시큐리티 로그인 정보에서 등록·수정자를 꺼내가야 하나, 여기선 임의의 값을 넣어 줌.

 

실행결과

 

참고]

등록일·수정일 => BaseTimeEntity

등록자·수정자 => BaseTimeEntity를 상속한 BaseEntity

실무에선 둘로 나눈 후 필요한 곳에서 각각 쓸 수 있도록 변경

 

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

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

 

■ 전체 적용

@EntityListener(AuditingEntityListener.class)를 생략하고 스프링 데이터 JPA가 제공하는 이벤트를 엔티티 전체에 적용하려면 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>
  • 실무에선 적용할 숫자가 적으면 xml 대신 @EntityListener(AuditingEntityListener.class)를 붙인다.

 

 

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

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

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

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

 

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

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

 

※ 주의사항

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

(트랜잭션이 없는 범위에서 엔티티를 조회했으므로 변경감지가 먹히지 않는다.)

 

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

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

 

■ 페이징과 정렬 예제

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

파라미터로 Pageable을 받을 수 있다.

 

① 요청 파라미터

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

② 기본값 변경 방법

▶ 글로벌 설정

application.yml

data:
  web:
    pageable:
      default-page-size: 10
      max-page-size: 2000
  • spring.data.web.pageable.default-page-size=20 : 기본 페이지 사이즈 (default값은 20개)
  • spring.data.web.pageable.max-page-size=2000 : 최대 페이지 사이즈

▶ 개별 설정

@PageableDefault 애노테이션 사용

@GetMapping(value = "/members") 
public String list(@PageableDefault(size = 12, sort = "username", direction = Sort.Direction.DESC) Pageable pageable) {
	   Page<Member> page = memberRepository.findAll(Pageable);
       return page;
}

 

■ 접두사

예제: /members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable, 
@Qualifier("order") Pageable orderPageable, ..

페이징 정보가 둘 이상일 땐 접두사로 구분한다.

 

■ Page 내용을 DTO로 변환하기

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

map을 이용하면 쉽게 해결할 수 있다.

 

MemberDto

@Data
public class MemberDto { 
    private Long id;
    private String username; 
    
    public MemberDto(Member m) {
        this.id = m.getId();
        this.username = m.getUsername(); 
    }
}

 

MemberController

Page.map() 사용

Page.map() 코드 최적화

 

■ Page 1부터 시작하기

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


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

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

id 1부터 값이 나오나 Page의 number는 0부터 시작.