Lined Notebook

[Spring Data JPA] 37. JPA 사용자정의 리포지토리와 Auditing

by ymkim

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

  • 스프링 데이터 JPA 리포지토리는 인터페이스로만 정의하고 구현체는 스프링이 자동 생성
  • 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많음
  • 만약 다양한 이유로 인터페이스의 메서드를 직접 구현하고 싶다면?
    • JPA 직접 사용
    • 스프링 JDBC Template 사용
    • Mybatis 사용
    • 데이터베이스 커넥션 직접 사용 등등..
    • Querydsl 사용
package org.springframework.data.jpa.repository;

import java.util.List;

import javax.persistence.EntityManager;

import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;

/**
 * JPA specific extension of {@link org.springframework.data.repository.Repository}.
 *
 * @author Oliver Gierke
 * @author Christoph Strobl
 * @author Mark Paluch
 * @author Sander Krabbenborg
 * @author Jesse Wouters
 * @author Greg Turnquist
 */
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.CrudRepository#findAll()
	 */
	@Override
	List<T> findAll();

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort)
	 */
	@Override
	List<T> findAll(Sort sort);

    //..중략
}

SOLID 원칙의 인터페이스 분리 원칙을 한번 더 찾아보면서 숙지

해당 인터페이스 안에 모든 추상메서드가 구현이 되어있으며 스프링 데이터 JPA에 의해 모든 메서드의 구현체가 생성이 된다. 다양한 이유로 인해 메서드를 직접 구현하고 싶은 경우 인터페이스의 모든 추상 메서드를 하위 계층에서 반드시 구현해야 한다는 규약이 존재한다.

 

이러한 규약을 해소하기 위해 스프링 데이터 JPA는 다음과 같은 방법을 제공 한다. 아래 소스를 보면서 하나씩 부가적인 기능을 살펴보자.

01-1. MemberRepositoryCustom 인터페이스 생성

public interface MemberRepositoryCustom {

    List<Member> findMemberCustom();
}
  • 일단 위와 같이 인터페이스 안에 추상 메서드를 선언 해둔다

01-2. MemberRepositoryImpl 클래스 생성

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {

    private final EntityManager em;

    @Override
    public List<Member> findMemberCustom() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
}

사용자 정의 리포지토리 기능은 주로 QueryDSL에서 많이 사용이 된다

  • 다음으로 Custom 인터페이스를 구현하는 MemberRepositoryImpl 생성
  • 인터페이스 구현 후 findMemberCustom 메서드를 재정의하여 사용한다
  • Impl 대신 다른 방식이 있지만 웬만하면 관례를 따르는것이 좋다

01-3. MemberRepository에 MemberRepositoryCustom 추가

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

    //..중략
}
  • 위와 같이 MemberRepository가 MemberRepositoryCustom을 상속하도록 수정
  • 스프링 데이터 JPA는 하나의 규칙이 존재
    • 스프링 데이터 JPA 리포지토리와 이름 동일, Impl이 붙어야함
    • ex) MemberRepositoryImpl

01-4. 주의해야 하는 부분

@Repository
@RequiredArgsConstructor
public class MemberQueryRepository {

    private final EntityManager em;

    List<Member> findAllMembers() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
}

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

예를 들어 사용자 정의(Custom) 리포지토리를 생성하지 않고 MemberQueryRepository 클래스로 생성하고 스프링 빈으로 등록하여 그냥 직접 사용을 해도 상관이 없다. 물론 이 경우 스프링 데이터 JPA와는 무관하게 동작한다.

즉, 우리가 신경써야 하는 부분은 커멘드(Command)와 쿼리(Query)를 분리하고 핵심 비즈니스 로직과 핵심이 아닌 비즈니스 로직(화면 딴)을 분리하는것에 신경을 써야 한다.

02. Auditing

엔티티를 생성하거나 변경할 때 변경한 사람과 시간을 추적하고 싶은 경우 사용 하는 기능을 Auditing 기능이라 한다. 기본적으로 우리는 개발을 할 때 거의 필수적으로 남기는 등록일, 수정일 같은 데이터가 존재하는데 지금부터 해당 기능에 대해 알아보자.

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

02-1. JPA의 주요 이벤트 어노테이션

  • @PrePersist
    • persist 이전
  • @PostPersist
    • persists 이후
  • @PreUpdate
    • update 이전
  • @PostUpdate
    • update 이후

02-2.순수 JPA를 사용하여 Auditing 적용

@MappedSuperclass
public class JpaBaseEntity {

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

    // Persist 하기 전
    @PrePersist
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        this.createdDate = now;
        this.updatedDate = now;
    }

    // Update하기 전
    @PreUpdate
    public void preUpdate() {
        updatedDate = LocalDateTime.now();
    }
}
  • JPA에서 속성만 상속 받기 위해서는 @MappedSuperclass를 사용해야 한다
  • 진짜 상속 관계는 아니고, 속성(Data)만 공유하는 역할이다
2022-06-21 21:37:23.963 DEBUG 13600 --- [           main] org.hibernate.SQL                        :
create table member (
       member_id bigint not null,
        created_date timestamp,
        updated_date timestamp,
        age integer not null,
        user_name varchar(255),
        team_id bigint,
        primary key (member_id)
    )
  • 위와 같이 모든 테이블에 created_date, updated_date가 추가 된다
  • 즉, 공통 속성을 내려 받을 때 @MappedSuperClass를 사용한다
@Test
public void JpaEventBaseEntity() throws Exception {
    //given
    Member member = new Member("member1");
    memberRepository.save(member); //@PrePersist

    System.out.println("=====");

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

    System.out.println("=====");

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

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

    //then
    System.out.println("findMember = " + findMember.getCreatedDate());
    System.out.println("findMember = " + findMember.getUpdatedDate());
}
  • 로그 확인 시 등록일, 수정일이 정상적으로 등록 됨

02-3. 스프링 데이터 JPA Auditing 적용

설정

스프링 데이터 JPA의 Auditing 사용을 위한 설정

  • @EnableJpaAuditing -> 스프링 부트 클래스에 적용 해야 한다
  • @EntityListeners(AuditingEntityListener.class) -> 엔티티에 적용

사용 어노테이션

엔티티에 적용이 되는 어노테이션

  • @CreatedDate
  • @LastModifiedDate
  • @CreatedBy
  • @UpdatedBy
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {

	public static void main(String[] args) {
		SpringApplication.run(DataJpaApplication.class, args);
	}
}
  • 기존 DataJpaApplication 클래스에 @EnableJpaAuditing을 추가 한다
  • 스프링 데이터 JPA가 기본적으로 제공하는 Auditing 기능
package study.datajpa.entity;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
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)
@Getter
@MappedSuperclass
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;
}
  • 우선 위에서 JpaBaseEntity를 생성 하였을때와 동작 방식은 동일
  • 하지만 스프링 부트 클래스에 추가적인 작업을 해줘야 한다
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {

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

	@Bean
	public AuditorAware<String> auditorProvider() {
		// 실제로는 HttpSession, Spring Security context에서 뽑아서 등록
		return () -> Optional.of(UUID.randomUUID().toString());
	}
}
@Bean
public AuditorAware<String> auditorProvider() {
    // 실제로는 HttpSession, Spring Security context에서 뽑아서 등록
    // return AuditorAware<String>
//		return () -> Optional.of(UUID.randomUUID().toString());
    return new AuditorAware<String>() {
        @Override
        public Optional<String> getCurrentAuditor() {
            return Optional.empty();
        }
    }
}
  • AuditorAware
  • auditorProvider() { } 추가
  • 임의의 랜덤 UUID를 반환하는 메서드
    • 실제로는 HttpSession에서 유저 값, Spring Security 값을 저장
  • 대략 람다식을 풀면 위와 같이 함수형 인터페이스를 반환 한다
@Test
public void JpaEventBaseEntity() throws Exception {
    //given
    Member member = new Member("member1");
    memberRepository.save(member); //@PrePersist

    System.out.println("=====");

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

    System.out.println("=====");

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

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

    //then
    System.out.println("findMember = " + findMember.getCreatedDate());
    System.out.println("findMember = " + findMember.getLastModifiedDate());
}

JPA를 이용해 자동으로 시간, 작성자 추가하기

  • 테스트 코드 실행 시 이전과 동일한 결과를 얻을 수 있다

참고 자료

블로그의 정보

기록하고, 복기하고

ymkim

활동하기