SpringDataJPA에 대해 - 기본 기능2

쿠우·2023년 1월 14일
0

기본2

확장 기능

1. 사용자 정의 리포지토리 구현 (커스텀 하는 법)

  • 스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성
  • 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많음
  • 다양한 이유로 인터페이스의 메서드를 직접 구현하고 싶다면 아래와 같은 여러 방법이 있다.
1) JPA 직접 사용( EntityManager )
2) 스프링 JDBC Template 사용
3) MyBatis 사용
4) 데이터베이스 커넥션 직접 사용 등등...
5) Querydsl 사용

1) 커스텀 해서 사용 하는 법

  • 규칙: 리포지토리 인터페이스 이름 + Impl
    (스프링 데이터 2.x 부터 사용자 정의 인터페이스 명 + Impl 방식도 지원한다)
    (xml, javaConfig 설정을 통해 바꿀 수 있지만 유지보수성 떨어지기 때문에 표준으로 사용하자)
  • 스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
  • 사용자 정의(커스텀) 인터페이스 만든다.
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();
    }
}
  • JPA 인터페이스와 함께 repository에 사용자 정의 인터페이스 상속시킨다.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
  • 사용자 정의 메서드를 호출 해준다.
List<Member> result = memberRepository.findMemberCustom();

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

2) 항상 사용자 정의 리포지토리가 필요한 것은 아니다. 그냥 임의의 리포지토리를 만들어도 된다. 예를들어 MemberQueryRepository를 인터페이스가 아닌 클래스로 만들고 스프링 빈으로 등록해서 그냥 직접 사용해도 된다. 물론 이 경우 스프링 데이터 JPA와는 아무런 관계 없이 별도로 동작한다.

3) 활용법2 강의 정리에서 얘기 했듯이 성능 최적화를 위해 OSIV를 껐을 때 repository 영역내에서 view단에서 사용될 패키지를 분리하고 명령 쿼리 책임 분리 ( CQRS)를 통해서 조회 쿼리를 분리 하는 등 하기 때문에 다각적으로 분석하고 분리하도록 해야한다.

2) 최근 사용 법 (권장)

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

@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom { // 최신 사용 법
// public class MemberRepositoryImpl implements MemberRepositoryCustom { 기존 사용법 

    private final EntityManager em;

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

2. Auditing (뜻: 감사 -> 생성,수정 정보)

  • 엔티티에 대해서 생성 변경할 때 변경한 사람과 시간을 추적하지 않으면 겁나게 복잡해진다.
    (등록일, 수정일, 등록자, 수정자)
  • 객체 세상에 상속을 이용해서 편하게 작성하는 방법을 얘기하는 부분이다.

순수 JPA에서 적용시키기

  • 등록일, 수정일 적용시킨 예제
  • @MappedSuperclass 과 extends 이용
  • 콜백메서드인 @PrePersist, @PostPersist, @PreUpdate, @PostUpdate 등 있고 이를 이용한다.
@MappedSuperclass //내 JPA 블로그 글에 무엇인지 정리 되어있음 참고**
@Getter
public class JpaBaseEntity {
    @Column(updatable = false)
    private LocalDateTime createdDate;
    private LocalDateTime updatedDate;

    @PrePersist // persist 직전에 실행 
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        createdDate = now;
        updatedDate = now;
    }

    @PreUpdate // 수정 직전에 실행 
    public void preUpdate() {
        updatedDate = LocalDateTime.now();
    }
}

// 상속을 이용한다. 
public class Member extends JpaBaseEntity {
}

Spring data jpa에서 적용

  • @EnableJpaAuditing 스프링 부트 설정 클래스에 적용해야함
  • @EntityListeners(AuditingEntityListener.class) 엔티티에 적용
    (@EntityListeners = JPA Entity에 이벤트가 발생할 때 콜백을 처리하고 코드를 실행하는 방법)
    (즉 콜백때 AuditingEntityListener.class에 있는 내용(업데이트시, 생성시 동작하는 내용)들을 맟춰 처리하겠다라는 뜻 )
  • 사용하는 어노테이션
@CreatedDate = 생성일
@LastModifiedDate = 수정일
@CreatedBy = 생성자
@LastModifiedBy = 수정자
  • 스프링 데이터 Auditing 적용 - 등록일, 수정일
    (이건 기본적으로 사용하는 내용들이다.)
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
    @CreatedDate
    @Column(updatable = false) // 엔티티 수정시 필드를 같이 수정하지 않는다. -> 읽기 전용
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}
  • 스프링 데이터 Auditing 적용 - 등록자, 수정자
    (등록자 수정자는 실무에서 없을 수도 있기 때문에 Base타입과 분리하고 필요할때 골라서 상속한다.)
public class BaseEntity extends BaseTimeEntity {
    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;
}
  • 등록자 수정자의 정보를 넣어주기 위한 코드
    (Spring security 이용해서 넣어주면 좋다.)
@Bean
public AuditorAware<String> auditorProvider() {
    return () -> Optional.of(UUID.randomUUID().toString());
}

Tip.
1) 처음 저장 할 때 수정일(=등록일)과 수정자(=등록자)도 함께 저장된다. 중복 저장 되는 것 같지만 이렇게 해두면 변경 컬럼만 확인해도 마지막에 업데이트한 유저를 확인 할 수 있으므로 유지보수 관점에서 편리 하다.

2) 전체적용하려면 어노테이션이 아닌 orm.xml에 AuditingEntityListener을 등록해야한다.

Web확장 - 도메인 클래스 컨버터(권장x)

도메인 클래스 컨버터란?

  • HTTP 파라미터로 넘어온 Entity의 ID(PK)로 Entity객체를 찾아서 바인딩하는 기능
  • Common 프로젝트에 있는 클래스
  • id -> 엔티티 타입으로 변환 해주거나, 엔티티 -> id 로 변환해 주는 컨버터를 제공
    -간단할때만 사용하고 복잡해지면 사용하기 힘듬
  • 사용할 때는 조회용으로만 사용하는게 좋다.
    (트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)

사용 전

@GetMapping("/members/{id}")
 public String findMember(@PathVariable("id") Long id) {
     Member member = memberRepository.findById(id).get();
     return member.getUsername();
 }

사용 후

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

Web확장 - 페이징과 정렬

  • 스프링 데이터가 제공하는 페이징과 정렬 기능을 사용해서 편리하게 사용가능

Page 내용을 DTO로 변환하며 페이지 정렬 사용

@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
     Page<Member> page = memberRepository.findAll(pageable);
     Page<MemberDto> pageDto = page.map(MemberDto::new);
     return pageDto;
}
  • 파라미터로 Pageable 을 받을 수 있다.
  • map() 은 Slice 혹은 Page 객체가 가지고 있는 엔티티를 다른 객체로 매핑(변환)할때 사용한다.
  • 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 생략 가능)

기본 값 바꾸는 법

  • 글로벌 설정: 스프링 부트
    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 String list(@PageableDefault(size = 12, sort = “username”, direction = Sort.Direction.DESC) Pageable pageable) {
      ...
}

페이징 정보가 둘 이상 되었을 때는?

  • @Qualifier 에 접두사명 추가 "{접두사명}_xxx”
  • 쿼리스트링이 /members?member_page=0&order_page=1 일 때
public String list(
      @Qualifier("member") Pageable memberPageable,
      @Qualifier("order") Pageable orderPageable, ..

만약에 page를 1부터 시작하고 싶다면

    1. Pageable, Page를 파리미터와 응답 값으로 사용히지 않고, 직접 클래스를 만들어서 처리한다.
      직접 PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘긴다.
      (응답값도 Page 대신에 직접 만들어서 제공해야 한다)
    1. spring.data.web.pageable.one-indexed-parameters 를 true 로 설정한다 (비추)
      이 방법은 web에서 page 파라미터를 -1 처리 할 뿐이다.
      pageable 객체에는 0으로 남아있음.
      따라서 응답값인 Page 에 모두 0 페이지 인덱스를 사용하는 한계가 있다.

스프링 데이터 JPA 내부를 확인

  • JPA의 기능을 대부분 이용한다.
  • 스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체

1. org.springframework.data.jpa.repository.support.SimpleJpaRepository

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> ...{

    @Transactional
    public<S extends T> S save(S entity){
        if(entityInformation.isNew(entity)){
            em.persist(entity);
            return entity;
        }else{
            return em.merge(entity);
        }
    }
        ...
}
  • 스프링 데이터 JPA내부에 들어가면 @Repository랑 @Transaciosnal 이 걸려 있는 것을 확인 할 수 있다.
  • @Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환한다. (스프링으로 통합시키는 느낌이요)
  • 스프링 데이터 JPA를 사용할 때 트랜잭션이 안걸려있어도 데이터 등록 변경이 가능한 이유는 위에 코드처럼 Repository
    계층에 걸려있어서 전파받아서 사용.
    (service 계층에서 트랜잭션을 시작하지 않으면 repository 에서 시작한다.)

2. 매우 중요한 부분

  • save() 메서드는 새로운 엔티티면 저장( persist ) 새로운 엔티티가 아니면 병합( merge )한다.
  • save를 update라고 생각하고 사용하면 성능상 손해를 볼 수 있다.
  • JPA기본 내용을 공부할 때 병합이 아닌 변경감지를 사용해서 수정을 했는데 여기서는 머지를 사용한다.
    ( merge의 단점은 Select 쿼리를 한번 쭉 부른다는것 -> JPA기본 정리한 블로그글 참고)

3. 구현체가 새로운 엔티티를 구별하는 방법

  • 새로운 엔티티를 판단하는 기본 전략
1. 식별자가 객체 일 때는 null이면 새로운 엔티티로 인식
2. 식별자가 자바 기본타입일때는 0이면 새로운 엔티티로 인식
3. Persistable 인터페이스를 구현해서 판단 로직 변경 가능핟. 
  • Persistable 인터페이스 구성
package org.springframework.data.domain;

public interface Persistable<ID> {
     ID getId();
     boolean isNew();
}

4. 실무에서는 어떤 방법을 쓰는 것이 좋을까?

  • JPA 식별자 생성 전략이 @GenerateValue 면 save() 호출 시점에 식별자가 없으므로 새로운 엔티티로 인식해서 정상 동작 한다.
  • @Id 만 사용해서 직접 할당이면 이미 식별자 값이 있는 상태로 save() 를 호출되기 때문에 merge()로 호출된다.
  • 따라서, @GenerateValue가 없을 때는 Persistable 를 사용해서 새로운 엔티티 확인 여부를 직접 구현하게는 효과적이다
  • 새로운 엔티티의 판단 기준을 어떤로직으로 하는 것이 좋을까?
    실무에서는 등록시간( @CreatedDate )을 조합해서 사용하면 이 필드로 새로운 엔티티 여부를 편리하게 확인할 수 있다.
    (@CreatedDate에 값이 없으면 새로운 엔티티로 판단)
    예시)

@Entity
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {

    @Id
    private String id;

    @CreatedDate
    private LocalDateTime createdDate;

    public Item(String id) {
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public boolean isNew() {
        return createdDate == null;
    }
}

여기서부터는 대충정리..

못난이 4형제

Specifications (명세) - 사용하지말라!

  • Specification 을 구현하면 명세들을 조립할 수 있음. where() , and() , or() , not() 제공 동적쿼리를 구현한다.
    (QueryDSL을 사용하자 )
  • 책 도메인 주도 설계(Domain Driven Design)는 SPECIFICATION(명세)라는 개념을 소개 스프링 데이터 JPA는 JPA Criteria를 활용해서 이 개념을 사용할 수 있도록 지원
  • JPA Criteria 문법을 활용했기에 사용하면 유지보수성 떨어진다.

Query By Example - 사용 비추

public class QueryByExampleTest {
    @Autowired
    MemberRepository memberRepository;
    @Autowired
    EntityManager em;

    @Test
    public void basic() throws Exception {
        
        //given
        Team teamA = new Team("teamA");
        em.persist(teamA);
        em.persist(new Member("m1", 0, teamA));
        em.persist(new Member("m2", 0, teamA));
        em.flush();
        
        //when
        //Probe 생성
        Member member = new Member("m1");
        Team team = new Team("teamA"); //내부조인으로 teamA 가능
        member.setTeam(team);
        
        //ExampleMatcher 생성, age 프로퍼티는 무시
        ExampleMatcher matcher = ExampleMatcher.matching()
                .withIgnorePaths("age");
        Example<Member> example = Example.of(member, matcher);
        List<Member> result = memberRepository.findAll(example);
        
        
        //then
        assertThat(result.size()).isEqualTo(1);
    }
}
  • Probe: 필드에 데이터가 있는 실제 도메인 객체
  • ExampleMatcher: 특정 필드를 일치시키는 상세한 정보 제공, 재사용 가능
  • Example: Probe와 ExampleMatcher로 구성, 쿼리를 생성하는데 사용

냅다 결론

  • 실무에서 사용하기에는 매칭 조건이 너무 단순하고, LEFT 조인이 안됨
  • 실무에서는 QueryDSL을 사용하자

Projections - 약간 도움될 때 있음(나는 안쓰는게 좋다고 봄)

  • 엔티티 대신에 DTO를 편리하게 조회할 때 사용
    (전체 엔티티가 아니라 만약 회원 이름만 딱 조회하고 싶으면?)
  • 프로젝션 대상이 root 엔티티를 넘어가면 JPQL SELECT 최적화가 안되기 때문에 사용하지 않을테야

네이티브 쿼리- 가급적 사용 x

  • 가급적 네이티브 쿼리는 사용하지 않는게 좋음, 정말 어쩔 수 없을 때 사용
  • 네이티브 SQL을 DTO로 조회할 때는 JdbcTemplate or myBatis 권장
  • 최근에 나온 궁극의 방법 스프링 데이터 Projections 짬뽕해서 활용
    (DTO로 뽑고싶은데 동적쿼리는 아닐 때 )
profile
일단 흐자

0개의 댓글