Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- @Aspect
- 자바의 정석 기초편 ch12
- 스프링 고급 - 스프링 aop
- 스프링 db2 - 데이터 접근 기술
- 스프링 입문(무료)
- 2024 정보처리기사 수제비 실기
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch1
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch3
- 게시글 목록 api
- 자바의 정석 기초편 ch11
- jpa - 객체지향 쿼리 언어
- 타임리프 - 기본기능
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch9
- 2024 정보처리기사 시나공 필기
- 스프링 mvc1 - 서블릿
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch4
- jpa 활용2 - api 개발 고급
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch6
- 스프링 mvc1 - 스프링 mvc
Archives
- Today
- Total
나구리의 개발공부기록
나머지 기능들, Specifications(명세), Query By Example, Projections, 네이티브 쿼리 본문
인프런 - 스프링부트와 JPA실무 로드맵/실전! 스프링 데이터 JPA
나머지 기능들, Specifications(명세), Query By Example, Projections, 네이티브 쿼리
소소한나구리 2024. 10. 28. 19:38출처 : 인프런 - 실전! 스프링 데이터 JPA (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
** 참고
- 해당 기능들은 실무에서 자주 사용하진 않지만 알고있으면 가끔 필요할때 유용하게 사용할 수 있음
- 기능의 복잡도에 비해 실무에서 사용하기 애매한 이유에 대해서 설명의 초점을 둠
1. Specifications (명세)
1) 기능설명
- 도메인 주도 설계(Domain Driven Design)이라는 책에서는 SPECIFICATION(명세)라는 개념을 소개하는데 스프링 데이터 JPA에서 JPA가 지원하는 Criteria를 활용해서 해당 개념을 사용할 수 있도록 지원함
- 참 or 거짓으로 평가하거나 AND, OR 같은 연산자로 조합하여 다양한 검색조건을 쉽게 생성할 수 있음
- 일단 JPA Criteria를 활용했다는것은 미래에는 모르겠으나 현 시점에서는 JPA Criteria는 한눈에 코드의 의미를 파악하기도 어렵기에 유지보수가 매우 힘들어서 사용을 권장하지 않음
(1) JpaSpecificationExecutor 인터페이스 상속
- 적용하고자 하는 리포지토리 인터페이스에 스프링 데이터 JPA와 함께 상속
- 내부 구조를 보면 findAll, findOne, count 등등의 메소드가 정의되어 있음
public interface MemberRepository extends JpaRepository<Member, Long>,
JpaSpecificationExecutor<Member> {
}
(2) Specification 구현
- 명세를 구현할 때 JPA Criteria를 사용하여 구현해야함
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);
}
}
(3) Test
- 작성해둔 Spec으로 명세들을 조합한 결과로 리포지토리에 입력하여 조회할 수 있으며 테스트를해보면 join쿼리가 생성되며 정상적으로 테스트가 통과됨
- Specification의 기능 자체는 괜찮지만 JPA Criteria 자체가 너무 복잡하기 때문에 사용하지 않는것을 권장하며 Querydsl로 사용자 정의를하면 훨씬더 쉽고 더 보기좋게 구현할 수 있음
// Specification 테스트
@Test
void specBasic() {
// given
Team teamA = new Team("teamA");
em.persist(teamA);
Member m1 = new Member("member1", 20, teamA);
Member m2 = new Member("member2", 20, teamA);
em.persist(m1);
em.persist(m2);
em.flush();
em.clear();
// when
Specification<Member> spec = MemberSpec.username("member1").and(MemberSpec.teamName("teamA"));
List<Member> result = memberRepository.findAll(spec);
assertThat(result.size()).isEqualTo(1);
}
2. Query By Example
1) 기능 설명
- 객체 자체를 검색조건으로 지정하여 동적으로 검색조건을 입력해야할 때 편하게 할 수 있음
- 공식문서
(1) Test 실행
- 실제 필드값이 있는 도메인 객체를 생성하여 검색조건으로 입력하여 편하게 검색조건을 입력할 수 있음
- ExampleMatcher를 이용하여 검색조건에서 제외할 필드나 null 값을 무시할 수 있으며 다양한 메소드로 상세 검색조건을 설정할 수 있기에 동적쿼리를 편하게 작성할 수 있음
- 연관관계가 적용된 엔터티에도 정상 동작하며 실행해보면 join 쿼리가 전송되어 테스트가 정상동작하는 것을 확인할 수 있음
- DB를 변경해도 코드의 변경이 없도록 추상화 되어있음
** 참고
- 영상의 강의 버전에서는 상세정보에 age값을 무시하는 정보를 제공하지 않아도 동작했지만, 현재 스프링 데이터 JPA 3.X 이상부터는 검색조건의 필드가 정확히 일치하도록 입력해야함
- 현재 기본 동작방식은 상세 조건을 입력하지않으면 모든 필드가 and 조건으로 연결됨
// QueryByExample 테스트
@Test
void queryByExample() {
Team teamA = new Team("teamA");
em.persist(teamA);
// given
Member m1 = new Member("member1", 20, teamA);
Member m2 = new Member("member2", 20, teamA);
em.persist(m1);
em.persist(m2);
em.flush();
em.clear();
// when
// Probe 생성, 객체 자체를 검색조건으로 생성
Member member = new Member("member1");
Team team = new Team("teamA");
member.setTeam(team);
// 특정 필드를 일치시키는 상세한 정보를 제공
ExampleMatcher matcher = ExampleMatcher.matching().withIgnorePaths("age");
// 검색할 조건인 객체와 상세정보를 제공
Example<Member> example = Example.of(member, matcher);
List<Member> result = memberRepository.findAll(example);
assertThat(result.get(0).getUsername()).isEqualTo("member1");
}
(2) 단점
- 그러나 실무에서 자주 사용하는 left 조인이 불가능함(inner join으로만 동작)
- 중첩 제약 조건이 안됨 ex) firstname = ?0 or (firstname = ?1 and lastname =?2) 안됨
- 매칭 조건이 매우 단순함(문자는 start/contains/ends/regex를 지원하지만 다른 속성은 정확한 매칭인 = 만 가능)
- 실무에서 사용하기에는 매칭 조건이 너무 단순하고 left조인이 되지 않기 때문에 실무에서는 Querydsl을 사용하는것을 권장함
3. Projections
1) 기능 설명
- select절에 특정 필드의 값을 지정하여 조회하는 기능이라고 생각하면 됨
- 엔터티 대신에 DTO를 편리하게 조회할 때 사용함
- 예를들어 전체 엔터티가 아니라 회원 이름만 딱 조회하거나 할때 편리하게 사용할 수 있음
- 공식문서
2) 인터페이스로 Projections 적용 및 테스트
(1) 인터페이스로 생성 - 클로즈 프로젝션
- 별도의 인터페이스를 생성하여 반환하고자할 필드를 메소드로 입력
public interface UsernameOnly {
String getUsername();
}
(2) 리포지토리에 적용
- 스프링 데이터 JPA로 구현한 리포지토리 메소드에 반환타입을 생성한 인터페이스로 지정
// 스프링 데이터 JPA로 동작하는 리포지토리의 메소드에 생성한 인터페이스를 반환타입으로 지정
List<UsernameOnly> findProjectionsByUsername(@Param("username") String username);
(3) Test
- 테스트 케이스를 생성하여 Projections를 적용한 메서드를 테스트해보면 위에서 지정한 인터페이스의 필드의 값만 정확히 반환되며 생성되는 쿼리도 해당 값만 조회함
- 스프링 데이터 JPA가 알아서 구현체를 생성하여 반환타입을 인식하여 데이터를 반환하기에 간단한 도메인데이터에서 원하는 데이터만 반환할 때 편하게 사용할 수 있음
- 스프링 데이터 JPA의 공통 메서드들이나 쿼리 메소드 기능을 사용할 때 유용함
// Projections 테스트 코드
@Test
void projections() {
Team teamA = new Team("teamA");
em.persist(teamA);
// given
Member m1 = new Member("member1", 20, teamA);
Member m2 = new Member("member2", 20, teamA);
em.persist(m1);
em.persist(m2);
em.flush();
em.clear();
// when
List<UsernameOnly> result = memberRepository.findProjectionsByUsername("member1");
for (UsernameOnly usernameOnly : result) {
System.out.println("usernameOnly.getUsername() = " + usernameOnly.getUsername());
}
}
(4) 오픈 프로젝션으로 적용
- 프로젝션을 적용할 인터페이스의 필드에 @Value 애노테이션의 값에 SpEL문법을 적용하여 전체값을 가져오는 것을 오픈 프로젝션이라고함
- 아래처럼 적용하게 되면 DB에서 엔터티의 필드를 다 조회한 다음에 애플리케이션에서 계산하기 때문에 JPQL select 절이 최적화가 안됨
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
String getUsername();
}
3) 클래스 기반 Projections 적용 및 테스트
(1) 클래스 생성
- 원하는 클래스 명으로 반환하고자할 필드를 선언하고 생성자와 게터를 작성하면 생성자의 파라미터 이름으로 스프링 데이터 JPA가 알아서 매칭하여 Projections이 동작함
@Getter
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
}
(2) 리포지토리에 적용 및 테스트
- 마찬가지로 스프링 데이터 JPA로 동작하는 리포지토리의 메서드에 반환타입으로 적용하고 동일한 테스트를 진행해보면 클래스에 지정한 필드의 값만 반환되고 쿼리도 동일하게 생성됨
// 프로젝션 - 클래스로 Dto 생성
List<UsernameOnlyDto> findProjectionsClassByUsername(@Param("username") String username);
4) 동적 Projections
(1) 적용
- 인터페이스나 클래스로 반환필드를 정의하는 것이아니라 메소드의 두번째 파라미터로 오브젝트 타입을 넘겨서 동적에 따라 반환타입을 적용할 수 있음
// 프로젝션 - 제네릭 타입으로 동적 프로젝션을 적용할 수 있음
<T> List<T> findProjectionsGenericByUsername(@Param("username") String username, Class<T> type);
(2) 테스트
- 동일한 메소드명으로 두번째 인수에 원하는 반환타입을 입력하여 상황에 따라 동적으로 프로젝션이 적용되도록 사용할 수 있음
// 동일한 메소드를 호출할 때 반환타입을 인수로 입력하여 동적으로 적용
List<UsernameOnlyDto> result1 = memberRepository
.findProjectionsGenericByUsername("member1", UsernameOnlyDto.class);
List<UsernameOnly> result2 = memberRepository
.findProjectionsGenericByUsername("member1", UsernameOnly.class);
5) 중첩 구조 처리
(1) 적용 및 테스트
- 인터페이스 내부에 중첩 인터페이스를 만들어서 프로젝션을 적용한 뒤에 테스트를 실행해보면 select 절의 쿼리가 최적화가 되어있지 않음
- 첫번째 인터페이스에 적용된 getUsername은 최적화되어 잘 가져오지만 team 정보를 보면 id와 이름을 같이 가져오는것을 확인할 수 있음
- 즉, 중첩쿼리에서는 원하는 값만 정확하게 뽑아서 반환시킬 수 없는 단점이 존재함
public interface NestedClosedProjections {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}
/* 발생된 쿼리
select
m1_0.username,
t1_0.team_id,
t1_0.name
from
member m1_0
left join
team t1_0
on t1_0.team_id=m1_0.team_id
where
m1_0.username=?
*/
6) 정리
- 프로젝션 대상이 root 엔터티면 JPQL SELECT 절이 최적화 가능함
- 그러나 프로젝션 대상이 ROOT가 아니면 LEFT OUTER JOIN처리를 하고 모든 필드를 SELECT 해서 엔터티로 조회한 다음에 계산하게 되는 문제가 있음
- 그래서 프로젝션 대상이 root 엔터티이면 유용하지만 root 엔터티를 넘어가면 select 최적화가 진행되지 않기 때문에 실무의 복잡한 쿼리를 해결하기에는 한계가 있음
- 매우 단순하게 조회할 때에만 사용하고 복잡해지면 Querydsl을 사용해야함
4. 네이티브 쿼리
** 참고
- JPA에서 네이티브 쿼리를 사용할 수 있도록 지원하긴하지만 네이티브 쿼리가 필요할 때는 훨씬 사용하기 편리하고 동적 쿼리도 쓸수있는 Mybatis나 JdbcTemplate을 사용하는 것을 권장함
- 페이징 기능이 지원된다고 해도 반환타입이 Object[], Tuple, DTO(Projections 지원)로 한정되어 있고 Sort 파라미터를 통한 정렬이 정상 동작하지 않을 수 있고 JPQL 처럼 애플리케이션 로딩 시점에 쿼리가 잘못되어있는지 문법 확인이 불가능하며 동적 쿼리도 불가함
1) 적용 및 테스트
(1) 적용
- @Query 애노테이션으로 JPQL이 아닌 SQL을 작성하고 nativeQuery 옵션을 true로 적용하면 SQL쿼리가 그대로 적용됨
// 네이티브 쿼리
@Query(value = "select * from Member m where username = ?", nativeQuery = true)
Member findByNativeQuery(String username);
(2) 테스트 결과
- 테스트 케이스를 만들어 실행해보면 SQL쿼리가 나가는 것을 확인할 수 있음
- 네이티브 SQL은 JPQL과 다르게 위치 기반 파라미터를 0부터 시작함(JPQL은 1)
// 네이티브 쿼리 테스트
@Test
public void nativeQuery() {
Team teamA = new Team("teamA");
em.persist(teamA);
// given
Member m1 = new Member("member1", 20, teamA);
Member m2 = new Member("member2", 20, teamA);
em.persist(m1);
em.persist(m2);
em.flush();
em.clear();
Member member1 = memberRepository.findByNativeQuery("member1");
assertThat(member1.getUsername()).isEqualTo("member1");
}
/* 실행된 쿼리 로그
select
*
from
Member m
where
username = ?
select * from Member m where username = ?
select * from Member m where username = 'member1';
*/
2) Projections 활용 조회
- 그나마 간혹 사용할만한 것은 정적 SQL쿼리를 써야하는 상황에서 간단한 DTO로 조회하려고 할 때 Projections를 활용하면 그나마 간편하게 조회할 수 있음
(1) 프로젝션을 반환타입으로 네이티브 쿼리 적용
- 페이징 기능을 지원하기 때문에 반환타입을 Page로하여 옵션에 countQuery를 적용해 카운트 쿼리를 입력하여 전체 개수 정보를 함께 넘길 수 있음
- 쿼리와 반환타입을 작성하여 테스트를 해보면 네이티브 SQL이 전송되면서 페이징도 실제 적용되는 것을 확인할 수 있음
// 네이티브 쿼리 - 프로젝션 활용
@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);
// 반환값을 정의한 별도의 프로젝션 인터페이스
public interface MemberProjection {
Long getId();
String getUsername();
String getTeamName();
}
// 테스트 코드 일부
Page<MemberProjection> result = memberRepository.findByNativeProjection(PageRequest.of(0, 5));
List<MemberProjection> content = result.getContent();
for (MemberProjection memberProjection : content) {
System.out.println("memberProjection.getUsername() = " + memberProjection.getUsername());
System.out.println("memberProjection.getTeamName() = " + memberProjection.getTeamName());
}
/* 실행된 쿼리 및 실행 결과
select m.member_id as id, m.username, t.name as teamName from member m left join team t fetch first 5 rows only;
memberProjection.getUsername() = member1
memberProjection.getTeamName() = teamA
memberProjection.getUsername() = member2
memberProjection.getTeamName() = teamA
*/