노트 정리/Spring Data JPA

[Spring Data JPA] 나머지

DuL2 2023. 4. 26. 17:24

Specifications (명세)

 도메인 주도 설계(Domain Driven Design - 책)에는 Specification(명세)라는 개념을 소개한다. 스프링 데이터 JPA는 JPA Criteria를 활용해서 이 개념을 사용할 수 있도록 지원해준다.

 

 Criteria는 JPQL 작성을 쉽게 도와주는 빌더 클래스다. 자바 코드 기반이므로 컴파일 시점에서 문법 오류를 체크 가능하다. 단, 사용이 복잡하여 실무에서 직관적으로 사용하기 힘들다.

 

 명세를 이해하기 위한 키워드는 술어(predicate)이다.

술어 - 사전
1. (언어학) 서술어
2. (논리학) 논리의 판단·명제에 있어서 주사(主辭)에 대하여 긍정 또는 부정의 입언(立言)을 하는 개념.

 

JPA에서 술어란 다음과 같다.

  • 참 또는 거짓으로 평가
  • AND OR과 같은 연산자로 조합해서 다양한 검색 조건을 쉽게 생선한다(컴포지트 패턴)

 

Repository에 `JpaSpecificationExecutor`를 상속 받아 사용할 수 있다.

 

다음과 같이 Specification 클래스를 정의하여 사용하면 된다.

 정의 후 위의 클래스를 레포지토리 메서드의 파라미터로 넣어 사용하면 명세들을 조립하여 사용할 수 있다.

 

 기본적으로 `where()`, `and()`, `or()`, `not()`을 제공하며 다음과 같이 사용할 수 있다.

        //when
        Specification<Member> spec = MemberSpec.userName("member1").and(MemberSpec.teamName("teamA"));
        List<Member> result = memberRepository.findAll(spec);

 사용하게 되면 다음과 같은 쿼리문이 나간다.

 

Query By Example

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example

 

Spring Data JPA - Reference Documentation

Example 121. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

필드에 데이터가 존재하는 실제 도메인 객체인 Probe를 이용해 검색하는 방법. JPA 자체에서 제공하는 기능이다.

 

주요 클래스

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

 

 장점으로는 동적 쿼리를 쉽게 만들 수 있으며 도메인 객체를 그대로 사용하게 된다. 또한 spring.data의 기술이므로 데이터 저장소를 RDB에서 NOSQL로 변경하더라도 코드 변경없이 그대로 사용할 수 있고, JpaRepository 인터페이스에 포함되어있으므로 스프링 데이터 JPA 레포지토리를 만들면 그대로 사용할 수 있다.

 

 단점으로는 Inner조인만 가능하다는 점, 조건의 중첩이 불가하다는 점(username이 철수와 영희인 사람 모두 찾기 - 불가능), 매칭 조건이 매우 단순하는 점(정확하게 매칭되는 것만 사용가능)이 있다. 이를 사용할 때 만약 outer 조인이 필요하게 되면 전부 다시 짜야한다는 불편함이 있다.

 

        //when
        //Probe
        Member member = Member.builder().username("member1").build();	//Probe - 객체로 검색 가능

        ExampleMatcher matcher = ExampleMatcher.matching().withIgnorePaths("age"); //조건 제외 설정
        Example<Member> example = Example.of(member, matcher);	//객체와 매처를 파라미터로 넣어 Example 생성

        List<Member> result = memberRepository.findAll(example);	//JPA 자체 규격으로 example 파라미터 검색 가능

        Assertions.assertThat(result.get(0).getUsername()).isEqualTo("member1");

 inner 조인은 가능하나 다른 조인들이 안되는 경우가 많다.

 

        //when
        //Probe
        Team teamA = Team.builder().name("teamA").build();
        Member member = Member.builder().username("member1").team(teamA).build();

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

        List<Member> result = memberRepository.findAll(example);

        Assertions.assertThat(result.get(0).getUsername()).isEqualTo("member1");

 

Projections

 일반적으로 이름과 같은 정보를 조회하기 위해 조회 쿼리를 하게 되면 엔티티의 정보를 모두 받아와 사용해야 할 것이다. 이 때에는 모든 엔티티의 정보를 가져오므로 사용되는 정보에 비해 쿼리 데이터가 상당히 커지게 되는 문제가 생기는데 이 문제를 Projections 기능을 통해 특정 필드(이름)만을 받아와 사용할 수 있다.

 

 엔티티 -> DTO로 조회가 가능!

 

 다음과 같이 프로젝션할 필드에 대해 인터페이스를 작성한 후

public interface UsernameOnly {
    String getUsername();

    @Value(value = "#{target.username = ' ' + target.age}") //open projection - spl 사용
    String getUsernameByOP();
}

작성한 인터페이스를 제네릭 값으로 받아오도록 Repository에 메서드를 작성해주면 된다.

    List<UsernameOnly> findProjectionsByUsername(@Param("username") String username);

 

위의 interface에서 두 메서드에는 차이점이 있는데 첫번째 `getUsername()`을 사용할시 최적화가 되어 username을 지정하여 쿼리를 보내지만 spl을 사용하게 되면 쿼리를 보낼 때는 받아야하는 데이터를 정확하게 분간하지 못하므로 엔티티 전체를 받아와 다시 넣어준다. 맞는 값을 넣어준다.

 

Interface가 아닌 DTO 클래스를 생성하여 받을 수도 있다.

public class UsernameOnlyDto {

    private final String username;

    public UsernameOnlyDto(String username) {
        this.username = username;
    }

    private String getUsername() {
        return username;
    }
}

최적화된 쿼리가 나가는 것을 확인할 수 있다.

동적 프로젝션

동적 프로젝션도 가능하다.

// Repository
    <T> List<T> findProjectionsDtoByUsername(@Param("username") String username, Class<T> type);
  
// Test
        List<UsernameOnlyDto> result2 = memberRepository.findProjectionsDtoByUsername("member1", UsernameOnlyDto.class);

 

프로젝션 - 중첩

 조인되어 관련 엔티티의 데이터를 받아올 경우에는 다음과 같이 중첩된 인터페이스를 만들면 된다.

public interface NestedClosedProjections {

    String getUsername();
    TeamInfo getTeam();

    interface TeamInfo {
        String getName();
    }
}

 동적 쿼리를 사용했던 것처럼 똑같이 사용이 가능하다.

// Repository
    <T> List<T> findProjectionsDtoByUsername(@Param("username") String username, Class<T> type);

// Test
        List<NestedClosedProjections> result3 = memberRepository.findProjectionsDtoByUsername("member1", NestedClosedProjections.class);

 

프로젝션 대상이 root 엔티티가 아닐 경우에는 LEFT OUTER JOIN으로 처리되며 모든 필드를 SELECT하여 엔티티로 조회한 다음 계산하여 필요한 부분만 프로젝션 해주게 된다. 따라서 프로젝션 대상이 Root 엔티티가 아니라면 JPQL select 최적화가 되지 않는다.

 

네이티브 쿼리

 가급적 네이티브 쿼리를 사용하지 않는 것이 좋다고 한다. -> 스프링 데이터 Projections 활용을 하면 좋다.

 

 스프링 데이터 JPA는 네이티브 쿼리에 대해서 페이징, 반환 타입(Object[], Tuple, DTO)을 지원한다. 다만 몇가지 제약사항이 있는데 Sort가 제대로 작동하지 않을 수 있고, JPQL처럼 애플리케이션 로딩 당시 문법 확인이 불가하다. 또한, 동적 쿼리가 불가하다는 단점이 있다.

 

//Repository
    @Query(value = "select * from member where username = ?", nativeQuery = true)
    Member findByNativeQuery(String username);

 

Projections 활용

public interface MemberProjection {

    Long getId();
    String getUsername();
    String getTeamName();
}
// Repository
    @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);
    
// Test
        Page<MemberProjection> result1 = memberRepository.findByNativeProjection(PageRequest.of(0, 10));

 

 다만 네이티브 쿼리는 제약이 많으므로 사용을 안하는 것이 좋다.

'노트 정리 > Spring Data JPA' 카테고리의 다른 글

[Spring Data JPA] 확장  (0) 2023.04.25
[Spring Data JPA] 쿼리  (0) 2023.04.23
[Spring Data JPA] 개요  (0) 2023.04.22