RE-Heat 개발자 일지

[Spring Data JPA] [7] 나머지 기능들 - Projections, 네이티브 쿼리 등 본문

백엔드/스프링 데이터 JPA

[Spring Data JPA] [7] 나머지 기능들 - Projections, 네이티브 쿼리 등

RE-Heat 2023. 9. 14. 23:42
 

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

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

www.inflearn.com

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

[1] Specifications (명세)

JPA Criteria는 실무에선 안 쓰인다. 

술어(predicate)

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

 

JpaSpecificationExecutor

public interface JpaSpecificationExecutor<T> { 
	Optional<T> findOne(@Nullable Specification<T> spec); 
	List<T> findAll(@Nullable Specification<T> spec);    
    ...
}

 

MemberRepository

public interface MemberRepository extends JpaRepository<Member, Long>, JpaSpecificationExecutor<Member> {
}
  • memberRepository가 JpaSpecificationExecutor 상속

 

MemberSpec

public class MemberSpec {
 
    public static Specification<Member> teamName(final String teamName) {
        return (Specification<Member>) (root, query, builder) -> {
 
            if (StringUtils.isEmpty(teamName)) {
                return null;
            }
 
            Join<Member, Team> t = root.join("team", JoinType.INNER);//회원과 조인
            return builder.equal(t.get("name"), teamName);
        };
    }
 
    public static Specification<Member> username(final String username) {
        return (Specification<Member>) (root, query, builder) -> builder.equal(root.get("username"), username);
    }
}

JPA Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스를 파라미터로 받아 조건으로 사용할 메서드 구현

 

결론 : 조금만 복잡해도 거의 읽기가 힘들어 사실상 안 쓰인다. Querydsl을 쓰자.


[2] Query By Example

조건이 여러 개이고 동적 쿼리를 짤 때 사용.

 

QueryByExampleExecutor

@SpringBootTest
@Transactional
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로 구성, 쿼리를 생성하는 데 사용

 

정리

조인은 가능하지만, 내부 조인만 가능하다(외부 조인 X)

또 매칭 조건이 너무 단순해 실무에선 안 쓰인다.

 

[3] Projections

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

■ 인터페이스 기반 Projections

UsernameOnly

public interface UsernameOnly {
    String getUsername();
}

조회할 엔티티 필드를 getter 형식으로 지정하면 해당 필드만 선택해서 조회한다.

 

MemberRepository

public interface MemberRepository ... {
 	List<UsernameOnly> findProjectionsByUsername(String username);
}

반환 타입으로 UsernameOnly를 넣어 줌.

 

Test 코드

@Test
public void projections() {
    //given
    Team teamA = new Team("teamA");
    em.persist(teamA);
 
    Member m1 = new Member("m1", 0, teamA);
    Member m2 = new Member("m2", 0, teamA);
    em.persist(m1);
    em.persist(m2);
 
    em.flush();
    em.clear();
 
    //when
    List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1");
 
    //then
    for (UsernameOnly usernameOnly : result) {
    		System.out.println("usernameOnly = " + usernameOnly);
    }
}

 

쿼리문

딱 username만 가져오는 걸 알 수 있다.

 

① 인터페이스 기반 Closed Projections

public interface UsernameOnly { 
    String getUsername();
}

프로퍼티 형식의 인터페이스를 제공하면 구현체는 스프링 데이터 JPA가 제공한다.

 

② 인터페이스 기반 Open Projections

public interface UsernameOnly {
    @Value("#{target.username + ' ' + target.age + ' ' + target.team.name}") 
    String getUsername();
}

SpringEL문법도 지원하나 이렇게 하면 DB에서 엔티티 필드를 다 조회해 온 다음에 계산한다. 

 

 

■ 클래스 기반 projections

UsernameOnlyDto

public class UsernameOnlyDto {
 
    private final String username;
 
    public UsernameOnlyDto(String username) {
        this.username = username;
    }
 
    public String getUsername() {
        return username;
    }
}

구체적인 DTO 형식으로도 projections을 쓸 수 있다.

 

MemberRepository

    //Projections
    List<NestedClosedProjection> findProjectionsByUsername(@Param("username") String username);

 

테스트 코드

이전과 동일 테스트를 진행해도 같은 결과 값이 나온다. 단, 구체적인 class를 명시해 줬으므로 proxy가 필요 없다.

 

■ 동적 Projections

<T> List<T> findProjectionsByUsername(String username, Class<T> type);

GenericType을 주면 동적으로 Projections 타입을 변경할 수 있다.

 

List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1", UsernameOnly.class);

이런 식으로 사용하면 된다.

 

■ 중첩구조 처리

NestedClosedProjection

public interface NestedClosedProjection { 
    String getUsername();
    TeamInfo getTeam();
    interface TeamInfo { 
            String getName();
    } 
}

 

테스트 코드 및 발생쿼리

  • 프로젝션 대상이 root 엔티티면 JPQL SELECT 절 최적화 가능
  • 프로젝션 대상이 root가 아니면
    • LEFT OUTER JOIN 처리
    • 모든 필드를 SELECT 해서 엔티티로 조회한 다음에 계산

결론 : 실무에선 단순할 때만 사용하고 복잡해지면 Querydsl을 쓰자.

 


[4] 네이티브 쿼리

가급적 네이티브 쿼리는 사용하지 않는 게 좋다.

 

MemberRepository

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

 

테스트 코드 및 SQL문

작성한 native 쿼리가 그대로 나오게 된다. 하지만, 단점이 많은 방식이므로 별도의 리포지토리를 만들거나 mybatis, jdbc template을 쓰는 게 낫다.

 

■ Projections을 활용한 네이티브 쿼리 방식

MemberProjection

public interface MemberProjection {
    Long getId();
    String getUsername();
    String getTeamName();
}

 

 

MemberRepository

    @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);
  • Page도 사용 가능

 

테스트 코드

    @Test
    public void nativeQuery() 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();
        em.clear();

        //when
        Page<MemberProjection> result = memberRepository.findByNativeProjection(PageRequest.of(0, 10));
        List<MemberProjection> content = result.getContent();
        for (MemberProjection memberProjection : content) {
            System.out.println("memberProjection = " + memberProjection.getUsername());
            System.out.println("memberProjection = " + memberProjection.getTeamName());
        }

        //then
    }

 

실행된 쿼리문

 

정적쿼리를 Projections를 활용하면 좀 더 수월하게 쓸 수 있다. 페이징 처리도 되는 게 장점이다. 그러나 동적 쿼리는 해결이 안 된다.

 

■ 동적 Native 쿼리

방법

  • 하이버네이트 직접 활용
  • 스프링 JdbcTemplate, myBatis, jooq 같은 외부 라이브러리 사용

예시] 하이버네이트 기능 사용

//given
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();
}