인프런/실전! 스프링 데이터 JPA

3) 확장 기능 - 사용자 정의 리포지토리 구현, Auditing

backend dev 2024. 6. 12.

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

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

스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많음

 

다양한 이유로 인터페이스의 메서드를 직접 구현하고 싶다면?

JPA 직접 사용( EntityManager )하고 싶다거나

스프링 JDBC Template 사용한다거나

MyBatis 사용한다거나

데이터베이스 커넥션 직접 사용 등등...

Querydsl 사용 

과 같은 여러 이유에서 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하고 싶다면?

public interface MemberRepository extends JpaRepository<Member, Long> {

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

    boolean existsByUsername(String username);

    List<Member> findByUsername(@Param("username") String username);

    @Query("select m from Member m where m.username = :username and m.age = :age")
    List<Member> findUser(@Param("username") String username,@Param("age")int age);

    @Query("select m.username from Member m")
    List<String> findUserNameList();

    @Query("select new study.data_jpa.dto.MemberDto(m.id, m.username,t.name) from Member m join m.team t")
    List<MemberDto> findMemberDto();

    @Query("select m from Member m where m.username in :names")
    List<Member> findByNames(@Param("names")List<String> names);

    @Query("select m from Member m where m.username in :names")
    Stream<Member> fff(@Param("names")List<String> names);

    Page<Member> findByAge(int age, Pageable pageable); // Pageable에는 쿼리에 대한 조건이 들어간다.

    @Modifying(clearAutomatically = true) // 이 어노테이션이 있어야 순수 jpa에서 사용했던 executeUpdate()를 실행해준다. 이게 없다면 getSingleResult()같은걸 실행해준다. 해당 어노테이션으로 수정이라는것을 명시해준다.
    @Query("update Member m set m.age = m.age +1 where m.age > :age")
    int bulkAgePlus(@Param("age") int age);

    @Query("select m from Member m left join fetch m.team")
    List<Member> findMemberFetchJoin();
    
}

이전에 구현한 멤버리포지토리를 보면 메소드를 정의만 하고 직접 구현은 JPA에게 맡겼다.

 

사용자 정의 인터페이스

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

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

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

    @Override
    public List<Member> findMemberCustom() {
        return List.of();
    }
}

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

->

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

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

 

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

 

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

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

이제 사용자 정의 인터페이스에 정의한 findMemberCustom()이라는 메소드를 사용하면

내가 만든 구현 클래스에 구현한대로 동작한다.

 

 

 

참고

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

 

 

핵심비즈니스로직이 담긴 리포지토리와 화면에 fit하게 DTO를 반환하는 리포지토리를 나눠서 구성하는것이 좋다.

항상 사용자 정의 리포지토리가 필요한 것은 아니다. 그냥 임의의 리포지토리를 만들어도 된다. 

예를들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 그냥 직접 사용해도
된다. 물론 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다.

 

엔티티를 이용하는 쿼리는 jpa리포지토리 인터페이스를 상속받아서 사용하는 리포지토리에 정리하고

 

DTO등 복잡한 쿼리는 따로 순수 jpa를 이용하는 리포지토리 클래스를 생성해서 구현하도록 구성

[스프링빈으로 등록하고 필요한곳에서 주입받으면 됨으로]

 

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

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

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

예를 들어서 위 예제의 MemberRepositoryImpl 대신에 MemberRepositoryCustomImpl 같이 구현해도 된다

 

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

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

 


 

Auditing

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

 

- 등록일

- 수정일

- 등록자

- 수정자

 

순수 JPA 사용

등록일, 수정일 적용

@MappedSuperclass
public class JpaBaseEntity {

    @Column(updatable = false) // createDate는 생성될때 값이 들어가고 수정되면 안되는 컬럼이므로 수정불가하게 설정, 실수로 바꿔도 DB에 업데이트 되지않는다.
    private LocalDateTime createDate;
    private LocalDateTime updateDate;

    @PrePersist // persist 동작전에 실행할 메소드 설정 [ 저장전 아래 메소드 실행된다. ]
    public void prePersist() {
        createDate = LocalDateTime.now();  // 현재시간을 넣어준다.
        updateDate = LocalDateTime.now();
    }

    @PreUpdate // update되기전에 아래 메소드 실행된다.
    public void preUpdate() {
        updateDate = LocalDateTime.now(); // 수정시간을 현재시간으로 수정한다.
    }
}

이렇게 만들어놓고

public class Member extends JpaBaseEntity{

 

상속을 통해 받아온다.

 

공통된 부분을 따로 클래스로 빼두고

상속을 이용하여 공통속성을 받아오도록 구성했다.

@MappedSuperclass

진짜 엔티티 상속관계가 아닌, 공통속성을 내려받기위한 클래스에는 @MappedSuperclass 어노테이션을 붙여줘야한다.

 
 

7) 고급 매핑 [ 상속관계 매핑 -조인,단일,각각] , MappedSuperclass

상속관계 매핑 • 관계형 데이터베이스는 상속 관계가 없다. • 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다. • 상속관계 매핑: 객체의 상속과 구조와         DB의 슈

keeeeeepgoing.tistory.com

 

 

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

save메소드는 persist로 동작하므로 @PrePersist가 동작한다.

 

set으로 인해 엔티티 변경이 생겼으므로

flush할때 변경감지로 update 쿼리가 발생한다.

그때 @Preupdate로 지정한 메소드가 실행된다.

 

 

JPA 주요 이벤트 어노테이션
@PrePersist, @PostPersist
@PreUpdate, @PostUpdate

 

위의 방법은 순수 jpa로 문제를 해결하는방법

 

스프링 데이터 JPA 사용

설정

@EnableJpaAuditing -> 스프링 부트 설정 클래스에 적용해야함
@EntityListeners(AuditingEntityListener.class) -> 엔티티에 적용
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
    public static void main(String[] args) {
       SpringApplication.run(DataJpaApplication.class, args);
    }
}

 

사용 어노테이션

- @CreatedDate

- @LastModifiedDate

- @CreatedBy

- @LastModifiedBy

 

스프링 데이터 Auditing 적용 - 등록일, 수정일 , 등록자, 수정자

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

 

 

 

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

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

    @Bean
    public AuditorAware<String> auditorProvider() { // 엔티티가 등록되거나 수정될때마다 이 메소드가 호출되서 반환값을 생성자,수정자 필드에 채워준다.
//     return new AuditorAware<String>() {
//            @Override
//            public Optional<String> getCurrentAuditor() {
//                return Optional.of(UUID.randomUUID().toString());
//            }
//        };
       //위의 코드를 람다식으로 한것이 아래코드
       return () -> Optional.of(UUID.randomUUID().toString());
    }
}

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

 

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

return () -> Optional.of(UUID.randomUUID().toString());

즉 이부분을 세션에서 로그인정보를 꺼내서 반환해주면 된다.

그 반환값이 

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

@LastModifiedBy
private String lastModifiedBy;

해당 필드에 담기는것

 

 

참고

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

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

public class BaseTimeEntity {
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;
    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}
public class BaseEntity extends BaseTimeEntity {
    @CreatedBy
    @Column(updatable = false)
    private String createdBy;
    @LastModifiedBy
    private String lastModifiedBy;
}

 

 

만약 등록일만 필요하다면 따로 클래스 만들필요없이

그냥 엔티티 필드에

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

코드만 추가해주면 된다.

 

 

참고

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

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

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

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

[ 하지만 저장시점에 수정일,수정자도 저장되게끔 설정을 수정안하는게 좋다. ]

 

 

전체 적용

@EntityListeners(AuditingEntityListener.class) 를 생략하고 
스프링 데이터 JPA가 제공하는 이벤트를 엔티티 전체에 적용하려면 orm.xml에 다음과 같이 등록하면 된다.
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>

댓글