BackEnd/JPA

[JPA] 나머지 기능들

샤아이인 2022. 5. 7.

내가 공부한 것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼 겸 상세히 기록하고 얕은 부분들은 가볍게 포스팅하겠습니다.

 

이번 시간에 배우는 나머지 기능들을 실무에서 자주 사용되지는 않는다.

다만 알아두면 필요한 순간에 가끔 사용할 수 있을 것 이다.

 

Specifications은 JPA Criteria와 관련된 기술 이다.

문제는 실무에서는 JPA Criteria를 거의 안쓴다! 대신에 QueryDSL를 사용하기 때문이다.

 

또한 Query By Example 도 실무에서 직접 사용하기는 애매한 부분이 있는 기술들이다.

 

따라서 해당 강의 영상 부분에 대한 정리는 생략하도록 하겠습니다 ㅎㅎ

 

1. Projections

회원의 이름만 딱 조회하고 싶은데 JPA를 통해 조회하면 Entity 전체를 조회해오게 된다.

전체의 데이터를 가져오는 것 이 아닌, Entity의 특정 필드값만 편하게 조회 해오는 기술을 projection이라 부른다.

 

1. 인터페이스 기반 Closed Projections

우선 간단한 interface를 하나 만들자. projections을 통해 username만 반환하도록 할것이다.

public interface UsernameOnly {
    String getUsername();
}

 

MemberRepository에 다음과 같이 추가해주자!

public interface MemberRepository extends JpaRepository<Member, Long> {

    List<UsernameOnly> findProjectionsByUsername(String username);
}

메서드의 이름은 자유지만, 쿼리메서드 방식을 사용하고 있으니 find...ByUsername으로 설정해 주었다.

반환 타입으로 기술한 UsernameOnly interface를 보고 Data JPA가 알아서 해당 필드만 조회 해준다.

 

이를 활용하는 테스트코드는 다음과 같다.

@Test
public void projections_test() {
    // given
    Team teamA = new Team("teamA");
    teamRepository.save(teamA);

    Member member1 = new Member("member1", 10, teamA);
    Member member2 = new Member("member2", 10, teamA);
    memberRepository.save(member1);
    memberRepository.save(member2);

    em.flush();
    em.clear();

    // when
    List<UsernameOnly> result = memberRepository.findProjectionsByUsername("member1");
    for (UsernameOnly usernameOnly : result) {
        System.out.println("usernameOnly = " + usernameOnly);
    }
}

result에 뭐가 담겨있는지 확인해보기 위해 디버깅을 해보자.

출력된 쿼리를 보면 username 에 해당되는 부분만 select 하는 SQL문이 나왔다.

 

List<UsernameOnly> result에 담긴 0번째 원소는 Proxy 객체이다.

즉, Interface만 정의하면 실제 구현체는 Spring Data JPA가 만들어서 주입해준다.

 

2. 인터페이스 기반 Open Projections -> SpEL 문법 지원

public interface UsernameOnly {

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

단! 이렇게 SpEL문법을 사용하면, DB에서 엔티티 필드를 우선 다 조회해온 다음에 계산한다.

select member0_.member_id as member_i1_1_, 
       member0_.created_date as created_2_1_, 
       member0_.last_modified_date as last_mod3_1_, 
       member0_.created_by as created_4_1_, 
       member0_.last_modified_by as last_mod5_1_, 
       member0_.age as age6_1_, 
       member0_.team_id as team_id8_1_, 
       member0_.username as username7_1_ 
from member member0_ 
where member0_.username='member1';

따라서 JPQL SELECT 절 최적화가 안된다.

 

3. Class 기반의 Projections

우선 다음과 같은 DTO를 하나 만들자.

public class UsernameOnlyDto {

    private final String username;

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

    public String getUsername() {
        return username;
    }
}

생성자가 중요하다! 생성자의 파라미터 이름으로 매칭을 시켜 Projections을 한다.

 

interface에서의 반환값도 Dto 버전으로 변경해주자.

public interface MemberRepository extends JpaRepository<Member, Long> {

    List<UsernameOnlyDto> findProjectionsByUsername(String username);
}

 

이전과 동일한 테스트를 진행시켜보자.

@Test
public void projections_test() {
    // given
    Team teamA = new Team("teamA");
    teamRepository.save(teamA);

    Member member1 = new Member("member1", 10, teamA);
    Member member2 = new Member("member2", 10, teamA);
    memberRepository.save(member1);
    memberRepository.save(member2);

    em.flush();
    em.clear();

    // when
    List<UsernameOnlyDto> result = memberRepository.findProjectionsByUsername("member1");
    for (UsernameOnlyDto usernameOnly : result) {
        System.out.println("usernameOnly = " + usernameOnly.getUsername());
    }
}

usernameOnly = member1 라는 결과가 정상 출력된다.

 

이번에는 Proxy가 필요가 없다. 구체적인 Class를 명시해줬기 때문이다.

 

4. 동적 Projections

다음과 같이 GenericType을 주면 동적으로 Projections의 타입을 변경할 수 있다.

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

사용은 다음과 같이 하면 된다.

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

 

5. 중첩 구조 처리

이번에는 member와 연관된 Team에 대한 부분까지 가져와보자!

public interface NestedClosedProjections {

    String getUsername();
    TeamInfo getTeam();

    interface TeamInfo {
        String getName();
    }
}

실행되는 쿼리는 다음과 같다.

select
    member0_.username as col_0_0_,
    team1_.team_id as col_1_0_,
    team1_.team_id as team_id1_2_,
    team1_.created_date as created_2_2_,
    team1_.updated_date as updated_3_2_,
    team1_.name as name4_2_ 
from
    member member0_ 
left outer join
    team team1_ 
        on member0_.team_id=team1_.team_id 
where
    member0_.username='member1'

프로젝션 대상이 root 엔티티면 JPQL SELECT 절 최적화 가능하지만, root 가 아니라면 최적화가 불가능하다.

따라서 SQL문에서 team에 대한 모든 정보를 select 하고있다.

또한 Projections 대상이 root가 아니면 Left join을 하여 모든 필드를 select 하게 된다.

 

실무에서는 단순할 때만 사용하고, 조금만 복잡해지면 QueryDSL을 사용하자!

 

2. 네이티브 쿼리

 

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

정말 어쩔 수 없을 때 사용 최근에 나온 궁극의 방법 스프링 데이터 Projections 활용하자!

 interface MemberRepository extends JpaRepository<Member, Long>, MemberCustomRepository {

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

 실행할 테스트코드는 다음과 같다.

@Test
public void native_query_test() {
    // given
    Team teamA = new Team("teamA");
    teamRepository.save(teamA);

    Member member1 = new Member("member1", 10, teamA);
    Member member2 = new Member("member2", 10, teamA);
    memberRepository.save(member1);
    memberRepository.save(member2);

    em.flush();
    em.clear();

    // when
    Member findMember = memberRepository.findByNativeQuery("member1");
    System.out.println("findMember = " + findMember);
}

실행시 나오는 쿼리는 다음과 같다.

우리가 작성한 native 쿼리가 그대로 나오게 된다. 

 

하지만 단점이 많은 방식이다. 그냥 custom repository를 만들어서 jdbc template, MyBatis, QueryDsl을 사용하길 권장한다.

 

최근 되서야 Spring Data Projections이 지원된다.

 

딱 DTO를 뽑을때 Native Query를 통해 좀 편하게 뽑을 수 있는 기능이다.

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

 

MemberProjection은 다음과 같다.

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

 

 실행할 테스트 코드는 다음과 같다.

@Test
public void native_query_test() {
    // given
    Team teamA = new Team("teamA");
    teamRepository.save(teamA);

    Member member1 = new Member("member1", 10, teamA);
    Member member2 = new Member("member2", 10, teamA);
    memberRepository.save(member1);
    memberRepository.save(member2);

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

 

실행된 SQL문은 다음과 같다.

SELECT
    m.member_id as id,
    m.username,
    t.name as teamName 
FROM
    member m 
left join
    team t limit 10

 

결과는 다음과 같이 출력된다.

memberProjection = member1
memberProjection = teamA
memberProjection = member2
memberProjection = teamA

 

▶ 동적 네이티브 쿼리를 어떻게 작성할까?

- 하이버네이트를 직접 활용해야한다.
- 스프링 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();

}

'BackEnd > JPA' 카테고리의 다른 글

[JPA] QueryDSL 기본문법 - 2  (0) 2022.05.13
[JPA] QueryDSL 기본문법 - 1  (0) 2022.05.13
[JPA] 스프링 데이터 JPA 분석  (0) 2022.05.06
[JPA] 확장 기능  (0) 2022.05.06
[JPA] 쿼리 메소드 기능 - 4  (0) 2022.05.04

댓글