QueryDsl 왜 쓰나요?

JPA가 기본적으로 제공해주는 CRUD 메서드 및 쿼리 메서드 기능을 사용하더라도 원하는 조건의 데이터를 수집하기 위해서는 필연적으로 JPQL을 작성하게 됩니다.
간단한 로직은 큰 문제가 없으나, 복잡한 로직의 경우 쿼리 문자열이 상당히 길어집니다.
JPQL 문자열에 오타 혹은 문법적인 오류가 존재하는 경우, 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생하게 됩니다.

이러한 문제를 어느정도 해소하는데 기여하는 프레임워크가 바로 QueryDsl 입니다.
QueryDsl은 정적 타입을 이용해서 Query를 생성해주는 프레임워크입니다.


QueryDsl의 장점과 단점은 무엇인가요?


QueryDsl 장점

1) 문자가 아닌 코드로 쿼리를 작성함으로써 컴파일 시점에 문법 오류를 쉽게 확인할 수 있습니다.

2) IDE의 자동완성으로 도움을 받을 수 있습니다.

3) 동적인 쿼리 작성이 편리합니다.

4) 쿼리 작성시 제약 조건 등을 메서드 추출을 통해 간편하고 재사용할 수 있습니다.


QueryDsl 단점

1) 번거로운 Gradle 설정 및 사용법을 배워야 합니다.



QueryDsl 예제 코드

  • Springboot 2.7.10, java 11, gradle 8.4 기반으로 작성한 예제 코드입니다.
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.14'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.oauth2.resource.server'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '11'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'com.querydsl:querydsl-jpa'
    implementation 'com.querydsl:querydsl-apt'

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.7.14'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'

    // queryDsl 설정
    implementation "com.querydsl:querydsl-jpa"
    implementation "com.querydsl:querydsl-core"
    implementation "com.querydsl:querydsl-collections"
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa" // querydsl JPAAnnotationProcessor 사용 지정
    annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
    annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
}

// Querydsl 설정부
def generated = 'src/main/generated'

// querydsl QClass 파일 생성 위치를 지정
tasks.withType(JavaCompile) {
    options.getGeneratedSourceOutputDirectory().set(file(generated))
}

// java source set 에 querydsl QClass 위치 추가
sourceSets {
    main.java.srcDirs += [ generated ]
}

// gradle clean 시에 QClass 디렉토리 삭제
clean {
    delete file(generated)
}

//tasks.named('test') {
//    useJUnitPlatform()
//}
사용자 Entity
package com.cafe.be.api.common.entity;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.sql.Timestamp;
import java.time.LocalDateTime;

@MappedSuperclass
public class BaseEntity {

    // 생성일
    @CreationTimestamp
    private LocalDateTime createDt;

    // 수정일
    @UpdateTimestamp
    private LocalDateTime modifyDt;

}

package com.cafe.be.api.userservice.entity;

import com.cafe.be.api.common.entity.BaseEntity;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Users extends BaseEntity {

    // 사용자 번호
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userNo;

    // 이메일
    private String email;

    // 비밀번호
    private String pw;

    // 닉네임
    private String nick;

    // 사용자 상태
    private String status;

    // 권한
    private String role;

    // 생년월일
    private String birth;

    // 이용약관 동의
    private String agree1;

    // 개인정보 수집 및 이용 동의
    private String agree2;

    // 이벤트, 프로모션, 메일, sms 수신
    private String agree3;

}
사용자 Repository
package com.cafe.be.api.userservice.repo;

import com.cafe.be.api.userservice.entity.Users;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UsersRepository extends JpaRepository<Users, Long> {
}
QueryDsl Config
package com.cafe.be.api.common.config;

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 QuerydslConfig{

    private final EntityManager em;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(em);
    }
}
사용자 QueryDsl
package com.cafe.be.api.userservice.querydsl;

import com.cafe.be.api.userservice.dto.QUsersResponse;
import com.cafe.be.api.userservice.dto.UsersResponse;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import static com.cafe.be.api.userservice.entity.QUsers.users;

@Repository
@RequiredArgsConstructor
public class UserQueryDsl {

    private final JPAQueryFactory queryFactory;

    public UsersResponse findByUserNo(Long userNo){
        UsersResponse usersResponse = queryFactory
                .select(
                        new QUsersResponse(
                                users.userNo,
                                users.email,
                                users.pw,
                                users.nick,
                                users.status,
                                users.role,
                                users.birth,
                                users.agree1,
                                users.agree2,
                                users.agree3
                        )
                ).from(users)
                .where(
                        users.userNo.eq(userNo)
                ).fetchFirst();

        return usersResponse;
    }

}
사용자 DTO
package com.cafe.be.api.userservice.dto;

import com.cafe.be.api.userservice.entity.Users;
import com.querydsl.core.annotations.QueryProjection;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@Builder
public class UsersResponse {

    private Long userNo;

    // 이메일
    private String email;

    // 비밀번호
    private String pw;

    // 닉네임
    private String nick;

    // 사용자 상태
    private String status;

    // 권한
    private String role;

    // 생년월일
    private String birth;

    // 이용약관 동의
    private String agree1;

    // 개인정보 수집 및 이용 동의
    private String agree2;

    // 이벤트, 프로모션, 메일, sms 수신
    private String agree3;

    @QueryProjection
    public UsersResponse(Long userNo, String email, String pw, String nick, String status, String role, String birth, String agree1, String agree2, String agree3) {
        this.userNo = userNo;
        this.email = email;
        this.pw = pw;
        this.nick = nick;
        this.status = status;
        this.role = role;
        this.birth = birth;
        this.agree1 = agree1;
        this.agree2 = agree2;
        this.agree3 = agree3;
    }


    public static UsersResponse UsersToUsersResponse(Users u){
        UsersResponse usersResponse = UsersResponse.builder()
                .userNo(u.getUserNo())
                .email(u.getEmail())
                .pw(u.getPw())
                .nick(u.getNick())
                .status(u.getStatus())
                .role(u.getRole())
                .birth(u.getBirth())
                .agree1(u.getAgree1())
                .agree2(u.getAgree2())
                .agree3(u.getAgree3())
                .build();

        return usersResponse;

    }

}
사용자 Controller
package com.cafe.be.api.userservice.controller;

import com.cafe.be.api.common.dto.CommonResponse;
import com.cafe.be.api.userservice.dto.UsersResponse;
import com.cafe.be.api.userservice.entity.Users;
import com.cafe.be.api.userservice.repo.UsersRepository;
import com.cafe.be.api.userservice.service.UsersService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UsersController {

    private final UsersService usersService;

    @GetMapping("/{userNo}")
    public CommonResponse<UsersResponse> detail(@PathVariable("userNo") Long userNo){

        return usersService.detail(userNo);

    }

}
사용자 Service
package com.cafe.be.api.userservice.service;

import com.cafe.be.api.common.dto.CommonResponse;
import com.cafe.be.api.common.enums.ResponseCode;
import com.cafe.be.api.common.exception.ApiException;
import com.cafe.be.api.userservice.dto.UsersResponse;
import com.cafe.be.api.userservice.entity.Users;
import com.cafe.be.api.userservice.querydsl.UserQueryDsl;
import com.cafe.be.api.userservice.repo.UsersRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@RequiredArgsConstructor
public class UsersService {

    private final UsersRepository usersRepository;

    private final UserQueryDsl userQueryDsl;

    public CommonResponse<UsersResponse> detail(Long userNo) {

        log.debug("[UsersService] [detail] request >>> {}", userNo);

        CommonResponse<UsersResponse> result = new CommonResponse<>();

        try{
//            Users users = usersRepository.findById(userNo)
//                    .orElseThrow(() -> new ApiException(ResponseCode.USER_NOT_FOUND));
//
//            UsersResponse usersResponse = UsersResponse.UsersToUsersResponse(users);


            UsersResponse usersResponse = userQueryDsl.findByUserNo(userNo);

            result.setResult(usersResponse);
            result.setCode(ResponseCode.SUCCESS.getCode());
            result.setMsg(ResponseCode.SUCCESS.getMsg());

        }catch (ApiException e){
            result.setCode(e.getCode());
            result.setMsg(e.getMsg());
        }

        return result;
    }
}
반응형

'개발 > JPA' 카테고리의 다른 글

JPA-OSIV(Open Session In View)  (0) 2023.10.14
[JPA] could not initialize proxy - no Session  (0) 2023.09.10
[JPA] GeneratedValue  (0) 2023.09.10
[JPA] Auditing - 공통 도메인 작업  (0) 2023.09.10
[JPA] @MappedSuperclass  (0) 2023.09.10

+ Recent posts