관리 메뉴

피터의 개발이야기

[Kotlin] Spring Boot와 Kotlin으로 QueryDSL 페이징 처리하기 본문

Programming/Kotlin

[Kotlin] Spring Boot와 Kotlin으로 QueryDSL 페이징 처리하기

기록하는 백앤드개발자 2024. 8. 4. 16:10
반응형

ㅁ 들어가며

  지난 글, [Spring] Kotlin으로 JPA Querydsl 세팅에서 Spring Boot와 Kotlin을 사용하여 QueryDSL을 적용한 프로젝트를 구성하였다. QueryDSL을 사용하면 동적 쿼리를 쉽게 작성할 수 있으며, Spring Data의 페이징 기능을 활용하면 대량의 데이터를 효율적으로 처리할 수 있다. 이번 글에서는 Spring Boot와 Kotlin을 사용하여 QueryDSL을 적용한 프로젝트에서 페이징 처리를 구현하는 방법을 정리하였다.

 

ㅁ Repository 인터페이스 생성

import org.springframework.data.domain.*
import org.springframework.data.jpa.repository.JpaRepository

interface ProductRepository : JpaRepository<Product, Long>, ProductRepositoryCustom


interface ProductRepositoryCustom {
    fun findProductsByName(name: String, pageable: Pageable): Page<Product>
}

ㅇ ProductRepository.kt에 Product를 name으로 검색하는 인터페이스 생성
ㅇ ProductRepositoryCustom은 JPA 기본 기능에 대한 override를 피하기 위해 만들었다.

 

import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.data.domain.*
import org.springframework.stereotype.Repository

@Repository
class ProductRepositoryImpl(
    private val jpaQueryFactory: JPAQueryFactory
): ProductRepositoryCustom {

    override fun findProductsByName(name: String, pageable: Pageable): Page<Product> {
        val qProduct = QProduct.product

        val query = jpaQueryFactory.selectFrom(qProduct)
            .where(qProduct.name.containsIgnoreCase(name))
            .offset(pageable.offset)
            .limit(pageable.pageSize.toLong())

        val products = query.fetch()
        val countQuery = jpaQueryFactory.selectFrom(qProduct)
            .where(qProduct.name.containsIgnoreCase(name))

        return PageImpl(products, pageable, countQuery.fetchCount())
    }
}

ㅇ ProductRepository.kt에 Product를 name으로 검색하는 기능을 구현하였다.

 

ㅁ 서비스 클래스 구현

import com.peterica.kotlinquerydsl.entity.Product
import com.peterica.kotlinquerydsl.entity.ProductRepository
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service

@Service
class ProductService(
    private val productRepository: ProductRepository
) {
    fun saveProduct(product: Product): Product =
        productRepository.save(product)

    fun findProductById(id: Long): Product? =
        productRepository.findById(id).orElse(null)

    fun searchProductsByName(name: String, pageable: Pageable): Page<Product> =
        productRepository.findProductsByName(name, pageable)
}

ㅇ ProductService.kt 구현.

 

ㅁ 컨트롤러 구현

package com.peterica.kotlinquerydsl.controller

import com.peterica.kotlinquerydsl.entity.Product
import com.peterica.kotlinquerydsl.service.ProductService
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.*


@Controller
@RequestMapping("/products")
class ProductController(
    private val productService: ProductService
) {
    
    @PostMapping
    fun createProduct(@RequestBody product: Product): Product {
        return productService.saveProduct(product)
    }
    
    @GetMapping("/{id}")
    fun getProduct(@PathVariable id: Long): Product? {
        return productService.findProductById(id)
    }
    
    @GetMapping("/search")
    fun searchProductsByName(@RequestParam name: String, pageable: Pageable): Page<Product> {
        return productService.searchProductsByName(name, pageable)
    }
}

 

ㅁ Circular view path 에러 트러블 슈팅

jakarta.servlet.ServletException: Circular view path [products]: would dispatch back to the current handler URL [/products] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)

ㅇ Circular view path 에러가 발생하여 해결방법을 [Kotlin] Spring MVC, Circular view path 에러에 정리하였다.

ㅁ 테스트 코드 작성

import com.fasterxml.jackson.databind.ObjectMapper
import com.peterica.kotlinquerydsl.controller.ProductController
import com.peterica.kotlinquerydsl.entity.Product
import com.peterica.kotlinquerydsl.service.ProductService
import org.junit.jupiter.api.Test
import org.mockito.Mockito.`when`
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.data.domain.PageImpl
import org.springframework.data.domain.PageRequest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import java.time.LocalDateTime

@WebMvcTest(ProductController::class)
class ProductControllerTest {
    @Autowired
    private lateinit var mockMvc: MockMvc
    @MockBean
    private lateinit var productService: ProductService
    @Autowired
    private lateinit var objectMapper: ObjectMapper

    @Test
    fun `create product`() {
        val product = Product(name = "Test Product", quantity = 2, registeredAt = LocalDateTime.now())
        val savedProduct = product.copy(id = 1)

        `when`(productService.saveProduct(product)).thenReturn(savedProduct)

        mockMvc.perform(post("/products")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(product)))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.name").value("Test Product"))
            .andExpect(jsonPath("$.quantity").value(2))
    }

    @Test
    fun `get product by id`() {
        val product = Product(id = 1, name = "Test Product", quantity = 2, registeredAt = LocalDateTime.now())

        `when`(productService.findProductById(1)).thenReturn(product)

        mockMvc.perform(get("/products/1"))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.name").value("Test Product"))
            .andExpect(jsonPath("$.quantity").value(2))
    }

    @Test
    fun `return 404 when product not found`() {
        `when`(productService.findProductById(1)).thenReturn(null)

        mockMvc.perform(get("/products/1"))
            .andExpect(status().isNotFound)
    }

    @Test
    fun `search products`() {
        val products = listOf(
            Product(id = 1, name = "Test Product 1", quantity = 1, registeredAt = LocalDateTime.now()),
            Product(id = 2, name = "Test Product 2", quantity = 2, registeredAt = LocalDateTime.now())
        )
        val pageable = PageRequest.of(0, 10)
        val page = PageImpl(products, pageable, 2)

        `when`(productService.searchProductsByName("Test", pageable)).thenReturn(page)

        mockMvc.perform(get("/products/search")
            .param("name", "Test")
            .param("page", "0")
            .param("size", "10"))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.content.length()").value(2))
            .andExpect(jsonPath("$.content[0].id").value(1))
            .andExpect(jsonPath("$.content[0].name").value("Test Product 1"))
            .andExpect(jsonPath("$.content[1].id").value(2))
            .andExpect(jsonPath("$.content[1].name").value("Test Product 2"))
            .andExpect(jsonPath("$.totalElements").value(2))
            .andExpect(jsonPath("$.totalPages").value(1))
    }
}

 

ㅁ PostMan 테스트

createProduct

curl --location 'localhost:8080/products' \
--header 'Content-Type: application/json' \
--data '{
	"name": "Peterica",
    "quantity": 1,
    "registeredAt": "2024-08-04T14:22:33"
    
}'

 

getProduct

curl --location 'localhost:8080/products/1'
{
  "name": "Peterica",
  "quantity": 1,
  "registeredAt": "2024-08-04T14:22:33",
  "id": 1
}
curl --location 'localhost:8080/products/1'{ "name": "Peterica", "quantity": 1, "registeredAt": "2024-08-04T14:22:33", "id": 1}

 

searchProductsByName

curl --location 'localhost:8080/products/search?name=peterica&page=0&size=10'

{
  "content": [
    {
      "name": "Peterica",
      "quantity": 1,
      "registeredAt": "2024-08-04T14:22:33",
      "id": 1
    }
  ],
  "pageable": {
    "pageNumber": 0,
    "pageSize": 10,
    "sort": {
      "sorted": false,
      "unsorted": true,
      "empty": true
    },
    "offset": 0,
    "paged": true,
    "unpaged": false
  },
  "totalPages": 1,
  "totalElements": 1,
  "last": true,
  "numberOfElements": 1,
  "size": 10,
  "number": 0,
  "first": true,
  "sort": {
    "sorted": false,
    "unsorted": true,
    "empty": true
  },
  "empty": false
}

 

ㅁ 함께 보면 좋은 사이트

kotlin + spring data jpa로 pageable을 사용해보자

Spring Boot Rest API with Kotlin - Pagination, Search & Sorting using MongoDB

반응형
Comments