본문으로 바로가기

[Spring] 실전! 스프링 데이터 JPA - 3

category Spring 2021. 9. 6. 15:40

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

 

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

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

www.inflearn.com

※ 확장 기능

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

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

- 인터페이스를 직접 구현하려면 구현해야하는 기능이 너무 많음

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

 > JPA 직접 사용 (EntityManager)

 > 스프링 JDBC Template 사용

 > MyBatis 사용

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

 > Querydsl 사용

 

- QueryDsl 사용할때 커스텀 리포지토리로 만들어서 구현함

- 네이밍을 맞춰주는 작업도 필요함 -> 인터페이스+Impl

 > Impl 이 아닌 다른것으로도 변경이 가능하긴한데, 권장하지 않음

- 커스텀 리포지토리에 모든 로직을 생성하여 담기보다는, 핵심로직에 따른 repository와 특별한 쿼리들을 분리하는 방식으로도 생각 필요

 

* CQRS ( Command and Query Responsibility Segregation )

 > 명령과 조회의 책임 분리

 > 시스템의 상태를 변경하는 작업과 시스템의 상태를 반환하는 작업의 책임을 분리하는 것입니다.

- 서비스와 리포지토리 1대1 연결은 안티패턴 (실제 많이 사용되는 패턴이지만 비효율적이거나 비생산적인 패턴) 이다

 

ㅁ Auditing

- JPA 사용

 > @PrePersist , @PreUpdate 사용 + @MappedSuperclass

 > 이후 Entity에 상속

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

 

- 스프링 데이터 JPA 사용

 > @EnableJpaAuditing 사용

 > AuditorAware 를 등록하여 등록자 수정자 자동으로 등록

  >> AuditingEntityListener 설정도 필요 ( META-INF/orm.xml 설정하여 글로벌 설정도 가능)

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

 - 실무에서는 BaseTimeEntity, BaseEntity로 나누어 등록하여,

   시간만 필요할때는 BaseTimeEntity 상속

   등록자도 함께 필요할때는 BaseEntity 상속 

   >> BaseEntity는 BaseTimeEntity 상속

 

ㅁ Web 확장 - 도메인 클래스 컨버터

- 조회용으로만 사용하자

 > update 할때도 사용할 수 있지만, 예외사항이 많다.

    @GetMapping("/members2/{id}")
    public String findMember(@PathVariable("id") Member member){
        return member.getUsername();
    }

 

ㅁ Web 확장 - 페이징과 정렬

 - web mvc 에서도 pageable을 사용할 수 있다.

  >  Pageable을 컨트롤러 메소드에서 파라미터로 사용하면 PageRequest를 자동으로 스프링이 구현해서 만들어준다

- 아래와 같이 page 관련 default값 설정 가능 (global) - application.yml

data:
web:
pageable:
default-page-size: 10
max-page-size: 2000

- local하게 하고 싶으면 @PageableDefault(size = 5) 를 Pageable 파라미터 앞에 사용

- 페이징 처리를 하더라도, Page<Dto>로 반환하도록 하자

  => map 함수 사용

- 페이징 인덱스를 1부터 시작하게 하는 방법 2개가 있음

 > 새로 Page를 구현함

 > spring.data.web.pageable.one-indexed-parameters를 true로 설정

  >> totalCount 등과 같은것은 0일때의 인덱스로 계산되어 문제가 있음

 >> 결과적으로 0으로 인덱스 계산하는것을 추천함

 

※ 스프링 데이터 JPA 분석

ㅁ 스프링 데이터 JPA 구현체 분석

- SimpleJpaRepositry 가 JPA 구현체!

- JDBC나 JPA 오류 (하부기술 오류) 도 Spring 오류로 변경되어 출력됨

- SpringJpaRepository에 @Transactionnal 이 전부 있음

- @Transactional(readOnly=true) 로 할 경우, 플러시를 하지 않아 영속성 컨텍스트 항목이 DB에 반영 안됨

 > 성능 최적화에 도움이 될 수 있음

- save 메서드

 > 새로운 객체가 아닐 경우, merge 사용 (조회 하여 컨텍스트에 올린 후, 트랜스액션 끝날때 저장됨)

 > 가급적이면 머지를 쓰면 안됨

 >> 영속상태 엔티티가 영속상태를 벗어났을때 다시 영속상태로 만들때만 쓰는게 머지임

 >> 영속상태의 엔티티는 merge를 사용하지 않는게 좋음

 

 

ㅁ 새로운 엔티티를 구별하는 방법

- EntityManager가 persist 할때 @GeneratedValue가 ID를 넣어줌

- id를 @GeneratedValue 를 빼고, 임의로 넣고 저장할 경우 어떻게 될까?

 > persist를 호출하지 않고, merge를 진행하게 됨.. (select문을 호출)

 > 없는것을 확인 한 후 , 새거임을 알아서 새로 저장

 > 새로 저장하는데도 merge를 쓰기때문에 좋지 않음

 >>> 엔티티에서 Persistable<Keytype> 을 구현함

 >>>  isNew를 createdDate 사용하여 구현

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

 

※  나머지 기능들

ㅁ Specifications (명세)

- JPA Criteria 는 비추 - 실무에서 어려움 (금지시키는 중) : 읽고 해석하기가 어렵다

- 동적쿼리 사용위해 이렇게 작성하긴한건데, QueryDsl 을 사용하면 더 쉬움

 

ㅁ Query By Example

 - 실무 사용은 어려울듯

- JpaRepository 가 QueryByExampleRepository 상속

- 객체 자체를 쿼리로 만들 수 있음

- 아래와 같이 구성

        ExampleMatcher matcher = ExampleMatcher.matching().withIgnorePaths("age");
        Example<Member> example = Example.of(member,matcher);
        List<Member> result = memberRepository.findAll(example);

- 문제점은 JOIN이 해결이 안됨

 > Inner Join은 가능하나, Left Join (OuterJoin) 은 안됨

 

ㅁ Projection

- 엔티티 대신에 DTO를 편리하게 조회할 때 사용

 > 전체 엔티티가 아니라 만약 회원의 이름만 조회하고 싶다면?

- 엔티티 객체가 아닌 프록시 객체가 나옴 (JdkDynamicAopProxy)

- 인터페이스에 특정 getter만 입력 해놓으면 스프링 데이터 jpa 가 구현체를 만들어서, 특정 field만 가져오게 쿼리를 날리게됨

 > close projection

- open projection -> 엔티티를 모두 가져와서, 애플리케이션에서 연산함

import org.springframework.beans.factory.annotation.Value;

public interface UsernameOnly {

    @Value("#{target.username + ' ' + target.age}")
    String getUsername();
}

- nestedClosedProjection을 통해 중첩 구조도 해결할 수 있음

 > user에 관해서는 정확하게 조회, 단 team은 엔티티로 가져와서 연산 하게됨

 > 이것이 한계점!

- 프로젝션 대상이 root 엔티티면 유용하나, root 엔티티를 넘어가면 JPQL SELECT 최적화가 안됨

 > 단순할때만 사용하고 복잡하면 querydsl을 사용하자

 

ㅁ 네이티브 쿼리

- @Query(nativeQuery = true, )  통해 작성

-  동적쿼리 불가, 반환타입이 어려움

 > 네이티브 쿼리 사용하고 싶을경우, JDBC Template이나 mybatis 사용하자

- 그나마 쓸만한 것