꿈틀꿈틀 개발일기

QueryDSL | 활용하기

by jeongminy

1. build.gradle.kts에 의존성 추가

여기서 추가하는 kapt는 Kotlin Annotation Processing Tool 입니다.

Annotation들을 분석하여 QueryDSL에 알려줘, QClass 들을 사용할 수 있게 만드는 역할을 합니다.

plugins {
    kotlin("kapt") version "1.8.22" // 추가!
}

val queryDslVersion = "5.0.0" // 추가! (queryDsl 버전을 설정)

dependencies{
    implementation("com.querydsl:querydsl-jpa:$queryDslVersion:jakarta") // 추가!
    kapt("com.querydsl:querydsl-apt:$queryDslVersion:jakarta") // 추가!
}

 

2. Qclass 생성

기존에 만들어 져 있는 Entity를 기반으로 Qclass가 생성된 걸 볼 수 있다.

단, 변수 자체가 아닌 Path 가 추가되어 경로로 설정되어 있는데 Path를 기반으로 최종적으로 SQL을 만들어 낸다는 점을 알 수 있다.

3. QueryFactory 생성

QueryDsl도 마찬가지로 DB를 조회하므로, Repository의 역할을 한다고 볼 수 있다.

모든 Repository에서 사용할 테니 다른데서도 공통으로 사용할 수 있는 QueryFactory 를 만들어 준다.

//이 클래스를 상속받은 구체적인 클래스에서는
//queryFactory를 사용하여 QueryDSL을 활용하여 SQL 쿼리를 작성하고 실행할 수 있습니다.
abstract class QueryDslSupport { //해당 클래스를 직접 사용할게 아니므로 abstract 추상클래스로 만들어줌

    @PersistenceContext
    //해당 어노테이션을 사용하여 EntityManager를 주입받습니다.
    //EntityManager는 JPA(Java Persistence API)를 사용하여 
    //데이터베이스와의 상호 작용을 담당하는 중요한 컴포넌트입니다.
    protected lateinit var entityManager: EntityManager //protected를 줌으로써, 상속받는 애들이 사용할 수 있게 함.


    //JPAQueryFactory는 QueryDSL에서 제공하는 쿼리 작성을 위한 핵심 클래스로,
    //EntityManager를 기반으로 쿼리를 생성하고 실행할 수 있습니다.
    protected val queryFactory: JPAQueryFactory //protected를 줌으로써, 상속받는 애들이 사용할 수 있게 함.
        get() {
            return JPAQueryFactory(entityManager)
        }
}

 

QueryDslCourseRepository 생성

@Repository //외부기능을 활용하다 보니 Repository 어노테이션을 붙여 주어야 함. 그래야 spring Bean으로서 동작함.
class QueryDslCourseRepository: QueryDslSupport() {

    private val course = QCourse.course //course 에 대한 QClass를 멤버 변수로 선언 해준다.

    //title 기준으로 course를 조회함
    fun searchCourseListByTitle(title: String): List<Course>{
        return queryFactory.selectFrom(course)
            .where(course.title.containsIgnoreCase(title)) //containsIgnoreCase 는 대소문자 구분 하지 않고 검색해 줘서 좋음.
            .fetch()
    }
}

 

4. 제목 기준으로 목록 조회 기능 구현하기

Controller 작성

@RequestMapping("/courses")
@RestController
class CourseController(
    private val courseService: CourseService
) {

    @GetMapping("/search")
    @PreAuthorize("hasRole('TUTOR') or hasRole('STUDENT')")
    fun searchCourseList(@RequestParam(value = "title") title: String): ResponseEntity<List<CourseResponse>> {
        return ResponseEntity
            .status(HttpStatus.OK)
            .body(courseService.searchCourseList(title))
    }
    
    //이하 생략
}

 

service 추가

interface CourseService {
	//생략
	fun searchCourseList(title: String): List<CourseResponse>?
	//생략
}

service 인터페이스에 메소드를 추가하고,

serviceImpl 구현부에 가서 queryDslCourseRepository를 생성자 주입하고,

getAllCourseList 를 구현한다.

@Service
class CourseServiceImpl(
    private val courseRepository: CourseRepository,
    private val queryDslCourseRepository: QueryDslCourseRepository, //주입
    private val lectureRepository: LectureRepository,
    private val courseApplicationRepository: CourseApplicationRepository,
    private val userRepository: UserRepository,
) : CourseService {

    override fun getAllCourseList(): List<CourseResponse> {
        return courseRepository.findAll().map { it.toResponse() }
    }
//생략
}

 

5. Repository 구조 변경

단! 여기서

    private val courseRepository: CourseRepository,
    private val queryDslCourseRepository: QueryDslCourseRepository

repository를 두개나 종속성에 추가해주어야해서 내심 찝찝하다...!
둘 다 결국 DB와 연결되어 쿼리를 수행하는 역할인데,

어떤 방식으로 쿼리를 작성하느냐에 따라 저렇게 나뉘어 버린 것이다.

이 부분을 해결하기 위해 아래처럼 구성하면 된다!

따라서, CustomCourseRepository를 만들어 준 후,

CourseRepository에 CustomCourseRepository 를 상속해준다.

Repository의 패키지 구조가 이렇게 될 것이다.

 

6. Pageable 인터페이스

이 인터페이스는 pagination을 지원하기 위한 메서드와 속성을 제공합니다.

해당 인터페이스를 구현하는 클래스는 페이지네이션 기능을 활용하여 데이터를 조회하고, 정렬하고, 페이지를 관리할 수 있습니다. 이를 통해 효율적인 데이터 검색페이지네이션을 구현할 수 있습니다.

public interface Pageable {
	//페이지네이션 없이 모든 결과를 반환하는 Pageable 객체를 생성하여 반환합니다.
	static Pageable unpaged() {
		return Unpaged.INSTANCE;
	}

	//주어진 페이지 크기로 초기화된 첫 번째 페이지를 나타내는 Pageable 객체를 생성하여 반환합니다.
	static Pageable ofSize(int pageSize) {
		return PageRequest.of(0, pageSize);
	}

	//현재 Pageable이 페이지네이션된 상태인지 여부를 반환합니다.
	default boolean isPaged() {
		return true;
	}

	//현재 Pageable이 페이지네이션되지 않은 상태인지 여부를 반환합니다.
	default boolean isUnpaged() {
		return !isPaged();
	}

	//현재 페이지 번호를 반환합니다.
	int getPageNumber();

	//페이지 크기를 반환합니다.
	int getPageSize();

	//현재 페이지의 오프셋(시작 인덱스)을 반환합니다.
	long getOffset();

	//페이지 결과를 정렬하는 데 사용되는 Sort 객체를 반환합니다.
	Sort getSort();

	//페이지 결과를 정렬하는 데 사용되는 Sort 객체를 반환합니다. 
      //만약 현재 Sort가 정렬되지 않은 상태라면, 주어진 fallback Sort를 반환합니다.
	default Sort getSortOr(Sort sort) {

		Assert.notNull(sort, "Fallback Sort must not be null");

		return getSort().isSorted() ? getSort() : sort;
	}

	//다음 페이지를 나타내는 새로운 Pageable 객체를 반환합니다.
	Pageable next();

	//이전 페이지를 나타내는 새로운 Pageable 객체를 반환합니다. 
      //이전 페이지가 없을 경우 첫 번째 페이지를 반환합니다.
	Pageable previousOrFirst();

	//첫 번째 페이지를 나타내는 새로운 Pageable 객체를 반환합니다.
	Pageable first();

	//주어진 페이지 번호로 초기화된 새로운 Pageable 객체를 반환합니다.
	Pageable withPage(int pageNumber);

	//이전 페이지가 있는지 여부를 반환합니다.
	boolean hasPrevious();
	
    //현재 Pageable 객체를 Optional로 감싸서 반환합니다. 
    //페이지네이션이 되지 않은 경우 Optional.empty()를 반환합니다.
	default Optional<Pageable> toOptional() {
		return isUnpaged() ? Optional.empty() : Optional.of(this);
	}
	
    //현재 Pageable 객체를 OffsetScrollPosition으로 변환하여 반환합니다. 
    //페이지네이션이 되지 않은 경우 예외가 발생합니다.
	default OffsetScrollPosition toScrollPosition() {

		if (isUnpaged()) {
			throw new IllegalStateException("Cannot create OffsetScrollPosition from an unpaged instance");
		}

		return ScrollPosition.offset(getOffset());
	}
}

 

이제 controller에서 pageable을 추가해주기만 해도, 이 안에서 알아서 page, size sort 가 자동으로 붙는다.

swagger를 확인해 보면 눈으로 확인 가능ㅎ

default값을 내가 원하는 대로 설정할 수 도 있다.

 

여기서, 반환할 때 현재 List 인 것을 볼 수 있다.

하지만 예를 들어, front에서 page를 표기할 때,

마지막 페이지가 몇인지에 대한 정보 등을 전해 주어야 front단 에서 페이지를 구성할 수 있을 것이다.

그러므로 이러한 정보들을 같이 주는게 좋다.

따라서 List로 반환 하는 것이 아니라 Page로 반환 하는 것이 좋아!

 

7. Page 인터페이스

Page<T> 인터페이스는 페이징된 데이터를 표현하고, 페이지 정보와 요소에 대한 다양한 작업을 수행할 수 있는 메서드들을 제공합니다. 구체적인 구현은 PageImpl<T> 클래스에서 이루어지며, 이 인터페이스를 구현하는 다른 클래스에서는 해당 메서드들을 구현해야 합니다.

public interface Page<T> extends Slice<T> {

	//빈 페이지(Page<T>)를 반환하는 정적 메서드입니다. 
    //Pageable.unpaged()를 사용하여 빈 페이지를 생성합니다.
	static <T> Page<T> empty() {
		return empty(Pageable.unpaged());
	}

	//주어진 pageable을 사용하여 빈 페이지(Page<T>)를 반환하는 정적 메서드입니다. 
    //Collections.emptyList()를 사용하여 빈 요소 리스트를 생성하고, 
    //이를 pageable과 함께 PageImpl<T> 객체로 감싸서 반환합니다.
	static <T> Page<T> empty(Pageable pageable) {
		return new PageImpl<>(Collections.emptyList(), pageable, 0);
	}

	//총 페이지 수를 반환하는 메서드입니다.
	int getTotalPages();

	//총 요소 수를 반환하는 메서드입니다.
	long getTotalElements();

	//요소 변환기(converter)를 사용하여 
    //현재 페이지의 요소를 다른 타입(U)으로 변환한 새로운 페이지(Page<U>)를 반환하는 메서드입니다.
	<U> Page<U> map(Function<? super T, ? extends U> converter);
}

 

 

8. CourseStatus 조건에 따라 조회 기능 구현하기.

CourseController

@RequestMapping("/courses")
@RestController
class CourseController(
    private val courseService: CourseService
) {
	@GetMapping
    @PreAuthorize("hasRole('TUTOR') or hasRole('STUDENT')")
    fun getCourseList(
        @PageableDefault(
            size=15,
            sort = ["id"]
        ) pageable:Pageable,
        @RequestParam(value = "status", required = false) status: String?
    ): ResponseEntity<Page<CourseResponse>> {
    
        return ResponseEntity
            .status(HttpStatus.OK)
            .body(courseService.getPaginatedCourseList(pageable, status))
    }
    //이하 생략
}

CourseServiceImpl

@Service
class CourseServiceImpl(
    private val courseRepository: CourseRepository,
    private val lectureRepository: LectureRepository,
    private val courseApplicationRepository: CourseApplicationRepository,
    private val userRepository: UserRepository,
) : CourseService {

    override fun getPaginatedCourseList(pageable: Pageable, status: String?): Page<CourseResponse>? {
        val courseStatus = when (status){
            "OPEN" -> CourseStatus.OPEN
            "CLOSED" -> CourseStatus.CLOSED
            null -> null
            else -> throw IllegalArgumentException("The status is invalied")
        }
        return courseRepository.findByPageableAndStatus(pageable, courseStatus).map {it.toResponse()}
    }

CourseRepositoryImpl

    override fun findByPageableAndStatus(pageable: Pageable, courseStatus: CourseStatus?): Page<Course> {

        val whereClause = BooleanBuilder()
        courseStatus?.let {whereClause.and(course.status.eq(courseStatus))}

        val totalCount = queryFactory.select(course.count()).from(course).where(whereClause).fetchOne() ?: 0L

        val query = queryFactory.selectFrom(course)
            .where(whereClause)
            .offset(pageable.offset)
            .limit(pageable.pageSize.toLong())

        if (pageable.sort.isSorted) { //정렬 조건이 있는지 학인하고
            when(pageable.sort.first()?.property) { //만약에 정렬 조건이 하나만 있을때, 그 조건이
                "id" -> query.orderBy(course.id.asc()) //id라면 
                "title" -> query.orderBy(course.title.asc()) //title이라면
                else -> query.orderBy(course.id.asc()) //이 외에는 courseId로 정렬한다.
            }
        } else { //만약에 정렬 조건이 없다면
            query.orderBy(course.id.asc()) //courseId로 정렬한다.
        }

        val contents = query.fetch()

        return PageImpl(contents, pageable, totalCount)
    }

 

 

 

 

 

 

'📖 Study > JPA, QueryDSL' 카테고리의 다른 글

JPA, QueryDSL | 심화  (0) 2024.02.08

블로그의 정보

꿈틀꿈틀 개발일기

jeongminy

활동하기