
실전! 스프링 데이터 JPA 수업을 듣고 정리한 내용입니다.
Specifications와Query By Example은 실무에서 거의 사용하지 않는다.
그러므로, 정리하지 않고 넘어가기
실무에서는
JPA Criteria를 거의 쓰지 않는다. (진짜 복잡한 코드를 사용한다.) 대신에QueryDSL을 사용하자!
Query By를 실무에서 사용하기에는 매칭 조건이 너무 단순하고,LEFT 조인이 되지 않는다. 실무에서는QueryDSL을 사용하자!
엔티티 대신에
DTO를 편리하게 조회할 때 사용한다.
ex) 전체 엔티티가 아니라 만약 회원 이름만 조회하고 싶을 때
✔️ 인터페이스 기반 Closed Projections
public interface UsernameOnly {
String getUsername();
}
getter 형식으로 지정하면 해당 필드만 선택해서 조회한다. (Projection)
MemberRepository에 추가
public interface MemberRepositry ... {
List<UsernameOnly> findProjectionsByUsername(@Param("username") String username);
}
테스트 소스
@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
Assertions.assertThat(result.size()).isEqualTo(1);
}
실행 결과

select절에서 username만 조회(Projection)하는 것을 확인할 수 있다.
✔️ 인터페이스 기반 Closed Projections
프로퍼티 형식(
getter)의 인터페이스를 제공하면, 구현체는 스프링 데이터 JPA가 제공한다.
UsernameOnly 인터페이스 추가
public interface UsernameOnly{
String getUsername();
}
인터페이스 기반 Open Projections
스프링의 SpEL 문법도 지원한다.
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age}")
String getUsername();
}
JPQL SELECT 절 최적화가 안된다.
테스트 실행 결과
✔️ 클래스 기반 Projection
- 인터페이스가 아닌 구체적인 DTO 형식도 가능하다.
- 생성자의 파라미터 이름으로 매칭한다.
UsernameOnlyDto클래스 생성
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
DTO 형식도 가능하다.
실행 결과
✔️ 동적 Projections
<T> List<T> findProjectionsByUsername(@Param("username") String username, Class<T> type);
Generic type을 주면, 동적으로 프로젝션 데이터 변경 가능하다.
테스트 코드
@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<UsernameOnlyDto> result = memberRepository.findProjectionByUsername("m1", UsernameOnlyDto.class);
for (UsernameOnlyDto usernameOnly : result) {
System.out.println("usernameOnly = " + usernameOnly.getUsername());
}
}
실행 결과

NestedClosedProjection 인터페이스 추가
public interface NestedClosedProjection {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}
테스트 코드 추가
@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<NestedClosedProjections> result = memberRepository.findProjectionByUsername("m1", NestedClosedProjections.class);
for (NestedClosedProjections nestedClosedProjections : result) {
String username = nestedClosedProjections.getUsername();
System.out.println("username = " + username);
String teamName = nestedClosedProjections.getTeam().getName();
System.out.println("teamName = " + teamName);
}
}
실행 결과

member는 username만, team은 모두 조회되었다.
⚠️ 주의
- 프로젝션 대상이
root엔티티면, JPQL SELECT 절 최적화가 가능하다.- 프로젝션 대상이
root가 아니라면
LEFT OUTER JOIN으로 처리한다.- 모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산한다.
📌 정리
- 프로젝션 대상이
root엔티티면projection이 유용하다.- 프로젝션 대상이
root엔티티를 넘어가면 JPQL SELECT 최적화가 안된다!
- 실무의 복잡한 쿼리를 해결하기에는 한계가 있다.
- 실무에서는 단순할 때만 사용하고, 조금만 복잡해지면
QueryDSL을 사용하자!
가급적 네이티브 쿼리는 사용하지 않는게 좋다.
최근에Projections활용이 나왔는데, 이 기능을 사용하자!
Object[]TupleDTO(스프링 데이터 인터페이스 Projections 지원) Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있다. (믿지 말고 직접 처리)
✔️ JPA 네이티브 SQL 지원
MemberRepository에 추가
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "select * from member where username = ?", nativeQuery = true)
Member findByNativeQuery(String username);
}
테스트 코드
@Test
public void nativeQuery() {
// 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
Member result = memberRepository.findByNativeQuery("m1");
System.out.println("result = " + result);
}
실행 결과
@SqlResultSetMapping → 복잡 Hibernate ResultTransformer를 사용해야 한다. → 복잡 JdbcTemplate or myBatis 권장한다.
ex) 스프링 데이터 JPA 네이티브 쿼리 + 인터페이스 기반 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);
테스트 코드
@Test
public void nativeQuery() {
// 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
Page<MemberProjection> result = memberRepository.findByNativeProjection(PageRequest.of(0, 10));
List<MemberProjection> content = result.getContent();
for (MemberProjection memberProjection : content) {
System.out.println("memberProjection.username = " + memberProjection.getUsername());
System.out.println("memberProjection.teamname = " + memberProjection.getTeamName());
}
}
실행 결과
✔️ 동적 네이티브 쿼리
ex) 하이버네이트 기능 사용
//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();
}