중급자를 위해 준비한
[백엔드, 웹 개발] 강의입니다.
Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!
✍️
이런 걸
배워요!
Querydsl을 기초부터 실무활용까지 한번에 배울 수 있습니다.
단순한 기능 설명을 넘어 실무활용 노하우를 배울 수 있습니다.
JPA를 사용할 때 동적 쿼리와 복잡한 쿼리 문제를 해결할 수 있습니다.
복잡한 쿼리, 동적 쿼리는 이제 안녕!
Querydsl로 자바 백엔드 기술을 단단하게.
???? 본 강의는 로드맵 과정입니다.
- 본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의입니다. 스프링 부트와 JPA 실무 완전 정복 로드맵을 우선 확인해주세요. (링크)
강좌
https://www.inflearn.com/course/querydsl-실전#
강의자료
https://github.com/braverokmc79/jpa-basic-lecture-file2
소스 :
https://github.com/braverokmc79/jpa-querydsl
[1] Querydsl 소개
1.소개
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=27939&tab=curriculum
2.강의자료
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30112&tab=curriculum
[2] 프로젝트 환경설정
3.프로젝트 생성
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30114&tab=curriculum
프로젝트 생성
스프링 부트 스타터(https://start.spring.io/)
Querydsl 스프링 부트 3.0 설정은 다음을 참고해주세요.
https://www.inflearn.com/chats/700670
스프링 부트 3.0 관련 자세한 내용은 다음 링크를 확인해주세요:
강좌 Gradle 전체 설정
plugins { id 'org.springframework.boot' version '2.2.2.RELEASE' id 'io.spring.dependency-management' version '1.0.8.RELEASE' id 'java' } group = 'study' version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } } test { useJUnitPlatform() }
현재 나의 Gradle 전체 설정
plugins { id 'java' id 'org.springframework.boot' version '2.7.9' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } group = 'study' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() }
동작 확인
기본 테스트 케이스 실행
스프링 부트 메인 실행 후 에러페이지로 간단하게 동작 확인(`http://localhost:8080')
테스트 컨트롤러를 만들어서 spring web 동작 확인(http://localhost:8080/hello)
테스트 컨트롤러
package study.querydsl.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "hello!"; } }
IntelliJ Gradle 대신에 자바로 바로 실행하기
최근 IntelliJ 버전은 Gradle로 실행을 하는 것이 기본 설정이다. 이렇게 하면 실행속도가 느리다. 다음과
같이 변경하면 자바로 바로 실행하므로 좀 더 빨라진다.
1. Preferences Build, Execution, Deployment Build Tools Gradle
2. Build and run using: Gradle IntelliJ IDEA
3. Run tests using: Gradle IntelliJ IDEA
롬복 적용
1. Preferences plugin lombok 검색 실행 (재시작)
2. Preferences Annotation Processors 검색 Enable annotation processing 체크 (재시작)
3. 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인
셋팅 -> build 검색 후 --> 그래들 build -> 설정
=> 롬복 적용
4.Querydsl 설정과 검증
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30115&tab=curriculum
1) build.gradle
//querydsl 추가 buildscript { ext { queryDslVersion = "5.0.0" } } plugins { id 'java' id 'org.springframework.boot' version '2.7.9' id 'io.spring.dependency-management' version '1.0.15.RELEASE' //querydsl 추가 id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" } group = 'study' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7' //querydsl 추가 implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" implementation "com.querydsl:querydsl-apt:${queryDslVersion}" compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() } //querydsl 추가 시작 def querydslDir = "$buildDir/generated/querydsl" querydsl { jpa = true querydslSourcesDir = querydslDir } sourceSets { main.java.srcDir querydslDir } compileQuerydsl{ options.annotationProcessorPath = configurations.querydsl } configurations { compileOnly { extendsFrom annotationProcessor } querydsl.extendsFrom compileClasspath } //querydsl 추가 끝
2) AppConfig
import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.persistence.EntityManager; @Configuration @RequiredArgsConstructor public class AppConfig { private final EntityManager em; @Bean public JPAQueryFactory queryFactory() { return new JPAQueryFactory(em); } }
설정후 그래드를 업그레이드 및 빌드처리하면 다음 화면과 같이 build 파일에 Q Entity 파일들이 생성된다.
3) Querydsl 환경설정 검증
검증용 엔티티 생성
package study.querydsl.entity; import lombok.Getter; import lombok.Setter; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity @Getter @Setter public class Hello { @Id @GeneratedValue private Long id; }
검증용 Q 타입 생성
Gradle IntelliJ 사용법
Gradle Tasks build clean
Gradle Tasks other compileQuerydsl
Gradle 콘솔 사용법
./gradlew clean compileQuerydsl
Q 타입 생성 확인
build generated querydsl
study.querydsl.entity.QHello.java 파일이 생성되어 있어야 함
참고: Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다.
앞서 설정에서 생성 위치를 gradle build 폴더 아래 생성되도록 했기 때문에 이 부분도 자연스럽게 해결된다.
(대부분 gradle build 폴더를 git에 포함하지 않는다.)
테스트 케이스로 실행 검증
package study.querydsl; import com.querydsl.jpa.impl.JPAQueryFactory; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Hello; import study.querydsl.entity.QHello; import javax.persistence.EntityManager; @SpringBootTest @Transactional @Rollback(value = false) class ApplicationTests { @Autowired EntityManager em; @Test void contextLoads() { Hello hello=new Hello(); em.persist(hello); JPAQueryFactory query=new JPAQueryFactory(em); //QHello qHello=new QHello("h"); QHello qHello=QHello.hello; //Querydsl Q타입 동작 확인 Hello result =query.selectFrom(qHello) .fetchOne(); Assertions.assertThat(result).isEqualTo(hello); //lombok 동작 확인(hello.getId()); Assertions.assertThat(result.getId()).isEqualTo(hello.getId()); } }
AppConfig 클래스 파일을 설정하면 다음과 같이 사용가능
package study.querydsl; import com.querydsl.jpa.impl.JPAQueryFactory; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Hello; import study.querydsl.entity.QHello; import javax.persistence.EntityManager; @SpringBootTest @Transactional @Rollback(value = false) class ApplicationTests { @Autowired EntityManager em; @Autowired JPAQueryFactory jpaQueryFactory; @Test void contextLoads2() { Hello hello=new Hello(); em.persist(hello); QHello qHello=QHello.hello; Hello result =jpaQueryFactory.selectFrom(qHello).fetchOne(); Assertions.assertThat(result).isEqualTo(hello); //lombok 동작 확인(hello.getId()); Assertions.assertThat(result.getId()).isEqualTo(hello.getId()); } }
Querydsl Q타입이 정상 동작하는가?
lombok이 정상 동작 하는가?
> 참고: 스프링 부트에 아무런 설정도 하지 않으면 h2 DB를 메모리 모드로 JVM안에서 실행한다
4) 사용예
public List<Order> findAll(OrderSearch orderSearch){ System.out.println("orderSearch = " + orderSearch); return em.createQuery("select o from Order o join o.member m " + " where o.status = :status " + " and m.name like :name ", Order.class) .setParameter("status", orderSearch.getOrderStatus()) .setParameter("name", orderSearch.getMemberName()) //.setFirstResult(10) .setMaxResults(1000) //최대 1000건 .getResultList(); }
package jpabook.jpashop.repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import jpabook.jpashop.domain.Order; import jpabook.jpashop.domain.OrderStatus; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import org.springframework.util.StringUtils; import java.util.List; import static jpabook.jpashop.domain.QMember.member; import static jpabook.jpashop.domain.QOrder.order; @Repository @RequiredArgsConstructor public class OrderRepository { private final JPAQueryFactory query; public List<Order> findAll(OrderSearch orderSearch){ return query.select(order) .from(order) .join(order.member, member) .where(statusEq(orderSearch.getOrderStatus()), nameLike(orderSearch.getMemberName())) .limit(1000) .fetch(); } //주문자 이름 확인 private BooleanExpression nameLike(String memberName){ if(!StringUtils.hasText(memberName)){ return null; } return member.name.like(memberName); } //주문 상태 private BooleanExpression statusEq(OrderStatus statusCond){ if(statusCond==null){ return null; } return order.status.eq(statusCond); } }
5.라이브러리 살펴보기
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30116&tab=curriculum
gradle 의존관계 보기
./gradlew dependencies --configuration compileClasspath
Querydsl 라이브러리 살펴보기
querydsl-apt: Querydsl 관련 코드 생성 기능 제공
querydsl-jpa: querydsl 라이브러리
스프링 부트 라이브러리 살펴보기
spring-boot-starter-web
spring-boot-starter-tomcat: 톰캣 (웹서버)
spring-webmvc: 스프링 웹 MVC
spring-boot-starter-data-jpa
spring-boot-starter-aop
spring-boot-starter-jdbc
HikariCP 커넥션 풀 (부트 2.0 기본)
hibernate + JPA: 하이버네이트 + JPA
spring-data-jpa: 스프링 데이터 JPA
spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅
spring-boot
spring-core
spring-boot-starter-logging
logback, slf4j
테스트 라이브러리
spring-boot-starter-test
junit: 테스트 프레임워크, 스프링 부트 2.2부터 junit5( jupiter ) 사용
과거 버전은 vintage
mockito: 목 라이브러리
assertj: 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
https://joel-costigliola.github.io/assertj/index.html
spring-test: 스프링 통합 테스트 지원
핵심 라이브러리
스프링 MVC
JPA, 하이버네이트
스프링 데이터 JPA
Querydsl
기타 라이브러리
H2 데이터베이스 클라이언트
커넥션 풀: 부트 기본은 HikariCP
로깅 SLF4J & LogBack
테스트
6.H2 데이터베이스 설치
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30117&tab=curriculum
개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공
https://www.h2database.com
다운로드 및 설치
h2 데이터베이스 버전은 스프링 부트 버전에 맞춘다.
권한 주기: chmod 755 h2.sh
데이터베이스 파일 생성 방법
jdbc:h2:~/querydsl (최소 한번)
~/querydsl.mv.db 파일 생성 확인
이후 부터는 jdbc:h2:tcp://localhost/~/querydsl 이렇게 접속
> 참고: H2 데이터베이스의 MVCC 옵션은 H2 1.4.198 버전부터 제거되었습니다. 이후 부터는 옵션 없이 사용하면 됩니다.
주의: 가급적 안정화 버전을 사용하세요. 1.4.200 버전은 몇가지 오류가 있습니다.
> 현재 안정화 버전은 1.4.199(2019-03-13) 입니다.
> 다운로드 링크: https://www.h2database.com/html/download.html
7.스프링 부트 설정 - JPA, DB
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30118&tab=curriculum
application.yml
spring: datasource: url: jdbc:h2:tcp://localhost/~/querydsl username: sa password: driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create properties: hibernate: # show_sql: true format_sql: true logging.level: org.hibernate.SQL: debug # org.hibernate.type: trace
application.properties
#yaml 전환사이트 https://mageddo.com/tools/yaml-converter #터미널 코드 컬러 출력 설정 spring.output.ansi.enabled=always #Springboot auto build spring.devtools.livereload.enabled=true spring.devtools.restart.enabled=true spring.driver-class-name=org.h2.Driver spring.datasource.url= jdbc:h2:tcp://localhost/~/querydsl spring.datasource.username=sa spring.datasource.password= spring.jpa.hibernate.ddl-auto= create #spring.jpa.properties.hibernate.show_sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.use_sql_comments=true #spring.jpa.properties.hibernate.default_batch_fetch_size=100 #페이지크기 spring.data.web.pageable.default-page-size=3 # 최대 페이지 크기 spring.data.web.pageable.max-page-size=2000 logging.level.org.hibernate.SQL=debug logging.level.org.hibernate.type=trace
spring.jpa.hibernate.ddl-auto: create
이 옵션은 애플리케이션 실행 시점에 테이블을 drop 하고, 다시 생성한다.
> 참고: 모든 로그 출력은 가급적 로거를 통해 남겨야 한다.
> show_sql : 옵션은 System.out 에 하이버네이트 실행 SQL을 남긴다.
> org.hibernate.SQL : 옵션은 logger를 통해 하이버네이트 실행 SQL을 남긴다.
쿼리 파라미터 로그 남기기
로그에 다음을 추가하기 org.hibernate.type : SQL 실행 파라미터를 로그로 남긴다.
외부 라이브러리 사용
https://github.com/gavlyukovskiy/spring-boot-data-source-decorator
스프링 부트를 사용하면 이 라이브러리만 추가하면 된다
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8'
> 참고: 쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로, 개발 단계에서는
편하게 사용해도 된다. 하지만 운영시스템에 적용하려면 꼭 성능테스트를 하고 사용하는 것이 좋다.
★쿼리 파라미터 로그 남기기 - 스프링 부트 3.0
p6spy-spring-boot-starter 라이브러리는 현재 스프링 부트 3.0을 정상 지원하지 않는다.
스프링 부트 3.0에서 사용하려면 다음과 같은 추가 설정이 필요하다
1) org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일 추가
src/resources/META-INF/spring/ org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.github.gavlyukovskiy.boot.jdbc.decorator.DataSourceDecoratorAutoConfigurati on
폴더명: src/resources/META-INF/spring
파일명: org.springframework.boot.autoconfigure.AutoConfiguration.imports
2) spy.properties 파일 추가
src/resources/spy.properties
appender=com.p6spy.engine.spy.appender.Slf4JLogger
이렇게 2개의 파일을 추가하면 정상 동작한다.
메이븐
<dependency> <groupId>com.github.gavlyukovskiy</groupId> <artifactId>p6spy-spring-boot-starter</artifactId> <version>1.9.0</version> </dependency>
[3] 예제 도메인 모델
8.예제 도메인 모델과 동작확인
강의 :
https://www.inflearn.com/course/lecture?courseSlug=querydsl-실전&unitId=30120&tab=curriculum
참고: 스프링 데이터 JPA와 동일한 예제 도메인 모델을 사용합니다. 스프링 데이터 JPA 강의를 들으신 분은
엔티티 코드만 작성하고 다음으로 넘어가도 됩니다.
예제 도메인 모델과 동작확인
Member 엔티티
package study.querydsl.entity; import lombok.*; import javax.persistence.*; @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {"id", "username" , "age"}) public class Member { @Id @GeneratedValue @Column(name="member_id") private Long id; private String username; private int age; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="team_id") private Team team; public Member(String username) { this(username, 0); } public Member(String username, int age) { this(username, age, null); } public Member(String username, int age, Team team){ this.username=username; this.age=age; if(team!=null){ changeTeam(team); } } private void changeTeam(Team team) { this.team=team; team.getMembers().add(this); } }
롬복 설명
@Setter: 실무에서 가급적 Setter는 사용하지 않기
@NoArgsConstructor AccessLevel.PROTECTED: 기본 생성자 막고 싶은데, JPA 스팩상 PROTECTED로 열어두어야 함
@ToString은 가급적 내부 필드만(연관관계 없는 필드만)
changeTeam() 으로 양방향 연관관계 한번에 처리(연관관계 편의 메소드)
Team 엔티티
package study.querydsl.entity; import lombok.*; import javax.persistence.*; import java.util.ArrayList; import java.util.List; @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {"id", "name" }) public class Team { @Id @GeneratedValue @Column(name="team_id") private Long id; private String name; @OneToMany(mappedBy = "team") private List<Member> members=new ArrayList<>(); public Team(String name){ this.name=name; } }
Member와 Team은 양방향 연관관계, Member.team 이 연관관계의 주인, Team.members 는 연관관계의
주인이 아님, 따라서 Member.team 이 데이터베이스 외래키 값을 변경, 반대편은 읽기만 가능
데이터 확인 테스트
package study.querydsl.entity; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Commit; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @Transactional //@Commit class MemberTest { @Autowired EntityManager em; @Test public void testEntity(){ 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); //초기화 em.flush(); em.clear(); List<Member> members = em.createQuery("select m from Member m ", Member.class).getResultList(); for (Member member :members){ System.out.println("member = " + member); System.out.println("-> member.team" +member.getTeam()); } } }
가급적 순수 JPA로 동작 확인 (뒤에서 변경)
db 테이블 결과 확인
지연 로딩 동작 확인
댓글 ( 4)
댓글 남기기