실무 활용 - 순수 JPA와 Querydsl
- 김영한님의 실전! Querydsl 강의를 바탕으로 순수 JPA 리포지토리 구성 방식과 Querydsl을 활용한 동적 쿼리, 성능 최적화, 그리고 API 컨트롤러 개발에 이르는 실무 적용 방법을 정리함
순수 JPA 리포지토리와 Querydsl
- 순수 JPA 리포지토리
EntityManager와JPAQueryFactory를 함께 사용하여 순수 JPA와 Querydsl 방식을 병행할 수 있음
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 32 33
@Repository public class MemberJpaRepository { private final EntityManager em; private final JPAQueryFactory queryFactory; public MemberJpaRepository(EntityManager em) { this.em = em; this.queryFactory = new JPAQueryFactory(em); } public void save(Member member) { em.persist(member); } public Optional<Member> findById(Long id) { Member findMember = em.find(Member.class, id); return Optional.ofNullable(findMember); } 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(); } }
- JPAQueryFactory 스프링 빈 등록
JPAQueryFactory를 생성자에서 직접 만드는 대신 스프링 빈으로 등록하여 주입받아 사용할 수 있음
1 2 3 4
@Bean JPAQueryFactory jpaQueryFactory(EntityManager em) { return new JPAQueryFactory(em); }
- 스프링이 주입해 주는
EntityManager는 실제 동작 시점에 트랜잭션 단위로 바인딩된 실제 영속성 컨텍스트를 찾아주는 프록시용 가짜 엔티티 매니저이기 때문에 동시성 문제는 발생하지 않음
동적 쿼리와 성능 최적화 조회
-
조회 최적화용 DTO 설계
- 성능 최적화를 위해 필요한 필드만 선택하여 조회하는 전용 DTO를 활용함
@QueryProjection을 생성자에 사용하고compileQuerydsl을 실행하면 Q타입 DTO가 생성되며, 컴파일 타임에 타입을 검증할 수 있어 안정적임
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
@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; } }
-
검색 조건 DTO
1 2 3 4 5 6 7
@Data public class MemberSearchCondition { private String username; private String teamName; private Integer ageGoe; private Integer ageLoe; }
-
BooleanBuilder를 사용한 동적 쿼리
BooleanBuilder객체를 생성하고, 각 파라미터가 유효한지(null 또는 빈 문자열이 아닌지) 검사하여 조건을and()연산으로 누적하는 방식임
1 2 3 4 5 6 7 8 9 10
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) { BooleanBuilder builder = new BooleanBuilder(); return queryFactory .select(new QMemberTeamDto(member.id, member.username, member.age, team.id, team.name)) .from(member) .leftJoin(member.team, team) .where(builder) .fetch(); }
- Where절 파라미터를 사용한 동적 쿼리
- 조건을 별도의 메서드로 분리하여
where절에 파라미터로 직접 전달하며, 콤마(,)는 AND 연산으로 치환됨 - 메서드가
null을 반환하면 Querydsl이 해당 조건을 자동으로 무시하므로 편리함
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
public List<MemberTeamDto> search(MemberSearchCondition condition) { return queryFactory .select(new QMemberTeamDto(member.id, member.username, member.age, team.id, team.name)) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ) .fetch(); } private BooleanExpression usernameEq(String username) { return isEmpty(username) ? null : member.username.eq(username); }
- 조건을 별도의 메서드로 분리하여
-
동적 쿼리 방식 비교
- Where 절 파라미터 방식은 쿼리 자체의 가독성이 뛰어나고, 분리된 조건 메서드를 다른 쿼리에서도 재사용할 수 있어 권장됨
조회 API 컨트롤러 개발
-
프로파일 분리 및 데이터 초기화
application.yml설정으로 로컬(local)과 테스트(test) 프로파일을 분리하여 각 환경에 맞는 데이터를 활용함@Profile("local")을 적용한 초기화 컴포넌트(InitMember)를 구성하여, 서버 기동 시 샘플 데이터를 자동으로 삽입하도록 구현함
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
@Profile("local") @Component @RequiredArgsConstructor public class InitMember { private final InitMemberService initMemberService; @PostConstruct public void init() { initMemberService.init(); } @Component static class InitMemberService { @PersistenceContext 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)); } } } }
-
조회 API 연동 흐름
1 2 3 4 5 6 7 8 9 10 11
@RestController @RequiredArgsConstructor public class MemberController { private final MemberJpaRepository memberJpaRepository; @GetMapping("/v1/members") public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) { return memberJpaRepository.search(condition); } }
연습 문제
-
순수 JPQL 대비 Querydsl 사용 시 얻을 수 있는 주요 장점은 무엇일까요?
a. 컴파일 시점에 타입 안전성을 확보할 수 있다
- Querydsl은 자바 코드로 쿼리를 작성하기 때문에 컴파일 시점에 타입 오류를 잡아내어 런타임 오류 가능성을 줄여줌
-
JPA 환경에서 Querydsl 쿼리를 생성하고 실행하기 위해 가장 주요한 객체는 무엇인가요?
a. JPAQueryFactory
- Querydsl을 사용하여 JPA 쿼리를 작성하려면 EntityManager를 인자로 받아 JPAQueryFactory 객체를 생성해야 하며, 이 객체를 통해 실제 쿼리를 구성하고 실행함
-
Querydsl로 동적 쿼리(선택적 검색 조건)를 구현할 때, 강의에서 권장하는 WHERE 절 구성 방식은 무엇인가요?
a. WHERE 절에 Boolean Expression 파라미터를 사용한다
- WHERE 절에 Boolean Expression 형태의 파라미터를 받아 동적으로 조건을 조합하는 방식이 코드 가독성, 재사용성, 그리고 SQL 유사성 면에서 더 뛰어나 권장됨
-
복잡한 조회 쿼리의 결과로 엔티티 대신 DTO를 사용하는 것이 성능 최적화에 도움이 되는 이유는 무엇일까요?
a. 필요한 데이터 필드만 선택적으로 조회하기 때문
- DTO는 엔티티의 모든 필드 대신 조회에 필요한 특정 필드만 담도록 설계되므로 데이터 전송량을 줄여 성능을 개선할 수 있음
-
Spring 애플리케이션에서 개발, 테스트 등 환경별로 다른 설정이나 데이터 초기화 로직을 적용하기 위해 사용되는 기능은 무엇인가요?
a. @Profile
- @Profile 어노테이션을 사용하면 특정 프로파일(예: ‘local’, ‘test’)이 활성화되었을 때만 해당 빈(Bean)이 등록되거나 설정이 적용되도록 하여 환경별 구분을 쉽게 할 수 있음
요약 정리
JPAQueryFactory를 스프링 빈으로 등록하여 사용하면 코드가 간결해지며 동시성 문제 없이 영속성 컨텍스트에 접근할 수 있음@QueryProjection을 활용한 최적화 DTO 및 검색 조건 객체(MemberSearchCondition)를 기반으로 타입 안정성이 높은 동적 쿼리를 작성함- 동적 쿼리 구현 시,
BooleanBuilder방식보다where다중 파라터를 활용하는 방식이 가독성과 메서드 재사용성 측면에서 더 적합함 - 프로파일(
@Profile) 설정을 분리하여 로컬 개발 환경에서만 작동하는 자동 데이터 초기화 컴포넌트를 구성함으로써 API 테스트 편의성을 개선함