Springboot 에서 Querydsl 사용하기

시작하기

앞에서 배웠던 기본적인 JPA(Hibernate) 만으로는 부족하다고 느껴질 경우가 있다. 원하는 결과 값이 아닌 모든 결과를 전부 데이터베이스로부터 얻어와서 가공하거나 ignore 처리를 해서 내보내야 하는 불편등이 있을 수 있겠고, 세밀한 쿼리를 직접 제어하고 싶은 경우도 있다. 이러하 이유로 인해서 여러가지 방안들이 제시되고 있는데 그 중에는 criteria 나 querydsl 등이 있다. 나는 querydsl이 조금 더 직관적이라고 생각이 들었고, 그러한 이유로 이번에는 querydsl 을 설명하고자 한다.

Querydsl 설정

Querydsl 을 시작함에 있어서 가장 힘든 점은 설정 부분이다. 미리 설명을 하자면 쿼리 DSL 을 사용하기 위해서는 precomfile 과정에서 querydsl에서 사용하기 위한 객체를 Entity로부터 생성해야 한다. 일단 Gradle 설정을 보자. (참고로 Querydsl 버전에 따라 다양한 문제가 발생한다. 함수 용도가 바뀌고, gradle 설정이 바뀌고.. 여기서는 거의 최신 버전인 4.1.4를 사용했다)

//QueryDSL
	compile('com.querydsl:querydsl-core')
	compile('com.querydsl:querydsl-apt') <== 마법같은 일이 일어나게 해준다고 외국에  아저씨는 설명한다
	compile('com.querydsl:querydsl-jpa')

위와 같이 querydsl 을 위한 라이브러리를 dependency 에 추가하자

sourceSets {
	generated {
		java {
			srcDirs =  ["src/main/generated"]
		}
	}
}

configurations {
	querydslapt <== 위에 compile 부분에 선언하고 여기와 같이 선언함으로써 gradle에서 마법과 같이 많은 처리를 해주는 task 연결되게  준다
}

task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Query dsl Generator') {
	source = sourceSets.main.java
	classpath = configurations.compile + configurations.querydslapt
	options.compilerArgs = [
			"-proc:only",
			"-processor", "com.querydsl.apt.jpa.JPAAnnotationProcessor"
	]
	options.encoding = 'UTF-8'
	destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}

compileJava {
	dependsOn generateQueryDSL
	source generateQueryDSL.destinationDir
}

compileGeneratedJava {
	dependsOn generateQueryDSL
	options.warnings = false
	classpath += sourceSets.main.runtimeClasspath
}

clean {
	delete sourceSets.generated.java.srcDirs.iterator().next()
}

다음 부분부터 보도록 하자.

sourceSets {
	generated {
		java {
			srcDirs =  ["src/main/generated"]
		}
	}
}

이 부분은 querydsl 이 사용할 class들이 위치할 폴더에 생성 위치를 나타낸다.

task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Query dsl Generator') {
	source = sourceSets.main.java
	classpath = configurations.compile + configurations.querydslapt
	options.compilerArgs = [
			"-proc:only",
			"-processor", "com.querydsl.apt.jpa.JPAAnnotationProcessor"
	]
	options.encoding = 'UTF-8'
	destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}

이 부분이 중요한데 source = sourceSets.main.java 이 부분은 기존에 jpa entity가 위치한 경로를 나타낸다.(상위 경로이더라도 알아서 recursive 하게 찾아준다) 나머지 부분들은 querydsl processor 로 해당 entity 들을 컴파일 하여 앞서 설정한 폴더에 위치시키는 역할을 한다.

그 아래 부분들은 위에 generateQueryDSL task 를 각라이프 사이클에서 실행할 수 있도록 하거나, gradle clean 시 삭제 하도록 하는 역할을 한다.

gradle compile!!

일단 entity 들은 전 게시물에서 만들었던 UserUserExtra 를 사용하겠다. 콘솔창이나 IDE에서 gradle buildgradle bootRepackage 를 실행하거나 실행 될 수 있도록 한다.

$> gradle build
:generateQueryDSL
Note: Running JPAAnnotationProcessor
Note: Serializing Supertypes
Note: Generating com.example.domain.QBaseUser for [com.example.domain.BaseUser]
Note: Serializing Entity types
Note: Generating com.example.domain.QUser for [com.example.domain.User]
Note: Generating com.example.domain.QUserExtra for [com.example.domain.UserExtra]
Note: Running JPAAnnotationProcessor
Note: Running JPAAnnotationProcessor
:compileJava

위와 같이 Java compile 이전 단계에서 JPAAnnotationProcessor 가 실행 되서 우리가 만들었던 Entity 들을 Querydsl 이 사용할 수 있는 QUser, QUesrExtra Class 들로 재생성한다.

Querydsl 을 작성하고 실행해보자

이제 본격적으로 Querydsl 을 사용해 볼 차례이다. 단독으로 사용할 수도 있지만 이전에 사용했던 Hibernate 용 interface와 함꼐 쓰기 위해서 조금은 특별한 구조를 만들어 보려고 한다.

먼저 이전 게시글과는 다르게 원하는 결과값만을 저장할 DTO class를 하나 생성한다.

public class UserDTO implements Serializable {

    private static final long serialVersionUID = -3777937207533558441L;

    private Integer userNo;
    private String userName;
    private String phoneNum;

    @QueryProjection // 하단에 나올 Projection 을 통해 쿼리 결과가 바로 이곳으로 들어온다
    public UserDTO(Integer userNo, String userName, String phoneNum) {
        this.userNo = userNo;
        this.userName = userName;
        this.phoneNum = phoneNum;
    }

    public Integer getUserNo() {
        return userNo;
    }

    public void setUserNo(Integer userNo) {
        this.userNo = userNo;
    }

    public String getPhoneNum() {
        return phoneNum;
    }

    public void setPhoneNum(String phoneNum) {
        this.phoneNum = phoneNum;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
}

그리고 UserRepositoryCustom이라는 interface를 하나 생성하자. 이 interface는 사용자를 가져오는 함수하나와 사용자 모두를 가져오는 함수 하나에 대한 interface이다.

public interface UserRepositoryCustom {

    UserDTO getUserByQuerydsl(Integer id);
    Page<UserDTO> getUserList(Pageable pageable);
}

구현체를 만들자

@Repository
public class UserRepositoryImpl extends QueryDslRepositorySupport implements UserRepositoryCustom{

    @PersistenceContext
    private EntityManager entityManager;

    public UserRepositoryImpl() {
        super(User.class);
    }

    @Override
    public UserDTO getUserByQuerydsl(Integer id) {

        JPAQueryFactory queryFactory = new JPAQueryFactory(this.getEntityManager());

        QUser user = QUser.user;
        QUserExtra extra = QUserExtra.userExtra;

        return queryFactory.select(Projections.constructor(UserDTO.class, user.userNo, user.userName,
                extra.phoneNum))
                .from(user)
                .innerJoin(user.userExtra, extra) // entity에서 관계를 맺은 조건으로 자동으로 on 절이 생성 됨
                .where(user.userNo.eq(id)).fetchOne();

    }

    @Override
    public Page<UserDTO> getUserList(Pageable pageable) {
        JPAQueryFactory queryFactory = new JPAQueryFactory(this.getEntityManager());

        QUser user = QUser.user;
        QUserExtra extra = QUserExtra.userExtra;

        QueryResults<UserDTO> result = queryFactory.select(Projections.constructor(UserDTO.class, user.userNo, user.userName,
                extra.phoneNum))
                .from(user)
                .innerJoin(user.userExtra, extra) // entity에서 관계를 맺은 조건으로 자동으로 on 절이 생성 됨
                .offset(pageable.getOffset()) // offset과
                .limit(pageable.getPageSize()) // Limit 을 지정할 수 있고
                .orderBy(user.userNo.desc()) // 정렬도 가능하다
                .fetchResults();

        return new PageImpl<>(result.getResults(), pageable, result.getTotal());
    }
}

차근 차근 설명해 보겠다

@Repository
public class UserRepositoryImpl extends QueryDslRepositorySupport implements UserRepositoryCustom{

    @PersistenceContext
    private EntityManager entityManager;

    public UserRepositoryImpl() {
        super(User.class);
    }

extends QueryDslRepositorySupport 는 Querydsl 을 사용하게 해주도록 한다. @PersistenceContext 이하 부분은 datasource와의 연결과 transaction 처리를 도와주는 entitymanager 를 사용할 수 있게 해준다. 그 다음 생성자 부분은 이 Repository가 User class와 맵핑 됨을 부모클래스에 알려준다.

나머지 부분들은 일반 쿼리 형태와 비슷하다. 단지 우리가 만든 UserDTO에 직접 값을 넣기 위해서 Projections.constructor 라는 것을 사용했다. 추가로 사용자 리스트를 얻어오는 것은 JPA 에서 제공해주는 Pageable 을 사용했는데 아래에서 설명하겠다.

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

일단 위와 같이 모두 구현 했으면 기존에 만들었던 UserRepository 와 연결을 해보자.

public interface UserRepository extends JpaRepository<User, Integer>, UserRepositoryCustom {

}

자 이렇게 된다면 기존에 hibernate에서 제공해주는 orm 함수와 우리가 만든 querydsl함수들이 모두 실행 가능하다. 둘중 필요한 부분을 선택해서 사용하면 된다.

자 이제 이것을 실행할 service 와 controller 를 만들어 보자.

DemoService.java

public UserDTO getUserByQueryDSL(Integer id) {
    return userRepository.getUserByQuerydsl(id);
}

public Page<UserDTO>  getUserListByQueryDSL(Pageable pageable) {
    return userRepository.getUserList(pageable);
}

DemoController.java

@RequestMapping(value="/user-by-querydsl/{id}", method = RequestMethod.GET)
public @ResponseBody
UserDTO getUserByQueryDSL(@PathVariable Integer id) {

    return demoService.getUserByQueryDSL(id);
}


@RequestMapping(value="/user/list", method = RequestMethod.GET)
public @ResponseBody
Page<UserDTO> getUserListByQueryDSL(@PageableDefault(size = 15) Pageable pageable) {
    return demoService.getUserListByQueryDSL(pageable);
}

페이징 처리를 위해서 JPA에서 편리함을 위해 제공해주는 Pageable 인터페이스를 사용했다. 해당 인터페이스는 size와 page 인자를 받아서 위에 있는 UserRepositoryImpl 에서 활용할 수 있다. 결과는 Page 객체로 내려오는데 해당 객체에 형태는 위에 예제를 실행한 결과 값으로 보여주도록 하겠다.

이제 실행해보자.

http://localhost:5000/user-by-querydsl/11

{
  "userNo": 11,
  "userName": "test2",
  "phoneNum": "01099999999"
}
http://localhost:5000/user/list?page=0

{
  "content": [
    {
      "userNo": 11,
      "userName": "test2",
      "phoneNum": "01099999999"
    },
    {
      "userNo": 9,
      "userName": "test",
      "phoneNum": "01011112222"
    }
  ],
  "totalElements": 2,
  "totalPages": 1,
  "last": true,
  "size": 15,
  "number": 0,
  "sort": null,
  "first": true,
  "numberOfElements": 2
}

자 잘 처리 된다. 페이징에 경우 content 에 결과 리스트가 나머지 부분들은 페이징에서 활용할 수 있는 다양한 값들이 담겨 있다.

%주의% 위에서도 짧게 언급했지만 Querydsl 은 버전에 따라서 사용법이 많이 다르다. 자기 버전에 맞는 Document 를 꼭 참고하도록 하자. Document URL 은 http://www.querydsl.com/static/querydsl/4.1.4/reference/html/ch02.html 이다.

이렇게 hibernate 만으로 한계가 있던 부분들을 querydsl로 보완하여 사용한다면 mybatis 에 비해 크게 불편함이 없으면서도, 어느 부분에서는 더 쉽고, DB에 종속적이지 않으며, 보안적인 측면에서 조금 더 우수하게 개발을 할 수 있을 것이라 생각한다.

너무 긴 설명을 한 게시물에 설명해서 부족한 부분이 있을 수 있습니다. 댓글을 달아주시면 성심 성의로 답변을 달아 드리겠습니다.

 

[출처] https://doohwan-yoo.github.io/querydsl/

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
81 SpringBoot JPA 예제(@OneToMany, 단방향) 졸리운_곰 2018.12.31 193
80 JPA OneToOne? 자바 jpa join 졸리운_곰 2018.12.31 203
79 Spring Data JPA 연관관계 매핑하는 방법 졸리운_곰 2018.12.31 179
78 JPA 생성과 수정시 날짜시간 자동삽입 Hibernate generate timestamp on create and update 졸리운_곰 2018.12.13 2089
77 JPA로 insert / update시 날짜시간 자동으로 설정 How to create an auto-generated Date/timestamp field in a Play! / JPA? 졸리운_곰 2018.12.13 975
76 Spring Data repository with empty IN clause. 졸리운_곰 2018.11.16 237
75 Spring Data JPA Tutorial: Introduction to Query Methods 졸리운_곰 2018.11.16 266
74 JPA OrderColumn 에서의 정렬 order by 졸리운_곰 2018.11.16 720
73 Jpa @elementcollection 의 데이터 정렬 @orderby : JPA - Using @OrderBy Annotation file 졸리운_곰 2018.11.16 877
» Springboot 에서 Querydsl 사용하기 졸리운_곰 2018.09.18 283
71 Springboot 에서 DATA-JPA(Hibernate) 사용하기[3] - JOIN file 졸리운_곰 2018.09.18 185
70 Springboot 에서 DATA-JPA(Hibernate) 사용하기[2] - Entity, Repository, CRUD file 졸리운_곰 2018.09.18 259
69 Springboot 에서 DATA-JPA(Hibernate) 사용하기[1] - 기초 설정 졸리운_곰 2018.09.18 319
68 JPA_Mini_Book 이북 Java JPA file 졸리운_곰 2018.08.27 218
67 [Mybatis] parameterType="String" 사용시 문제점 졸리운_곰 2018.08.22 1510
66 MyBatis 에서 출력하는 에러 보기 : Catch exception in MyBatis 졸리운_곰 2018.08.22 354
65 MyBatis 기본 - insert,delete,update 졸리운_곰 2018.08.22 165
64 %Like% Query in spring JpaRepository 졸리운_곰 2018.08.20 189
63 Spring Data JPA Tutorial: Creating Database Queries From Method Names 졸리운_곰 2018.08.20 245
62 Spring Data JPA 에서 Java8 Date-Time(JSR-310) 사용하기 file 졸리운_곰 2018.05.28 257
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED