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
- 스프링 mvc2 - 로그인 처리
- 스프링 mvc1 - 스프링 mvc
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch14
- 스프링 mvc1 - 서블릿
- 스프링 mvc2 - 타임리프
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch4
- 코드로 시작하는 자바 첫걸음
- 스프링 db1 - 스프링과 문제 해결
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch3
- @Aspect
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch6
- 2024 정보처리기사 시나공 필기
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch7
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch13
- 게시글 목록 api
- 스프링 mvc2 - 검증
- jpa - 객체지향 쿼리 언어
- 스프링 입문(무료)
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch8
Archives
- Today
- Total
나구리의 개발공부기록
실무 활용 - 스프링 데이터 JPA 리포지토리와 Querydsl(스프링 데이터 JPA 리포지토리로 변경), 사용자 정의 리포지토리, 스프링 데이터 페이징 활용(Querydsl 페이징 연동/CountQuery 최적화/컨트롤러 개발/정렬) 본문
인프런 - 스프링부트와 JPA실무 로드맵/실전! Querydsl
실무 활용 - 스프링 데이터 JPA 리포지토리와 Querydsl(스프링 데이터 JPA 리포지토리로 변경), 사용자 정의 리포지토리, 스프링 데이터 페이징 활용(Querydsl 페이징 연동/CountQuery 최적화/컨트롤러 개발/정렬)
소소한나구리 2024. 10. 30. 20:17출처 : 인프런 - 실전! Querydsl (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 스프링 데이터 JPA 리포지토리로 변경
1) 순수 JPA -> 스프링 데이터 JPA리포지토리로 변경
(1) 리포지토리 생성
- 인터페이스를 생성하고 JpaRepository를 상속받아 스프링 데이터JPA 리포지토리로 생성
- 공통인터페이스로 제공하지 않는 이름으로 회원 찾기를 하는 메서드만 추가로 생성하면 끝남
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
(2) 테스트
- 순수 JPA에서 진행했던 동일한 테스트를 리포지토리 주입만 스프링 데이터 JPA로 주입하면 정상적으로 동작함
- 기존에 순수 JPA에서 만들었던 메서드들의 이름이 스프링 데이터 JPA에서 지원하는 공통 인터페이스 이름이기 때문에 그대로 사용해도 동작함
@SpringBootTest
@Transactional
class MemberRepositoryTest {
@Autowired
EntityManager em;
@Autowired MemberRepository memberRepository;
@Test
void basicTest() {
Member member = new Member("member1", 10);
memberRepository.save(member);
Member findMember = memberRepository.findById(member.getId()).get();
assertThat(findMember).isEqualTo(member);
List<Member> result1 = memberRepository.findAll();
assertThat(result1).containsExactly(member);
List<Member> result2 = memberRepository.findByUsername("member1");
assertThat(result2).containsExactly(member);
}
}
2. 사용자 정의 리포지토리
1) 설명
(1) 생성하는 이유
- 스프링 데이터 JPA에서 Querydsl을 사용하기 위해서는 별도의 리포지토리를 인터페이스로 생성하여, 구현체를 직접 만들어서 사용해야함
- 스프링 데이터 JPA의 리포지토리에 사용자 정의 리포지토리 인터페이스를 상속시키면 사용자정의 리포지토리 인터페이스에 선언된 메서드들을 호출할 수 있고, 이를 호출하면 구현체에 작성한 Querydsl이 호출되어 동작함
(2) 구성도
2) 사용자 정의 리포지토리 구현
(1) 사용자 정의 리포지토리 인터페이스 생성
- 사용하고자 하는 메서드를 선언
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
(2) 사용자 정의 리포지토리 인터페이스를 구현
- 과거에는 구현 클래스 이름을 스프링 데이터 JPA 리포지토리의 이름 + Impl로 적어줘야했지만, 이제는 사용자 정의 리포지토리 인터페이스 이름 + Impl로 작성해도 됨
- 후자가 훨씬 가독성이나 구분하기 좋으므로 후자로 이름을 작성하는 것을 권장함
- 생성자의 파라미터로 EntityManager를 받아서 JPAQueryFactory객체를 생성하도록 작성 후 인터페이스의 메서드를 구현
package study.querydsl.repository;
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
public MemberRepositoryCustomImpl(EntityManager em) {
queryFactory = new JPAQueryFactory(em);
}
// 순수 JPA에서 구현한 기능과 동일하도록 구현
@Override
public List<MemberTeamDto> search(MemberSearchCondition condition) {
// ... 구현된 로직은 동일하므로 생략
}
}
(3) 스프링 데이터 JPA에 상속
- 인터페이스는 다중상속이 가능하므로 사용자정의 인터페이스를 실제 사용할 스프링 데이터 JPA에 상속
- MemberRepository에서 모든 메서드들을 호출하여 사용할 수 있음
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
(4) 테스트
- 순수 JPA에서 진행했던 테스트 케이스와 동일한 테스트 케이스를 스프링 데이터 JPA의 메서드로만 변경해서 진행해보면 정상적으로 실행이 되고 쿼리도 정상적으로 동작하는 것을 확인할 수 있음
(5) 정리
- 스프링 데이터 JPA에서 Querydsl을 사용하려면 이렇게 사용자 정의 리포지토리를 만들어서 사용해야함
- 해당 내용도 반복적으로 나오지만 정말 복잡하거나 특정 화면이나 API에 특화된 Query들을 항상 커스텀 리포지토리를 만들어서 한다기 보다는, 별도의 조회용 리포지토리 클래스를 만들어서 관련된 클래스들을 별도의 패키지에 모아두어 관리하면 관심사 분리와 응집도 측면에서 더 나은 아키텍처 설계를 할 수 있으며 유지보수성도 높아짐
- 즉, 커스텀 리포지토리를 만들어서 사용하는 것이 기본이며 실제 관심사에 따라서 쿼리들은 별도의 클래스로 관리하여 설계하는 것이 좋을 수 있음
3. 스프링 데이터 페이징 활용
1) Querydsl 페이징 연동
(1) 인터페이스에 메서드 추가
- Pageable을 파라미터로 입력받고 반환타입이 Page인 메서드를 추가로 생성
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
// 페이지 정보를 받는 메서드 추가
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable);
}
(2) searchPageComplex메서드 구현
- Querydsl5.0.0 버전부터는 쿼리 결과와 count 쿼리를 한번에 가져오는 fetchResults()와, count값을 내부적인 로직으로 가져오는 fetchCount()는 디프리케이트가 되어서 별도의 count쿼리를 만들어서 전체 개수를 가져와야 함
- 각각의 쿼리를 생성한 후 Page 인터페이스의 구현체인 PageImpl을 생성하여 반환하고 입력값으로 쿼리 결과, 페이지정보, 전체 개수 정보 순으로 입력
** 참고
- 강의에서 진행한 fetchResults와 fetchCount()를 이용한 내용은 생략함
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> result = queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.offset(pageable.getOffset()) // 시작 페이지정보
.limit(pageable.getPageSize()) // 출력할 개수 정보
.leftJoin(member.team, team)
.fetch();
// Querydsl에서 결과와 count를 같이 조회하는 코드가 디프리케이트 되었기 때문에 별도의 count 쿼리를 작성
// 디프리케이트된 메서드들 fetchCount(), fetchResults()
Long total = queryFactory
.select(member.count())
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
).fetchOne();
return new PageImpl<>(result, pageable, total); // Page의 구현체인 PageImpl에 쿼리의 정보들을 입력
}
(3) 테스트 및 결과
- 페이징이 적용된 메서드를 호출해서 테스트를 실행해보면 쿼리가 두번 실행되는 로그와 테스트가 통과되어 정상적으로 메서드가 동작되고있는 것을 알 수 있음
- 테스트 데이터의 개수가 적어 검색 조건은 빈 검색조건으로 넘겨서 메서드를 호출하였음
// 페이징 테스트
@Test
void searchPageSimple() {
/* ... 데이터 초기화 생략
(member1, 10, teamA)
(member2, 20, teamA)
(member3, 30, teamB)
(member4, 40, teamB) */
MemberSearchCondition condition = new MemberSearchCondition(); // 예제이므로 조건은 입력 X
PageRequest pageRequest = PageRequest.of(0, 3); // 페이지 정보만 생성
// 조건과 페이지 정보를 인수로 입력하여 메서드 호출
Page<MemberTeamDto> result = memberRepository.searchPageSimple(condition, pageRequest);
assertThat(result.getSize()).isEqualTo(3);
assertThat(result.getContent()).extracting("username")
.containsExactly("member1", "member2", "member3");
}
2) countQuery 최적화
(1) PageableExecutionUtils.getPage()
- 스프링 데이터 라이브러리가 제공하며 count 쿼리가 생략 가능한 경우 생략해서 처리함
- 페이지의 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때, 마지막 페이지일 때(정확히는 마지막 페이지이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때) count 쿼리가 생략됨
- 기존에 작성한 코드에서 반환타입을 PageableExecutionUtils의 getPage()로 변경하고 파라미터에 쿼리 결과, 페이징값, 그리고 별도의 쿼리로 작성했던 countQuery를 람다식으로 입력해주면 됨
- countQuery를 반환값의 파라미터로 입력하지않고 쿼리의 결과를 참조한 참조변수만 입력하면 이미 쿼리가 실행되기 때문에 최적화가 안됨
- 실무에서는 비즈니스 로직에 영향을 미치지 않는다면 가급적 getPage로 반환하는 것을 권장함(static import하여 getPage로 반환 가능)
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
// ... 구현 코드 생략
// 반환타입을 PageableExecutionUtils.getPage()로 설정 후 countQuery를 마지막 파라미터로 직접 입력
return getPage(result, pageable,
() -> queryFactory
.select(member.count())
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
).fetchOne());
}
3) 컨트롤러 개발
(1) 컨트롤러 추가
- 스프링 데이터 JPA에서 개발한 메서드를 호출하는 컨트롤러를 추가
- 파라미터에 조건과 페이지정보를 넘겨서 호출하는 인수로 입력하고 반환타입을 Page로 리턴
@GetMapping("/v2/members")
public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
return memberRepository.searchPageComplex(condition, pageable);
}
(2) 실행 결과
- 순수 JPA 강의때 입력해놓은 초기 데이터가 있어서 실행후 요청 파라미터로 페이징 정보를 입력하면 결과를 확인할 수 있음
- countQuery 최적화 조건에 맞춰서 size를 110으로 전체 데이터를 요청하면 countQuery가 실행되지 않고 최적화 되는 것도 확인할 수 있음
4) 스프링 데이터 정렬(Sort)
- 스프링 데이터 JPA가 제공하는 Sort를 Querydsl의 OrderSpecifier로 단순한 쿼리를 정렬할 수 있는데 조건이 조금만 복잡해져도 Sort기능을 사용하기가 어려움
- join을 활용하고 동적 정렬 기능이 필요하면 Sort를 사용하기보다는 파라미터를 받아서 직접 처리하는 것을 권장하며 자세한 방법은 다음 강의에서 작성
(1) 스프링 데이터 JPA의 정렬을 Querydsl의 정렬로 직접 전환하는 방법
JPAQuery<Member> query = queryFactory
.selectFrom(member);
for (Sort.Order o : pageable.getSort()) {
PathBuilder pathBuilder = new PathBuilder(member.getType(), member.getMetadata());
query.orderBy(
new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC,
pathBuilder.get(o.getProperty())));
}