스프링 데이터 JPA 확장기능

구본식·2023년 2월 10일
1

Spring Data JPA

목록 보기
6/8
post-thumbnail

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

  • 스프링 데이터 JPA 리포지토리는 인터페이스만 제공, 스프링이 구현체인 Hibernate는 자동으로 스프링빈으로 등록

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

  • 사용자가 필요한 기능만 따로 정의한 인터페이스의 구현체를 직접 구현하고 싶을 때 사용

    • JPA 사용
    • 스프링 JDBC Template 사용
    • MyBatis 사용
    • Querydsl 사용 등등

사용자 정의 인터페이스

public interface MemberRepositoryCustom {

    List<Member> findMemberCustom();
}

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

@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {

    private final EntityManager em;

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

참고로 스프링 2.x부터는 사용자 정의 인터페이스 구현체의 이름을
스프링 데이터 JPA를 상속한 인터페이스+Impl 이 아닌 사용자 정의 인터페이스+Impl 로 가능하다.

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

/**
 * 스프링 데이터 JPA 기반 레포지토리
 */
public interface MemberRepository extends JpaRepository<Member, Long> , MemberRepositoryCustom {
}

스프링 데이터 JPA가 인식하여 구현체(사용자 정의 구현체)를 스프링 빈으로 자동 등록해주게 된다.

프로젝트, 실무시 참고사항💡

"위의 사용자 정의 인터페이스 구현체 정의 방식"은 Querydsl, springJdbcTemplate등을 함께 사용할때 스프링 데이더 JPA의 인터페이스를 모두 구현하기 어려우니 따로 인터페이스를 생성하여 구현체를 스프링 데이터 JPA가 관리,등록 하도록 사용되어진다.

Querydsl등을 사용하기 위해서 사용자 정의 방식을 사용할때에도 비지니스 로직과 연관된 레포지토리를 사용자 정의 방식으로 사용하는 것이 올바른 설계이다.

만약, 특정 서비스의 화면에 특화된 상황(DTO로 바로 변환)등의 비지니스 로직과는 관련없는 레포지토리 같은 경우에는 사용자 정의 방식보다는 순수 임의의 레포지토리를 만들어 분리하여 설계하는것이 옳다.


2. Auditing

서비스를 운영할때 사용자의 기본 로그를 DB에 기록해야할 때가 있다.
예를 들어 작성시간, 변경시간, 작성자, 변경자 등이 있다.
(DB의 트리거와 비슷한 느낌인거 같다.)

그래서 JPA에서는 감시하다라는 의미에 Audit기능을 제공해준다. 즉 데이터 삽입, 수정시에 자동으로 지정한 컬럼에 값을 넣어주는 기능을 해준다.(시간, 작성자 등)

스프링 데이터 JPAAuditing기능을 쉬게 사용할 수 있게 제공해준다. 뿐만아니라 순수 JPA에서도 사용할수 있다.

참고로 순수 JPA에서는 Audigin 기능을 사용하기 위한 주요 어노테이션으로는 @Prepersist,@PostPersist, @PreUpdate, @PostUpdate등이 있다.
관심있으면 찾아보면 될거 같다.

그럼 스프링 데이터 JPA에서 제공해주는 Auditing 기능을 살펴보겠다.

Springboot 설정 클래스

@EnableJpaAuditing //Spring Data Jpa - Auditing 사용
@SpringBootApplication
public class SpringDataJpaApplication {

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

	//Spring Data Jpa - Auditing(@CreatedBy, @LastModifiedBy) 사용
	@Bean
	public AuditorAware<String> auditorProvider() {
		return () -> Optional.of(UUID.randomUUID().toString());
	}
}

Springboot 설정 클래스에 @EnableJpaAuditing을 꼭 적어준다.

추 후 뒤에서 나올 @CreatedBy, @LastModifiedBy 어노테이션을 사용하여
작성자, 수정자를 자동으로 삽입하기 위해서 주체를 지정하기 위해서 AuditorAware<T>클래스를 선언해주었다.

참고💡
디폴트로 저장시점에 등록일자, 등록자는 물론이고 수정자, 수정일자에도 같은 데이터가 저장한다. 즉 저장시점에 등록일자, 수정일자가 같은 같으로 삽입됨.
@EnableJpaAuditing(modifyOnCreate=false)라 지정하게 되면 저장시점에 수정자,수정일자들은 null로 설정된다.

위의 코드에서는 AuditorAware<T> 부분을 현재에는 UUID로 임의로 지정하였다.
JWT기반 Spring Security를 사용할때에는 아래와 같이 SecurityContext에서 Authentication객체를 꺼내와 사용하면 된다.

번외) Spring Security 사용시 설정

@EnableJpaAuditing
@SpringBootApplication
public class AmsSideprojectApplication {

	public static void main(String[] args) {
		SpringApplication.run(AmsSideprojectApplication.class, args);
	}
	//!!!Test!!!
	//Spring Data Jpa - Auditing(@CreatedBy, @LastModifiedBy) 사용
	public class AuditorAwareImpl implements AuditorAware<String> {
		@Override
		public Optional<String> getCurrentAuditor() {
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			String name = "";

			if(authentication!=null) {
				//nickname
//				PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
//				User user = principal.getUser()
//				name = user.getNickname();

				//email
				name = authentication.getName();
			}
			return Optional.of(name);
		}
	}
	@Bean
	public AuditorAware<String> auditorProvider() {
		return new AuditorAwareImpl();
	}
}

간단하게 테스트 해보기 위해서 Spring Security를 구현해논 자체 프로젝트에 적용해보았다.
코드에서 보다싶이 SecurityContext 영역에서 객체를 꺼내 사용할 필드를 지정하였다.

내가 작성한 코드 내에서,
authentication.getName()는 Email을 반환해주게 된다. 만약 Nickname을 사용하기 위해서는 형변환을 통해 사용자의 닉네임을 가져오면 된다.

Spring Security의 관한 내용은 아래 블로그에 정리해놓았는데 참고 하시길 바랍니다.
https://velog.io/@rnqhstlr2297/series/Spring-Secuiry%EC%99%80-JWT%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8

BaseEntity(작성자,등록자), BaseTimeEntity(등록일,수정일)

@Getter
@MappedSuperclass //엔티티가 아닌 클래스 상속 -> 매핑 정보 속성만 제공
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {

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

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}

@Getter
@MappedSuperclass //엔티티가 아닌 클래스 상속 -> 매핑 정보 속성만 제공
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity extends BaseTimeEntity{

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

    @LastModifiedBy
    private String lastModifiedBy;
}
  • @EntityListeners

    • 콜백을 요청할 클래스를 지정해주면 되는데, 스프링 데이터 JPA로 Auditing 할 경우 AuditingEntityListener.class을 지정해주면 됌.
  • @MappedSuperclass

    • 엔티티의 공통 매핑 정보가 필요할 때 주로 사용되어진다.
    • 상속관계 매핑이 아니다.(@Inheritance와 다름)
    • 엔티티가 아니고, 테이블과 매핑되지 않는다.!
    • 부모 클래스를 상속받는 자식클래스에 단순히 속성 정보만 제공한다.
    • 주로 등록일, 수정일, 등록한 사람, 수정한 사람 같은 전체 엔티티에서 공통으로 사용되는 정보를 모을때 사용된다.

      @MappedSuperclass vs @Embeddable+@Embedded 차이점💡 - 추후 정리

  • @CreatedDate

    • Spring Data의 기능이다.
    • 해당 엔티티가 생성될때, 생성하는 시각을 자동으로 삽입해준다.
  • LastModifiedDate

    • 해당 엔티티가 수정될때, 수정하는 시작을 자동으로 삽입해준다.
  • @CreatedBy

    • 해당 엔티티가 삽입될 때, 생성하는 사람이 누구인지 자동 삽입해준다.
    • 생성하는 주체를 지정하기 위해서는 AuditorAware<T> 구현 클래스를 지정해주어야된다.
  • @LastModifiedBy

    • 해당 엔티티가 수정될때, 수정하는 사람이 누군인지 자동으로 삽입해준다.
    • 생성하는 주체를 지정하기 위해서는 AuditorAware<T> 구현 클래스를 지정해주어야된다.
  • @Column(updatable = false)

    • JPA 어노테이션인 @Column과 동일하다. 단지 한번 데이터가 삽입된 컬럼은 수정이 되지 않기 하기 위한 설정이다.

위 코드를 보면 BaseEntity(작성자,수정자), BaseTimeEntity(시간관련)으로 분리한 것을 볼 수 있다.

대부분 엔티티에서 등록시간, 수정시간은 필요하지만 등록자,수정자는 필요하지 않은 경우도 있다.
그래서 클래스를 분리하여 원하는 타입을 선택해서 상속받아 사용하는 방식으로 설게하였다.

Test

 	//순수 JPA, Spring Dat JPA - Auditing
    @Test
    public void JpaEventBaseEntity() throws Exception {

        Member member = new Member("member1");
        memberJpaRepository.save(member); //@PrePersist 실행

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

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

        Member findMember = memberJpaRepository.findById(member.getId()).get();

        System.out.println("findMember.createdDate = " + findMember.getCreatedDate());
        //System.out.println("findMember.updatedDate = " + findMember.getUpdatedDate());
        System.out.println("findMember.lastModifiedDate = " + findMember.getLastModifiedDate());
        System.out.println("findMember.createdBy = " + findMember.getCreatedBy());
        System.out.println("findMember.lastModifiedBy = " + findMember.getLastModifiedBy());
    }

아래 처럼 결과가 정상적으로 나오는것을 볼 수 있다.


3. Web 확장 - 페이징과 정렬

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

Pageable인터페이스를 파라미터와 응답으로 값으로 사용할 수 있게 된다.
코드를 통해 기능을 살펴보자

요청

@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와 같다.
page 번호와, 개수(size), 정렬조건 등을 쿼리스트링으로 요청하면 된다.

Pageable 인터페이스만 파라미터로 적었지만 가능한 이유는,
스프링 부트가 파라미터로 Regeable 인터페이스가 있으면 요청한 파라미터 정보를 가지고 구현체인 PageRequest를 생성해서 주입해주기 때문이다.❗

참고💡
추가로 Spring Data Jpa를 사용할때, 공통 인터페이스, Query Method, NamedQuery 방식 등 사용시 인터페이스 메소드 파라미터로 마지막에 Pageable를 전달하게 되면 스프링 데이터가 자동으로 페이징 처리를 해준다.

스프링 데이터 JPA가 내부적으로 method(Repositoty에 정의한 쿼리 메소드) 정보를 통해 메소드의 파라미터에 Pageable 타입을 가지는 파라미터를 확인한 후 처리하게 된기 때문이다.
NamedQuery도 동일한 방식으로 동작하게 된다.

추후에 구현체를 분석하여 동작방식을 좀 더 상세히 파악할 필요가 있을거 같다🤣

참고💡
Pageable기본 size를 변경하는 방식으로는 yml파일을 직접 설정하는 방식과 @PageableDefault 어노테이션을 사용한 방식이 있다. 사용방법은 찾으면 쉽게 나올 것이다.

응답

	@GetMapping("/members")
    public Page<MemberDto> page (Pageable pageable){

        Page<Member> page = memberRepository.findAll(pageable);
        return page.map(MemberDto::new);
    }

Pageable인터페이스를 응답으로 사용하게 되면 구현체인 PageRequest의 필드들이 응답값으로 포함되게 된다.

또한 Pagemap()기능을 제공하고 있어 쉽게 DTO로 변환이 가능하다.

profile
백엔드 개발자를 꿈꾸며 기록중💻

0개의 댓글