본문 바로가기
Dev/Spring Data JPA

[Spring Data JPA] 나머지 기능들

by dev_jsk 2021. 12. 19.
728x90
반응형

Specifications (명세)

언어에 상관없이 조건을 조립해서 쓸 수 있도록 만든 추상화한 개념

Predicate (술어)

  • 참 또는 거짓으로 평가
  • AND, OR 같은 연산자로 조합해서 다양한 검색조건을 쉽게 생성 (컴포지트 패턴)
  • 스프링 데이터 JPA는 org.springframework.data.jpa.domain.Specification 클래스로 정의

사용 방법

JpaSpecificationExecutor 인터페이스 상속

public interface MemberRepository extends JpaRepository<Member, Long>, JpaSpecificationExecutor<Member> {}

JpaSpecificationExecutor 인터페이스

public interface JpaSpecificationExecutor<T> {
    Optional<T> findOne(@Nullable Specification<T> spec);
    List<T> findAll(Specification<T> spec);
    Page<T> findAll(Specification<T> spec, Pageable pageable);
    List<T> findAll(Specification<T> spec, Sort sort);
    long count(Specification<T> spec);
}
  • Specification을 파라미터로 받아 검색조건으로 사용
  • where(), and(), or(), not()을 제공한다.

※ 자바 코드로 동적 쿼리를 만들 수 있다는 장점이 있지만 사용하기 너무 어렵고 직관적이지 않다는 단점이 있다.

Query By Example

단순한 인터페이스를 통해 동적으로 쿼리를 만드는 사용자 친화적인 기술로 어떠한 객체(엔티티)를 갖고 쿼리를 만드는 개념

용어

  • Probe : 필드에 데이터가 있는 실제 도메인 객체
  • ExampleMatcher : 특정 필드를 일치시키는 상세한 정보(포함, 예외) 제공, 재사용 가능
  • Example : ProbeExampleMatcher로 구성되어 쿼리를 생성하는데 사용

장점

  • 동적 쿼리를 편리하게 처리
  • 도메인 객체를 그대로 사용
  • 데이터 저장소를 RDB에서 NoSQL로 변경해도 코드 변경이 없게 추상화 되어 있음
  • 스프링 데이터 JPA 인터페이스 포함

단점

  • Inner Join은 가능하지만 Outer Join은 불가
  • A = ?0 OR (A = ?1 AND B = ?2)와 같은 중첩 조건 설정이 불가
  • 매칭 조건이 매우 단순
    - 문자는 starts/contains/ends/regex
    - 다른 속성은 일치 '=' 만 지원

※ 매칭 조건이 단순하고 외부 조인이 불가능하여 실무에서는 잘 사용하지 않는다.

Projections

쿼리 SELECT절에 들어갈 데이터를 지정하는 것으로 엔티티 대신 DTO를 편리하게 조회할 때 사용한다.

인터페이스 기반 Closed Projections

public interface UsernameOnly {
    String getUsername();
}
  • 프로퍼티 형식(Getter)의 인터페이스를 제공하면 구현체는 스프링 데이터 JPA가 제공
  • 정확히 매칭하여 값을 가져오기 때문에 SELECT절 최적화

인터페이스 기반 Open Projections

public interface UsernameOnly {
    @Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
    String getUsername();
}
  • @Value 어노테이션과 스프링의 SpEL 문법 사용하여 구현
  • DB에서 엔티티 필드를 다 조회해 온 후 계산하기 때문에 SELECT절 최적화가 잘 안된다.

클래스 기반 Projection

public class UsernameOnlyDto {
    private final String username;
    public UsernameOnlyDto(String username) {
        this.username = username;
    }
    public String getUsername() {
        return username;
    }
}
  • 인터페이스가 아닌 DTO형식으로 프로젝션 가능
  • 생성자의 파라미터 이름으로 매칭

동적 Projection

<T> List<T> findProjectionsByUsername(String username, Class<T> type);
  • Generic Type을 주면 동적으로 프로젝션 데이터 변경 가능

중첩 구조 처리

public interface NestedClosedProjection {
    String getUsername();
    TeamInfo getTeam();
    interface TeamInfo {
        String getName();
    }
}
  • 프로젝션 대상이 ROOT 엔티티면 JPQL SELECT절 최적화가 가능하다.
  • 프로젝션 대상이 ROOT 엔티티가 아니면 Left Outer Join이 되고 모든 필드를 SELECT 해서 엔티티로 조회한 이후 계산하기 때문에 SELECT절 최적화가 안된다.

정리

  • 프로젝션 대상이 ROOT 엔티티면 유용하다.
  • 프로젝션 대상이 ROOT 엔티티를 넘어가면 JPQL SELECT 최적화가 안된다.
  • 실무의 복잡한 쿼리를 해결하기엔 한계가 있다.
  • 실무에서는 단순할 때만 사용하는 것이 좋다.

네이티브 쿼리

JPA가 제공하는 기능으로 JDBC, JdbcTemplate, MyBatis 등을 사용하여 SQL을 직접 작성하여 사용하는 것으로 JPA를 사용한다면 가급적 네이티브 쿼리를 사용하지 말아야 하며 정말 최후의 방법으로 사용해야 한다.

스프링 데이터 JPA 기반 네이티브 쿼리

  • 페이징 지원
  • 반환 타입
    - Object[]
    - Tuple
    - DTO(스프링 데이터 인터페이스 Projections 지원)
  • 제약
    - Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있어 직접 처리하는 것을 권장한다.
    - JPQL처럼 애플리케이션 로딩 시점에 문법 확인 불가
    - 동적 쿼리 불가

JPA 네이티브 SQL 지원

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query(value = "select * from member where username = ?", nativeQuery = true)
    Member findByNativeQuery(String username);
}
  • JPQL은 위치 기반 파라미터가 1부터 시작하지만 네이티브 SQL은 0부터 시작
  • 네이티브 SQL을 엔티티가 아닌 DTO로 변환하기 위해선
    - DTO 대신 JPA Tuple 조회
    - DTO 대신 Map 조회
    - @SqlResultSetMapping 사용
    - Hibernate ResultTransformer 사용
    - 네이티브 SQL을 DTO로 조회할 때는 JdbcTemplate or MyBatis 권장

Projections 활용

@Query(value = "SELECT m.member_id as id, m.username, t.name as teamName FROM member m left join team t",
    countQuery = "SELECT count(*) from member",
    nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);
  • 스프링 데이터 JPA 네이티브 쿼리 + 인터페이스 기반 Projections 활용

동적 네이티브 쿼리

String sql = "select m.username as username from member m";
List<MemberDto> result = em.createNativeQuery(sql)
                           .setFirstResult(0)
                           .setMaxResults(10)
                           .unwrap(NativeQuery.class)
                           .addScalar("username")
                           .setResultTransformer(Transformers.aliasToBean(MemberDto.class))
                           .getResultList();
  • 하이버네이트 직접 활용
  • 스프링 JdbcTemplate, MyBatis, jooq 같은 외부 라이브러리 사용
728x90
반응형

댓글