Query 메서드 리팩터링
지난 토요일 중간발표를 통해 나의 현 위치를 인지할 수 있었다. 어찌어찌 기능 구현은 가능하지만 그 기능이 어떻게 구현된 것인지, 정확히 어떤 기술들을 사용하여 기능을 구현한 것인지에 대한 명확한 설명이 불가하였다. 특히 기존에 작성한 코드의 경우 JPA, JPQL, Native Query 등을 무분별하게 혼재하여 사용하였는데 이렇게 했을 때 어떠한 문제가 발생하는지, 어떠한 이점이 있는지 조차 모르고 있었다.
JPA, JPQL은 지난 WIL을 통해 정리해두었다.
다행히 중간발표를 거치며 나의 무지를 깨달을 수 있었고 이번 기회에 기존 작성하였던 Query 메서드들을 검토하고 리팩터링 하는 시간을 가져보고자 한다.
그간 무지하여 NativeQuery가 무조건 좋다고 알고 있었지만 지난 개념 정리를 바탕으로 JPA와 JPQL을 사용하였을 때의 이점을 파악할 수 있었다. JPA와 JPQL로 Query메서드를 작성할 경우 DB 의존적이지 않기에 유지보수에 있어서 훨씬 쉽고 용이하다는 큰 강점이 있었다.
이러한 개념을 토대로 전주까지 무분별하게 사용하였던 Query 메서드들을 전부 JPQL로 변경해주었다.
하지만 JPQL로 해결하지 못했던 쿼리 메서드가 남아있었다. 바로 다음 카테고리에 의한 조회 메서드이다.
메인페이지 상품 리스트 조회 메서드 리팩터링(Querydsl 적용 동적 쿼리 사용)
ItemRepository - 상품 리스트 조회 메서드들(QueryDSL 도입 전)
/** 전체 상품 리스트 조회 */
Page<Item> findAll(Pageable pageable);
/** 이중 카테고리에 의한 조회 */
@Query("select i from Item i " +
"where i.petCategory = :petCategory and i.itemCategory = :itemCategory ")
Page<Item> getAllItemListByTwoCategory(String petCategory, String itemCategory, Pageable pageable);
/** 단일 카테고리에 의한 조회 - petCategory */
@Query("select i from Item i " +
"where i.petCategory = :petCategory ")
Page<Item> getAllItemListByPetCategry(String petCategory, Pageable pageable);
/** 단일 카테고리에 의한 조회 - itemCategory */
@Query("select i from Item i " +
"where i.itemCategory = :itemCategory ")
Page<Item> getAllItemListByItemCategory(String itemCategory, Pageable pageable);
/** 상품 리스트 호출 시 LastData(마지막 게시글)인지 확인 */
@Query("select min(i.id) from Item i") /* 전체 상품 조회 */
int lastData();
@Query("select min(i.id) from Item i where i.petCategory = :petCategory") /* 펫 카테고리 */
int lastDataPetCategory(String petCategory);
@Query("select min(i.id) from Item i where i.itemCategory = :itemCategory") /* 아이템 카테고리 */
int lastDataItemCategory(String itemCategory);
@Query("select min(i.id) from Item i where i.petCategory = :petCategory and i.itemCategory = :itemCategory")
int lastDataTwoCategory(String petCategory, String itemCategory); /* 이중 카테고리 */
기존에 작성하였던 상품 리스트 조회 메서드들이다. 정말로 전부 필요한 메서드이긴 하나 중복되는 부분들이 너무 많았다,
상품의 리스트를 가져온다는 부분들은 동일했지만 전체 조회, 펫 카테고리에 의한 조회, 상품 카테고리에 의한 조회, 두 경우 모두까지 총 4개의 유사한 코드가 발생한 것이다. 물론 이것은 레파지토리 단에서만 봤을 때 이 만큼이었을 뿐.
Service, Controller 단 까지 합치면 반복되는 코드가 너무나 많았다.
ItemService - 상품 리스트 조회 메서드들(QueryDSL 도입 전)
/** 전체 상품 조회 메서드(MainPage) */
@Transactional(readOnly = true)
public List<ItemMainResponseDto> getAllItem(Pageable pageable) {
Page<Item> itemList = itemRepository.findAll(pageable);
List<ItemMainResponseDto> itemMainResponseDtoList = new ArrayList<>();
int lastData = itemRepository.lastData(); /* FE 무한스크롤 위한 마지막 게시글 확인 */
for (Item item : itemList) {
itemMainResponseDtoList.add(
buildItemMainResponseDto(lastData, item)
);
}
return itemMainResponseDtoList;
}
/** 카테고리에 따른 상품 조회 메서드(MainPage/단일 카테고리 - itemCategory) */
public List<ItemMainResponseDto> getItemByItemCategory(String itemCategory, Pageable pageable) {
validator.validateItemCategory(itemCategory); /* 상품 카테고리 유효성 검사 */
Page<Item> itemList = itemRepository.getAllItemListByItemCategory(itemCategory, pageable);
List<ItemMainResponseDto> itemMainResponseDtoList = new ArrayList<>();
int lastData = itemRepository.lastDataItemCategory(itemCategory); /* FE 무한스크롤 위한 마지막 게시글 확인 */
for (Item item : itemList) {
itemMainResponseDtoList.add(
buildItemMainResponseDto(lastData, item)
);
}
return itemMainResponseDtoList;
}
/** 카테고리에 따른 상품 조회 메서드(MainPage/단일 카테고리 - petCategory) */
public List<ItemMainResponseDto> getItemByPetCategory(String petCategory, Pageable pageable) {
validator.validatePetCategory(petCategory); /* 펫 카테고리 유효성 검사 */
Page<Item> itemList = itemRepository.getAllItemListByPetCategry(itemCategory, pageable);
List<ItemMainResponseDto> itemMainResponseDtoList = new ArrayList<>();
int lastData = itemRepository.lastDataPetCategory(petCategory); /* FE 무한스크롤 위한 마지막 게시글 확인 */
for (Item item : itemList) {
itemMainResponseDtoList.add(
buildItemMainResponseDto(lastData, item)
);
}
return itemMainResponseDtoList;
}
/** 카테고리에 따른 상품 조회 메서드(MainPage/이중 카테고리) */
@Transactional(readOnly = true)
public List<ItemMainResponseDto> getItemByTwoCategory(String petCategory, String itemCategory, Pageable pageable) {
validator.validateItemCategory(itemCategory); /* 상품 카테고리 유효성 검사 */
validator.validatePetCategory(petCategory); /* 펫 카테고리 유효성 검사 */
Page<Item> itemList = itemRepository.getAllItemListByTwoCategory(petCategory, itemCategory, pageable);
List<ItemMainResponseDto> itemMainResponseDtoList = new ArrayList<>();
int lastData = itemRepository.lastDataTwoCategory(petCategory, itemCategory); /* FE 무한스크롤 위한 마지막 게시글 확인 */
for (Item item : itemList) {
itemMainResponseDtoList.add(
buildItemMainResponseDto(lastData, item)
);
}
return itemMainResponseDtoList;
}
ItemController - 상품 리스트 조회 메서드들(QueryDSL 도입 전)
/* 전체 상품 조회(MainPage) */
@ApiOperation(value = "전체 상품 조회 메소드")
@GetMapping("items")
public ResponseEntity<?> getAllItem(@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
return ResponseEntity.ok().body(itemService.getAllItem(pageable));
}
/* 카테고리에 따른 상품 조회(MainPage/단일 카테고리 - petCategory) */
@ApiOperation(value = "pet_category에 따른 상품 조회 메소드")
@GetMapping("items/petcategory")
public ResponseEntity<?> getItemByPetCategory(@RequestParam("petCategory") String petCategory,
@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
return ResponseEntity.ok().body(itemService.getItemByPetCategory(petCategory, pageable));
}
/* 카테고리에 따른 상품 조회(MainPage/단일 카테고리 - itemCategory) */
@ApiOperation(value = "item_category에 따른 상품 조회 메소드")
@GetMapping("items/itemcategory")
public ResponseEntity<?> getItemByItemCategory(@RequestParam("itemCategory") String itemCategory,
@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
return ResponseEntity.ok().body(itemService.getItemByItemCategory(itemCategory, pageable));
}
/* 카테고리에 따른 상품 조회(MainPage/이중 카테고리) */
@ApiOperation(value = "카테고리를 2중(pet_category + item_category)으로 반영한 상품 조회 메소드")
@GetMapping("items/twocategory")
public ResponseEntity<?> getItemByTwoCategory(@RequestParam("petCategory") String petCategory, @RequestParam("itemCategory") String itemCategory,
@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
return ResponseEntity.ok().body(itemService.getItemByTwoCategory(petCategory, itemCategory, pageable));
}
다른 조들의 중간발표를 보면서 이러한 중복된 메서드들을 동적쿼리를 이용하여 하나의 메서드로 묶어줄 수 있다는 사실에 대해서 알게 되었다. 동적 쿼리란 파라미터 값이 유동적일 수 있는 쿼리 메서드이다. 이를 기존 코드에 적용시킨다면 기존 4개의 조회 메서드를 하나로 묶어 파라미터 값이 없을 때는 전체 조회, petCategory만 파라미터 값으로 들어오면 petCategory에 의한 조회, itemCategory만 들어오면 itemCategory에 의한 조회, 둘 다 들어오면 이중 카테고리에 의한 조회로 처리가 가능하였다.
이러한 동적쿼리는 지난번 미리 조사해두었던 Querydsl로 해결 가능하였기에 이번 기회에 적용시켜 보기로 하였다.
Querydsl 적용
QueryDSL을 사용하기 위해서는 밑 작업이 필요하였다. 우선 build.gradle에 필요 코드를 추가해주었다.
build.gradle
buildscript { /* queryDSL */
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'org.springframework.boot' version '2.7.3'
id 'io.spring.dependency-management' version '1.0.13.RELEASE'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" /* queryDSL */
id 'java'
}
...
dependencies {
...
/* queryDSL */
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}"
...
}
/* 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
}
tasks.named('test') {
useJUnitPlatform()
}
build.gradle 설정을 마치고는
Gradle - Task - other - complieQuerydsl을 실행하면
프로젝트 파일 내 build - generated - querydsl 내 다음과 같이 파일들이 생성된 것이 확인 가능해진다.
다음으로 QueryDSL을 사용하여 쿼리를 build 위해서는 JPAQeuryFactory가 필요하기에 QuerydslConfig class를 생성하여주었다.
QuerydslConfig
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() { /* JPAQueryFactory를 사용하면 EntityManager를 통해 질의가 처리되고,JPQL을 생성하여 처리 */
return new JPAQueryFactory(entityManager); /* EntityManager는 영속성 컨텍스트에 접근하여 Entity에 대한 DB 작업을 제공하고 관리한다 */
} /* 영속성 컨텍스트란 Entity를 영구적으로 저장하는 환경을 뜻한다 */
} /* 어플리케이션과 DB사이의 객체를 저장하는 가상의 DB 역할 수행 */
그리고 대망의 동적 쿼리 메서드를 작성해보았다.
ItemQuerydslRepository
@Repository
@RequiredArgsConstructor
public class ItemQuerydslRepository {
private final JPAQueryFactory jpaQueryFactory;
/**
* 상품 리스트 조회 동적 쿼리 메서드(전체 상품 조회 및 카테고리에 의한 상품 조회 메서드 통합)
*/
public Page<Item> getItemListByCategory(String petCategory, String itemCategory, Pageable pageable) {
/* content - 조건에 맞는 itemList 반환 */
List<Item> itemList = jpaQueryFactory.selectFrom(item)
.where(
petCategoryEq(petCategory),
itemCategoryEq(itemCategory))
.orderBy(item.id.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
/* totalCount - 조건에 맞는 itemList를 count */
long total = jpaQueryFactory.select(Wildcard.count).from(item)
.where(
petCategoryEq(petCategory),
itemCategoryEq(itemCategory))
.fetchFirst();
/* PageImpl -> Page 인터페이스의 구현체, 인자로 1. content 2. pageable 3. totalCount(content 수)를 받음 */
return new PageImpl<>(itemList, pageable, total);
}
/**
* 상품 조회 시 해당 상품이 마지막 상품인지 조회하는 메서드(무한 스크롤 구현 위함)
*/
public Long getLastData(String petCategory, String itemCategory) {
return jpaQueryFactory.select(item.id.min())
.from(item)
.where(
petCategoryEq(petCategory),
itemCategoryEq(itemCategory))
.orderBy(item.id.desc())
.fetchFirst();
}
/* BooleanExpression -> where 절에 필요한 조건식 반환
* null 반환 시 조건(where)절에서 조건이 무시(제거)되어 안전 / 전부 null이면 전체 값 호출 */
private BooleanExpression petCategoryEq(String petCategory) {
return petCategory != null ? item.petCategory.eq(petCategory) : null;
}
private BooleanExpression itemCategoryEq(String itemCateogry) {
return itemCateogry != null ? item.itemCategory.eq(itemCateogry) : null;
}
}
상품 리스트 조회를 위한 Repository 단의 코드가 살짝 길어지긴 하였으나 직접 연결된 메서드 수 자체는 기존 8개에서 2개로 대폭 감소시킬 수 있었다. Service와 Controller 단의 코드도 각 1개의 메서도 일축시킬 수 있었다.
ItemService - 상품 리스트 조회 메서드(QueryDSL 도입 후)
/**
* 상품 리스트 조회 메서드(MainPage)
*/
@Transactional(readOnly = true)
public List<ItemMainResponseDto> getAllItem(ItemRequestParam itemRequestParam, Pageable pageable) {
Page<Item> itemList = itemQuerydslRepository.getItemListByCategory(
itemRequestParam.getPetCategory(), itemRequestParam.getItemCategory(), pageable);
List<ItemMainResponseDto> itemMainResponseDtoList = new ArrayList<>();
/* FE 무한스크롤 위한 마지막 게시글 확인 */
Long lastData = itemQuerydslRepository.getLastData(itemRequestParam.getPetCategory(), itemRequestParam.getItemCategory()); /* FE 무한스크롤 위한 마지막 게시글 확인 */
for (Item item : itemList) {
itemMainResponseDtoList.add(
buildItemMainResponseDto(lastData.intValue(), item)
);
}
return itemMainResponseDtoList;
}
ItemController - 상품 리스트 조회 메서드(QueryDSL 도입 후)
/* 상품 리스트 조회(MainPage) */
@ApiOperation(value = "상품 리스트 조회 메소드")
@GetMapping("items")
public ResponseEntity<?> getAllItem(ItemRequestParam itemRequestParam,
@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
return ResponseEntity.ok().body(itemService.getAllItem(itemRequestParam, pageable));
}
추가로 Controller 단에서는 RequestParam 인자를 받아오는 DTO class를 추가하여 적용시켜주었다.
ItemRequestParam
@Getter
@AllArgsConstructor
public class ItemRequestParam {
@Nullable
private String petCategory;
@Nullable
private String itemCategory;
}
이를 통해 이와같은 설정을 토대로 기존 4개로 분리되어있던 상품 리스트 호출 API를 단 하나의 API로 압축시킬 수 있었다.
'✍️개발로그' 카테고리의 다른 글
실전프로젝트 6주차(평균 가격 호출 메서드 리팩터링 feat.Spring Cache + Scheduler) (1) | 2022.09.26 |
---|---|
20220909_실전 프로젝트 14일차(최근 조회한 상품 목록) (0) | 2022.09.09 |
20220909_실전 프로젝트 14일차(Swagger 도입) (0) | 2022.09.09 |
220830_실전프로젝트 8일차(feat.Item 수정부분 트러블슈팅🚀) (0) | 2022.09.03 |
220829_실전프로젝트 4일차 (0) | 2022.08.29 |