프로젝트 생성
- InetelliJ-Ultimate Ver 이라면 new Project로 SpringBoot start를 해주면 되고, 그게 아니라면 아래 링크를 통해 프로젝트를 생성 다운로드해준다.
→ https://start.spring.io/ - 사용 기능: Spring Web, JPA, h2, lombok
- SpringBootVersion: 2.7.16
- groupId: study
- artifactId: querydsl
Querydsl 스프링 부트 3.0 설정은 다음을 참고
스프링 부트 3.0 관련 자세한 내용은 다음 링크
Gradle 전체 설정
동작 확인
기본 테스트 케이스 실행
스프링 부트 메인 실행 후 에러페이지로 간단하게 동작 확인(`http://localhost:8080')
테스트 컨트롤러를 만들어서 spring web 동작 확인(http://localhost:8080/hello)
테스트 컨트롤러
참고: IntelliJ Gradle 대신 자바로 바로 실행하기
IntelliJ 최신 버전은 Gradle로 실행하는것이 기본 설정인데, 이보다는 자바로 바로 실행하는게 좀 더 빠르다.
- Preferences → Build, Execution, Deployment → Build Tools → Gradle
- Build and run using: Gradle → IntelliJ IDEA
- Run tests using: Gradle → IntelliJ IDEA
롬복 적용
- Preferences plugin lombok 검색 실행 (재시작)
- Preferences Annotation Processors 검색 Enable annotation processing 체크 (재시작)
- 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인
Querydsl 설정과 검증
build.gradle 에 주석을 참고해서 querydsl 설정 추가
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.16'
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'
java {
sourceCompatibility = '11'
}
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'
// querydsl 추가
implementation "com.querydsl:querydsl-jpa:5.0.0"
implementation "com.querydsl:querydsl-apt:5.0.0"
runtimeOnly 'com.h2database:h2'
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
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
//querydsl 추가 끝
해당 build.gradle로 세팅 후 reload를 해서 importing 이 완료되었다면 검증용 엔티티를 생성하여 Querydsl이 정상적으로 세팅되었는지 확인해보자.
스프링 부트 2.6 이상, Querydsl 5.0 지원 방법
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.16'
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'
java {
sourceCompatibility = '11'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
//querydsl 추가
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}"
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor '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
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
//querydsl 추가 끝
- querydsl-jpa , querydsl-apt 를 추가하고 버전을 명시해야 한다.
검증용 엔티티 생성
@Entity
@Getter
@Setter
public class Hello {
@Id
@GeneratedValue
private Long id;
}
→ 여기까지 완료 되었으면, 검증용 QEntity를 생성하여 사용해보도록 하자.
Gradle IntelliJ 사용법
- Gradle → Tasks → build → clean
- Gradle → Tasks → other → compileQuerydsl
Gradle 콘솔 사용법
- ./gradlew clean compileQuerydsl
위와 같이 Gradle을 통해 Q타입 생성을 완료했다면 해당 경로에 QEntity.java가 생성되어 있는지 확인.
- build => generated => querydsl => study.querydsl.entity.QHello.java
※ 참고
Q타입은 컴파일 시점에 자동 생성되므로 버전관리(GIT)에 포함하지 않는 것이 좋다. 앞서 설정에서 생성 위치를 gradle build 폴더 아래 생성되도록 했기 때문에 이 부분도 자연스럽게 해결된다. (대부분 gradle build 폴더를 git에 포함하지 않는다.)
테스트 케이스 작성 및 검증
@SpringBootTest
@Transactional
class QuerydslApplicationTests {
@Autowired
EntityManager em;
@Test
void contextLoads() {
Hello hello = new Hello();
em.persist(hello);
JPAQueryFactory query = new JPAQueryFactory(em);
QHello qHello = new QHello("h"); //Querydsl Q타입 동작 확인
Hello result = query
.selectFrom(qHello)
.fetchOne();
assertThat(result).isEqualTo(hello);
//lombok 동작 확인 (hello.getId())
assertThat(result.getId()).isEqualTo(hello.getId());
}
}
※ 참고
스프링 부트에 아무런 설정도 하지 않으면 h2 DB를 메모리 모드로 JVM안에서 실행한다.
H2 데이터베이스 설치
개발이나 테스트 용도로 가볍고 편리한 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
스프링 부트 설정 - JPA, DB
application.yml
※ 참고
모든 로그 출력은 가급적 로거를 통해 남겨야 한다.
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
// 스프링 부트 3.0 이상을 사용하면 라이브러리 버전을 1.9.0 이상을 사용해야 한다.
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
※ 참고
쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로, 개발 단계에서는 편하 게 사용해도 된다. 하지만 운영시스템에 적용하려면 꼭 성능테스트를 하고 사용하는 것이 좋다.
예제 도메인 모델
Member 와 Team 두개의 엔티티를 구현하고 연관관계를 맺어줄 것이다.
그리고 해당 관계가 정상적으로 맺어졌는지 테스트까지 진행.
엔티티 클래스
- Member와 Team 은 다(N) 대 일(1) 관계다.
ERD
- 외래키(FK)는 Member에서 가지고 있는다.
회원(Member) 엔티티
@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);
}
}
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
- @Setter: 실무에서 가급적 Setter는 사용하지 않기
- @NoArgsConstructor (AccessLevel.PROTECTED): 기본 생성자 막고 싶은데, JPA 스팩상 PROTECTED로 열어두어야 함
- @ToString은 가급적 내부 필드만(연관관계 없는 필드만)
changeTeam() 으로 양방향 연관관계 한번에 처리(연관관계 편의 메소드)
팀(Team) 엔티티
@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 이 데이터베이스 외래키 값을 변경, 반대편은 읽기만 가능
데이터 확인 테스트
@SpringBootTest
@Transactional
@Commit
class MemberTest {
@Autowired
EntityManager em;
@Test
public void testEntity () {
//given
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());
}
}
}