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
- 스프링 고급 - 스프링 aop
- 스프링 mvc1 - 서블릿
- jpa 활용2 - api 개발 고급
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch2
- jpa - 객체지향 쿼리 언어
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch4
- 2024 정보처리기사 수제비 실기
- 스프링 mvc2 - 검증
- 스프링 입문(무료)
- 스프링 mvc2 - 타임리프
- 게시글 목록 api
- 자바의 정석 기초편 ch9
- 스프링 db2 - 데이터 접근 기술
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch6
- @Aspect
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch13
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch12
- 2024 정보처리기사 시나공 필기
Archives
- Today
- Total
나구리의 개발공부기록
실무 활용 - 순수 JPA 리포지토리와 Querydsl, 동적 쿼리와 성능 최적화 조회(Builder 사용, Where절 파라미터 사용), 조회 API 컨트롤러 개발 본문
인프런 - 스프링부트와 JPA실무 로드맵/실전! Querydsl
실무 활용 - 순수 JPA 리포지토리와 Querydsl, 동적 쿼리와 성능 최적화 조회(Builder 사용, Where절 파라미터 사용), 조회 API 컨트롤러 개발
소소한나구리 2024. 10. 30. 16:32출처 : 인프런 - 실전! Querydsl (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 순수 JPA 리포지토리와 Querydsl
1) 순수 JPA
(1) 순수 JPA의 리포지토리 코드
- JPA를 이용하여 구현하는 가장 기본적인 메서드들과 EntityManager와 JPAQueryFactory를 생성하는 생성자를 입력
package study.querydsl.repository;
@Repository
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
// 생성자로 EntityManager와 JPAQueryFactory를 생성할 수 있도록 작성
public MemberJpaRepository(EntityManager em) {
this.em = em;
this.queryFactory = new JPAQueryFactory(em);
}
public void save(Member member) {
em.persist(member);
}
// ofNullable()로 null을 체크해서 Optional로 반환
public Optional<Member> findById(Long id) {
Member findMember = em.find(Member.class, id);
return Optional.ofNullable(findMember);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class).getResultList();
}
public List<Member> findByUsername(String username) {
return em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", username)
.getResultList();
}
}
(2) JPA 리포지토리 테스트
package study.querydsl.repository;
@SpringBootTest
@Transactional
class MemberJpaRepositoryTest {
@Autowired EntityManager em;
@Autowired MemberJpaRepository memberJpaRepository;
@Test
void basicTest() {
Member member = new Member("member1", 10);
memberJpaRepository.save(member);
Member findMember = memberJpaRepository.findById(member.getId()).get();
assertThat(findMember).isEqualTo(member);
List<Member> result1 = memberJpaRepository.findAll();
assertThat(result1).containsExactly(member);
List<Member> result2 = memberJpaRepository.findByUsername("member1");
assertThat(result2).containsExactly(member);
}
}
2) JPA를 Querydsl로 변경
(1) 기존 리포지토리코드에서 Querydsl 메서드로 변경, 테스트 생략
- 순수 JPA로 작성한 findAll과 findByUsername의 메서드를 Querydsl로 변경
- 문자로 쿼리를 작성해서 IDE의 도움을 받지 못했던 순수 JPA와 달리 Querydsl은 IDE의 도움을 받아 기능을 작성할 때에도 힌트를 얻기 수월하고 타입체크가 가능해 안전함
- 해당 코드를 가지고 test를 수행해보면 정상적으로 동작함
/**
* 순수 JPA 메서드들을 Querydsl 변환
*/
private final JPAQueryFactory queryFactory;
// 전체조회
public List<Member> findAll_Querydsl() {
return queryFactory
.selectFrom(member)
.fetch();
}
// 이름으로 조회
public List<Member> findByUsername_Querydsl(String username) {
return queryFactory
.selectFrom(member)
.where(member.username.eq(username))
.fetch();
}
(2) JPAQueryFactory를 스프링 빈으로 등록
- MemberJpaRepository에서는 생성자를 통해서 JPAQueryFactory의 객체를 생성하였는데, 이것을 스프링부트 클래스에서 스프링 Bean으로 등록하면 private final로 선언만 해주면 롬복의 @RequiredArgsConstructor를 통해서 생성자 주입 코드를 생략하여 간단히 주입 받을 수 있음
- 그러나 두가지의 주입을 한번에 받게될 경우 테스트 코드를 작성할 때 조금 번거로울 수 있음
@SpringBootApplication
public class QuerydslApplication {
// ... main 메서드 생략
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
// Repository에 @RequiredArgsConstructor로 생성자를 주입
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
private final JPAQueryFactory queryFactory;
// ... 코드 생략
}
** 참고
- Querydsl은 EntityManager를 통해 동작하기 때문에 싱글톤으로 생성하여 여러객체에서 사용하여도 멀티쓰레드환경에서 사용 시 동시성 문제를 걱정하지 않아도됨
- 스프링이 주입해주는 EntityManager는 실제 동작 시점에 진짜 EntityManager를 찾아주는 프록시용 가짜 엔터티 매니저이며 이 가짜 EntityManager는 실제 사용 시점에 트랜잭션 단위로 실제 EntityManager를(영속성 컨텍스트) 할당해주기 때문에 문제가 없음
2. 동적 쿼리와 성능 최적화 조회 - Builder 사용
1) DTO 및 검색 조건 생성
(1) DTO 생성
- 회원과 팀의 정보를 한번에 담은 DTO 생성
- @QueryProjection을 이용하여 Q클래스를 생성, gradle - clean, build가 필요하며 DTO를 종속적이지않고 순수하게 유지하고 싶다면 Projection.bean() or fields() or constructor()를 사용하여 DTO를 사용하면 됨
@Data
public class MemberTeamDto {
private Long memberId;
private String username;
private int age;
private long teamId;
private String teamName;
@QueryProjection
public MemberTeamDto(Long memberId, String username, int age, long teamId, String teamName) {
this.memberId = memberId;
this.username = username;
this.age = age;
this.teamId = teamId;
this.teamName = teamName;
}
}
(2) 회원 검색 조건
- 클래스의 이름은 자유롭게 일관성만 있다면 줄여도 상관없음
- 회원이름과 팀이름은 String, 나이의 범위 값은 null이 들어올 수도 있기 때문에 Integer로 선언
@Data
public class MemberSearchCondition {
// 회원명, 팀명, 나이(ageGoe, ageLoe)
private String username;
private String teamName;
private Integer ageGoe;
private Integer ageLoe;
}
2) 동적쿼리 - Builder 사용
(1) 동적쿼리 작성
- 빌더를 생성한 후 예외 조건(null, 빈문자열을 통과했을 때 연산되어야할 조건들을 설정
- select 절에 QMemberTeamDto를 생성하고 각 필드의 값들을 작성하여 조회할 대상을 지정
- 검색조건들이 담긴 builder 변수를 where의 인수로 입력
// 동적쿼리 - builder 사용
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) {
BooleanBuilder builder = new BooleanBuilder();
// StringUtils.hasText()를 이용하면 null, ""(빈문자열)일때 false를 반환함
if (StringUtils.hasText(condition.getUsername())) {
builder.and(member.username.eq(condition.getUsername()));
}
if (StringUtils.hasText(condition.getTeamName())) {
builder.and(team.name.eq(condition.getTeamName()));
}
if (condition.getAgeGoe() != null) {
builder.and(member.age.goe(condition.getAgeGoe()));
}
if (condition.getAgeLoe() != null) {
builder.and(member.age.loe(condition.getAgeLoe()));
}
return queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.where(builder)
.leftJoin(member.team, team)
.fetch();
}
(2) Test
- 초기 데이터를 입력하고 만들어둔 검색조건 클래스를 생성하여 검색조건의 값들을 입력
- 검색조건을 통해 조회하는 메서드를 호출하여 결과를 호출하면 정상적으로 테스트가 통과되고 SQL도 조건들이 모두 들어가있는 쿼리가 생성되어있음
** 주의
- 만약 조건을 모두 제외하고 실행하면 DB의 전체 데이터를 가져오는 쿼리를 날려버리기 때문에 이런 작은 애플리케이션은 문제가 없을지라도 실무에서는 매우 조심해야함
- 이런 동적쿼리를 짤 때는 웬만하면 기본조건이 있거나 적어도 limit라도 있는것이 좋으며 가급적이면 페이징조건을 입력해서 제한을 두는 것을 권장함
// 동적쿼리 - Builder 사용
@Test
void searchTest() {
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
MemberSearchCondition condition = new MemberSearchCondition();
condition.setAgeGoe(35);
condition.setAgeLoe(45);
condition.setTeamName("teamB");
List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(condition);
assertThat(result).extracting("username").containsExactly("member4");
}
3. 동적 쿼리와 성능 최적화 조회 - Where절 파라미터 사용
1) 동적쿼리 - Where절 파라미터 사용
- 위에서 사용한 DTO와 검색조건을 동일하게 사용
(1) 동적쿼리 작성 및 테스트
- where절에 사용할 조건들을 별도의 메서드로 정의하고, 필요한 검색조건에 따라 해당 메서드를 호출하여 where절에 조건을 추가
- BooleanBuilder로 작성된 구조에 비해 where절로 조건을 메서드화하여 파라미터로 입력하면 Querydsl 구조가 where절에 어떤 조건이 있는지 명확하게 알 수 있으며, 그 조건의 동작을 상세하게 보려고 할때 메서드를 확인하는 구조로 되어있음
- 동일한 테스트케이스로 작성된 메서드를 테스트 수행해보면 정상적으로 조건들이 반영된 쿼리가 호출되고 테스트가 통과함
- 조건을 작성한 메서드의 반환타입은 Predicate보다 Querydsl의 BooleanExpression을 사용하는 것이 좋음
- 만들어둔 조건들을 조립해서 새로운 메서드로 뽑아서 사용할 수 있고, 메서드들을 재사용할 수 있는 장점이 있어 재사용성과 확장성 측면에서 유리함(물론 예외 처리는 잘 해주어야 함)
// 동적쿼리 - where절 파라미터 사용
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return 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())
)
.leftJoin(member.team, team)
.fetch();
}
// 조건들을 메서드로 입력
private BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? team.name.eq(teamName) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
4. 조회 API 컨트롤러 개발
1) 편리한 데이터를 확인하기 위한 설정 추가
- 테스트와 애플리케이션의 프로파일을 분리하여 실행시 다른 프로파일로 동작하도록 할 수 있음
- 현재 테스트코드는 별도의 초기화 데이터가 있기 때문에 테스트실행이 아닌 애플리케이션 실행시에는 또 다른 별도의 초기화 데이터를 입력하도록 하기 위함
(1) main 프로파일 설정
- ~/main/resources/application.yml에 아래 내용을 추가
- 보통 김영한님은 local 서버는 local, 개발 서버는 dev 이나 develop, 운영은 real 이런 식으로 입력한다고 함
spring:
profiles:
active: local
(2) test 프로파일 설정
- ~/test에 resources디렉토리를 생성하고 application.yml파일 생성
- 기존에 main에 있던 설정을 모두 복사한 뒤에 profiles의 active의 이름만 test로 작성해주면 됨
spring:
profiles:
active: test
(3) 샘플 데이터 추가하는 클래스 생성
- @Profile애노테이션을 사용하여 프로파일이 local일 때만 동작하도록하고 컴포넌트스캔 대상이 되도록 @Component 애노테이션을 작성
- 내부 클래스로 실제 초기화하는 클래스와 메서드를 생성한 뒤 외부클래스에서 내부클래스를 선언한 후 초기화 메서드를 호출하여 데이터를 초기화
- 내부적으로 동작하는 라이프사이클 때문에 @PostConstruct와 @Transactional을 동시에 사용할 수 없어서 이런 식으로 작성한다고 함
@Profile("local")
@Component
@RequiredArgsConstructor
public class InitMember {
private final InitMemberService initMemberService;
@PostConstruct
public void init() {
initMemberService.init();
}
@Component
static class InitMemberService {
@PersistenceContext
private EntityManager em;
@Transactional
public void init() {
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
for (int i = 0; i < 100; i++) {
Team selectedTeam = i % 2 == 0 ? teamA : teamB;
em.persist(new Member("member" + i, i, selectedTeam));
}
}
}
}
2) API 컨트롤러 개발
(1) 조회 컨트롤러
- API 컨트롤러이기 때문에 @RestController를 생성한 클래스에 적용
- 조회용이므로 @GetMapping을 적용하여 만들어둔 컨트롤러의 연산결과를 반환
package study.querydsl.controller;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberJpaRepository memberJpaRepository;
@GetMapping("/v1/members")
public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) {
return memberJpaRepository.search(condition);
}
}
(2) 실행결과
- 애플리케이션 실행 후 Postman으로 매핑된 url로 들어가보면 전체 데이터가 전부 조회됨
- MemberSearchCondition에 입력된 필드명을 쿼리파라미터로 조건을 입력해주면 조건이 적용되어 응답결과가 반환되는 것을 확인할 수 있음