5 분 소요

📌 Topic

  • Querydsl 소개
  • 프로젝트 생성
  • Querydsl 설정과 검증
  • 라이브러리 살펴보기
  • H2 데이터베이스 설치
  • 스프링 부트 설정 - JPA, DB

01. Querydsl 소개

동적 쿼리의 한계를 극복하기 위해 나온 기술 중 하나 Querydsl F/W

최신 자바 백엔드 기술로는 Spring Boot + Spring Data JPA 조합으로 사용이 된다.
하지만 이러한 기술만으로는 복잡한 쿼리(동적 쿼리)를 해결할 수가 없다.

이러한 문제를 해결하기 위해 나온 기술이 Querydsl이다. Querydsl은 쿼리를 자바 코드로 작성할 수 있게 만들어 문법 오류를 컴파일 시점에 잡아낼 수 있다는 장점이 존재한다.

@Test
public void querydsl() {
    String userName = "kim";
    List<Member> members = queryFactory
            .select(member)
            .from(member)
            .where(member.userName.eq(userName))
            .fetch();
}

위와 같이 메서드 체이닝을 사용하여 각 요소에 맞는 값을 셋팅해주고
이상이 있는 경우 컴파일 시점에 오류를 발견할 수 있다.

02. 프로젝트 생성

기본적인 셋팅은 제외하고 Querydsl 관련된 내용만 정리

02-1. Tech Spec

Tech Version
Spring Boot 2.7.1
Java 11
H2 2.1.2

02-2. 간단한 애플리케이션 테스트

package com.study.querydsl.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    /**
     * Test Mapping
     * @return
     */
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

간단한 컨트롤러를 생성하여 이상이 없는지 테스트.

02-3. Lombok 관련 설정

Build, Execution, Deployment > Compiler > Annotation Processors

  • Lombok plugin 설치
  • Enable annotation processing 선택
    • 기존에 체크가 안되있는 경우가 있음
  • Intellij 재시작

03. Querydsl 설정과 검증

이번 시간에는 Querydsl의 설정과 검증 과정을 진행 한다

03-1. Querydsl 플러그인 추가

// build.gradle
plugins {
	id 'org.springframework.boot' version '2.7.1'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" // querydsl 추가
	id 'java'
}
  • Querydsl plugin 추가
implementation 'com.querydsl:querydsl-jpa' // querydsl lib 추가
  • 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
}
  • build와 관련된 프로세싱 내용 추가

03-2. Querydsl 빌드

package com.study.querydsl.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Getter
@Setter
@Entity
public class Hello {

    @Id
    @GeneratedValue
    private Long id;

}
  • Querydsl 빌드 프로세싱 테스트를 위해 Hello 엔티티 생성

other-querydsl-build

  • Gradle > Tasks > build > build 더블 킄릭(이 부분으로 카바 가능)
  • Gradle > Tasks > other > compileQuerydsl 더블 클릭

after-build

  • build.gradle에서 설정한 경로에 Querydsl 관련 파일이 생성됨
  • QHello라는 Java 파일이 생성된 것 역시 확인 가능
package com.study.querydsl.entity;

/**
 * QHello is a Querydsl query type for Hello
 */
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QHello extends EntityPathBase<Hello> {

    private static final long serialVersionUID = -1353511186L;

    public static final QHello hello = new QHello("hello");

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public QHello(String variable) {
        super(Hello.class, forVariable(variable));
    }

    public QHello(Path<? extends Hello> path) {
        super(path.getType(), path.getMetadata());
    }

    public QHello(PathMetadata metadata) {
        super(Hello.class, metadata);
    }
}
  • QHello 파일은 위와 같이 작성이 되어있다
  • 해당 파일은 엔티티를 보고 Querydsl에 의해 만들어진 파일이다
  • Generated QFile은 Git 같은 오픈소스에서 관리가 되면 안된다

03-3. Querydsl 설정 테스트

src/test/java/com/study/querydsl/SpringDataJpaQuerydslApplicationTests.java

package com.study.querydsl;

import com.querydsl.jpa.impl.JPAQueryFactory;
import com.study.querydsl.entity.Hello;
import com.study.querydsl.entity.QHello;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Transactional
class SpringDataJpaQuerydslApplicationTests {

	@PersistenceContext
	private EntityManager em;


	@Test
	void contextLoads() {
		// 객체 생성 : Hello Entity
		Hello hello = new Hello();
		em.persist(hello);

		// Querydsl 사용을 위해 JPAQueryFactory 생성
		JPAQueryFactory query = new JPAQueryFactory(em);
		QHello qHello = new QHello("h");

		// Querydsl 사용
		Hello result = query
				.selectFrom(qHello)
				.fetchOne();

		// 검증 : 하나의 트랜잭션 단위 내에서 동일한 엔티티 반환을 보장해야 한다
		assertThat(result).isEqualTo(hello);
		assertThat(result.getId()).isEqualTo(hello.getId());
	}
}

지금까지 위에서 Querydsl 관련 설정을 진행 하였다.
간단한 테스트 코드를 작성하여 설정이 잘 되었는지 확인 해보자.

test_success

테스트 코드를 실행하여 위와 같이 성공이 화면이 보이면
Querydsl 설정이 잘된 것이다.

04. 예제 도메인 모델

참고: 스프링 데이터 JPA와 동일한 예제 도메인 모델을 사용한다.

example_domain_model

  • 하나의 Team이 한 명의 Member를 갖는 구조
  • Team(N) : Member(1) 관계
  • xToOne(ManyToOne, OneToOne) 관계는 지연로딩(fetch = LAZY) 설정 필요

04-1. Member 엔티티 작성

package com.study.querydsl.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Getter
@Setter // 실무 지양
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA는 스펙상 기본 생성자가 필요하다
@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);
    }
}

스프링 데이터 JPA를 진행할 때 만들었던
Member 엔티티와 동일한 구조로 작성 되었다.

id, userName, age를 기본 필드로 갖고
Team 과의 연관관계 설정을 위해 team 변수를 갖는다.
또한 현재 ManyToOne 관계이기에 지연로딩 설정을 추가 하였다.

04-2. 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;

    // - 1:N 관계로, 연관관계의 주인을 Member의 Team 변수로 설정
    //   - 연관관계 주인쪽만 등록(INSERT), 수정(UPDATE), 삭제(DELETE)가 가능하다
    //   - 연관관계가 아닌 경우 읽기(SELECT)는 가능하지만, 나머지는 불가능하다
    //   - 정확하지는 않지만 실무에서는 양방향 관계보다는 단방향 연관관계로 전부 설정해서 사용한다고 함
    @OneToMany(mappedby = "team")
    private List<Member> members = new ArrayList<>();

    // team명 셋팅 생성자
    public Team(String name) {
        this.name = name;
    }
}

Team 엔티티의 경우 기본적으로 id, name(팀명)을
필드로 갖고 List<Member> members 변수를 통해 연관관계 설정을 하였다.

04-3. 엔티티 테스트 코드 작성

package com.study.querydsl.entity;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Transactional
class MemberTest {

    @PersistenceContext
    EntityManager em;

    @Test
    @Rollback(false)
    public void testEntity() {
        Team teamA = new Team("데이터 플랫폼 팀");
        Team teamB = new Team("인프라 팀");
        Team teamC = new Team("웹 개발 팀");
        Team teamD = new Team("APP 개발 팀");

        em.persist(teamA);
        em.persist(teamB);
        em.persist(teamC);
        em.persist(teamD);

        // teamA
        Member member1 = new Member("김영민", 30, teamA);
        Member member2 = new Member("원영식", 30, teamA);
        em.persist(member1);
        em.persist(member2);

        // teamB
        Member member3 = new Member("김진엽", 29, teamB);
        Member member4 = new Member("박진우", 29, teamB);
        Member member5 = new Member("임수현", 29, teamB);
        em.persist(member3);
        em.persist(member4);
        em.persist(member5);

        // teamD
        Member member6 = new Member("김주한", 19, teamD);
        em.persist(member6);

        // 초기화
        em.flush(); // DB 반영
        em.clear(); // 영속성 컨텍스트 초기화

        //---------------------------------------------------------------------------//

        // 01. 단일 회원 테스트
        Member findMemberByUserName = em.createQuery("select m from Member m where m.userName = :userName", Member.class)
                                .setParameter("userName", "김영민")
                                .getSingleResult();

//        assertThat(member1).isEqualTo(findMemberByUserName); // em.flush(), em.clear()를 사용하지 않고 비교 == 'true'
        assertThat(member1.getUserName()).isEqualTo(findMemberByUserName.getUserName());

        // 02. 전체 팀 조회
        List<Team> findTeam = em.createQuery("select t from Team t", Team.class).getResultList();
        assertThat(findTeam.size()).isEqualTo(4); // 팀이 4개가 맞는가?
//        assertThat(findTeam.size()).isEqualTo(5); // fail

        // 03. 전체 회원 조회
        /*List<Member> findMember = em.createQuery("select m from Member m", Member.class).getResultList();
        System.out.println("----------------------------------------------------");
        for (Member member : findMember) {
            System.out.println("[01: AAA] -> ✅ member = " + member);
            System.out.println("[02: BBB] ---> ⚡ member.team = " + member.getTeam()); // 지연로딩 -> 실제 team.get
            System.out.println();
        }
        System.out.println("----------------------------------------------------");*/
    }
}
  • Team, Member를 각각 생성한 후 전체 회원을 조회하는 테스트 코드 작성
  • 지연 로딩으로 설정된 team의 경우 실제 사용이 될 때 쿼리가 발생 된다
  • 테스트 코드를 돌렸을 때 문제가 없으면 연관관계 셋팅과 엔티티 생성이 잘 된 것이다

참고 자료

댓글남기기