트러블슈팅 및 코드리뷰
Spring Data JPA Vs QueryDSL 동적 쿼리 구현하기
옴악핫세
2023. 7. 19. 15:02
동적 쿼리란 상황에 따라 다른 문법의 SQL을 적용하는 것을 말한다.
동적 쿼리를 적용한 예를들면
아래와 같이 약국을 조회하는 기능을 구현하고자 한다.
조회 조건, 즉 필터는 (1) 지역, (2) 내위치, (3) 약국명, (4) 영업 여부, (5) 공휴일 영업 여부, (6) 야간 영업 여부, (7) 외국어 가능 여부 모두 7가지이다.
지역 필터 조건을 관악구로 해서 약국 DB에 필터에 맞는 조건을 조회 할 수 있는 SQL문을 작성해야한다.
여기에 필터를 한개 더 추가할 경우 기존에 만들어 놓은 SQL 대신 필터가 추가된 조건을 담은 SQL문을 다시 작성해야한다.
이렇게 필터 조건들이 많아질수록 많은 SQL문을 작성해야하고, 이 필터 조건을 선택적으로 적용할 경우
2^n-1 에 해당하는 경우의 수가 나오게되어 매우 코드가 길어지고 가독성이 떨어진다.
이런 방법도 있고 아래 Spring Data JPA로 구현한 코드 처럼
한개의 조건에 대해 데이터를 조회해서 List에 담고
그다음 조건에 대해 데이터를 조회해서 기존 List에 없으면 데이터를 추가한다.
이런 방식으로 모든 조건에 대해서 코드를 작성할 수도 있다.
이 경우도 가독성이 떨어지기는 마찬가지!
Spring Data JPA (코드 311줄)
public Page<ForeignStoreResponse> searchForeignStore(int page, int size, String storeName, String gu, boolean open, boolean holidayBusiness, boolean nightBusiness, boolean english, boolean chinese, boolean japanese, String radius, String latitude, String longitude, UserDetailsImpl userDetails) {
int progress = 0; //stores 리스트가 null일 때 0, 반대는 1
List<ForeignStoreResponse> foreignStoreResponses = new ArrayList<>();
List<Store> stores = new ArrayList<>();
//내 위치 기반 가까운 약국 검색
if (latitude != "") {
progress = 1;
//stores = storeRepository.findByDistanceWithinRadius(baseRadius, baseLatitude, baseLongitude);
stores = storeRepositoryCustom.searchStoreWithinDistance(radius, latitude, longitude);
}
//약국 이름 검색하기
if(storeName != ""){
if(progress == 0){
progress = 1;
stores = storeRepository.findAllByNameContaining(storeName);
}else{
List<Store> testStores = new ArrayList<>();
for(Store store: stores){
testStores.add(store);
}
for(Store testStore : testStores){
if(!testStore.getName().contains(storeName)){
stores.remove(testStore);
}
}
}
}
//구 검색하기
if(!gu.equals("")){
if(progress == 0){ //저장된 stores가 없을 때
progress = 1;
stores = storeRepository.findAllByAddressContaining(gu);
}else{ //저장된 stores가 있을 때
List<Store> testStores = new ArrayList<>();
for(Store store: stores){
testStores.add(store);
}
for(Store testStore : testStores){
if(!testStore.getAddress().contains(gu)){
stores.remove(testStore);
}
}
}
}else if(progress == 0){
Pageable pageable = PageRequest.of(page, size);
final int start = (int)pageable.getOffset();
final int end = Math.min((start + pageable.getPageSize()), foreignStoreResponses.size());
final Page<ForeignStoreResponse> foreignStoreResponsePage = new PageImpl<>(foreignStoreResponses.subList(start, end), pageable, foreignStoreResponses.size());
return foreignStoreResponsePage;
}
//각종 필터
if(open){ // 영업중 필터
if(progress == 1){ //저장된 stores가 있을 때만 실행 가능함
stores = openCheck(stores);
}
}else if(holidayBusiness){
if(progress == 0){
progress = 1;
stores = storeRepository.findAllByHolidayTimeIsNotNull();
}else{
List<Store> restStores = new ArrayList<>();
for(Store store: stores){
restStores.add(store);
}
for(Store restStore : restStores){
if(restStore.getHolidayTime() == null){
stores.remove(restStore);
}
}
}
}else if (nightBusiness){
if(progress == 0){
progress = 1;
stores = storeRepository.findAllByNightPharmacy(1);
}else {
List<Store> restStores = new ArrayList<>();
for(Store store: stores){
restStores.add(store);
}
for(Store restStore : restStores){
if(restStore.getNightPharmacy() != 1){
stores.remove(restStore);
}
}
}
}
if(english){
if(progress == 0){
progress = 1;
stores = storeRepository.findAllByEnglish(1);
} else{
// List<Store> restStores = new ArrayList<>();
//
// for(Store store: stores){
// restStores.add(store);
// }
// for(Store restStore : restStores){
for(Iterator<Store> storeIterator = stores.iterator(); storeIterator.hasNext();){
Store store = storeIterator.next();
if(store.getEnglish() == null || store.getEnglish() == 0){
// stores.remove(restStore);
storeIterator.remove();
}
}
}
}else if(chinese){
if(progress == 0){
progress = 1;
stores = storeRepository.findAllByChinese(1);
} else{
List<Store> restStores = new ArrayList<>();
for(Store store: stores){
restStores.add(store);
}
for(Store restStore : restStores){
if(restStore.getChinese() == null || restStore.getChinese() == 0){
stores.remove(restStore);
}
}
}
}else if(japanese){
if(progress == 0){
progress = 1;
stores = storeRepository.findAllByJapanese(1);
} else{
List<Store> restStores = new ArrayList<>();
for(Store store: stores){
restStores.add(store);
}
for(Store restStore : restStores){
if(restStore.getJapanese() == null || restStore.getJapanese() == 0){
stores.remove(restStore);
}
}
}
}
foreignStoreResponses = checkForeignBookmark(stores, foreignStoreResponses, userDetails);
Pageable pageable;
if(latitude.equals("")){
// pageable = PageRequest.of(page, size, Sort.by("name").ascending());
pageable = PageRequest.of(page, size, Sort.Direction.DESC, "name");
}else {
pageable = PageRequest.of(page, size);
}
final int start = (int)pageable.getOffset();
final int end = Math.min((start + pageable.getPageSize()), foreignStoreResponses.size());
final Page<ForeignStoreResponse> foreignStoreResponsePage = new PageImpl<>(foreignStoreResponses.subList(start, end), pageable, foreignStoreResponses.size());
return foreignStoreResponsePage;
}
//영업중 필터 검사 로직
private List<Store> openCheck(List<Store> stores){
// List<Store> restStores = new ArrayList<>();
// for(Store store: stores){
// restStores.add(store);
// }
LocalDate now = LocalDate.now();
int dayOfWeek = now.getDayOfWeek().getValue();
LocalTime nowTime = LocalTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
// 포맷 적용하기
String formatedNow = nowTime.format(formatter);
int nowHour = Integer.parseInt(formatedNow.substring(0, 2));
int nowMin = Integer.parseInt(formatedNow.substring(3, 5));
int openHour = 0;
int openMin = 0;
int closeHour = 0;
int closeMin = 0;
// for (Store restStore : restStores) {
for(Iterator<Store> storeIterator = stores.iterator(); storeIterator.hasNext();){
Store store = storeIterator.next();
int status = 0; //시간 null이면 1, 아니면 0
if (dayOfWeek > 0 && dayOfWeek < 6) { //평일
String storeTime = store.getWeekdaysTime();
if(storeTime != null && !storeTime.contains("nu")){ //TODO: nu:ll 로 시간 들어가 있는 객체 골라서 작업하기
String[] storeTimes = storeTime.split("~");
openHour = Integer.parseInt(storeTimes[0].substring(3, 5));
openMin = Integer.parseInt(storeTimes[0].substring(6, 8));
closeHour = Integer.parseInt(storeTimes[1].substring(1, 3));
closeMin = Integer.parseInt(storeTimes[1].substring(4, 6));
}else {
status = 1;
// stores.remove(restStore);
storeIterator.remove();
}
}else if (dayOfWeek == 6){ // 토요일 TODO: 일요일이랑 합치기
String storeTime = store.getSaturdayTime();
if (storeTime != null && !storeTime.contains("nu")){
String[] storeTimes = storeTime.split("~");
openHour = Integer.parseInt(storeTimes[0].substring(2, 4));
openMin = Integer.parseInt(storeTimes[0].substring(5, 7));
closeHour = Integer.parseInt(storeTimes[1].substring(1, 3));
closeMin = Integer.parseInt(storeTimes[1].substring(4, 6));
}else{
status = 1;
// stores.remove(restStore);
storeIterator.remove();
}
}else if( dayOfWeek == 7){ // 일요일
String storeTime = store.getSundayTime();
if(storeTime != null && !storeTime.contains("nu")){
String[] storeTimes = storeTime.split("~");
openHour = Integer.parseInt(storeTimes[0].substring(2, 4));
openMin = Integer.parseInt(storeTimes[0].substring(5, 7));
closeHour = Integer.parseInt(storeTimes[1].substring(1, 3));
closeMin = Integer.parseInt(storeTimes[1].substring(4, 6));
}else {
status = 1;
// stores.remove(restStore);
storeIterator.remove();
}
}
if(status != 1){
if((openHour < nowHour) && (closeHour > nowHour)){
continue;
}else if((openHour == nowHour) && (openMin < nowMin)){
continue;
}else if((closeHour == nowHour) && (closeMin > nowMin)){
continue;
}else if((closeHour == openHour)){
continue;
}else{
// stores.remove(restStore);
storeIterator.remove();
}
}
}
return stores;
}
이번에는 QueryDSL을 적용해서 작성해보자.
아래와 같이 SQL 문과 비슷한 구조의 java 코드를 확인할 수 있다.
때문에 가독성 측면에서 훨씬 직관적이고 유지보수가 쉽다.
그렇기에 동적 쿼리를 구현하려면 QueryDSL을 많이 쓰나보다.
QueryDSL (코드 166줄)
public Page<ForeignStoreResponse> searchForeignStoreWithFilter(MappedSearchForeignRequest request, UserDetailsImpl userDetails) {
int page = request.getPage();
int size = request.getSize();
QueryResults<ForeignStoreResponse> results = jpaQueryFactory
.select(Projections.constructor(
ForeignStoreResponse.class,
store.id, store.address, store.name, store.callNumber,
store.weekdaysTime, store.longitude, store.latitude,
store.english, store.chinese, store.japanese))
.from(store)
.where(
withinDistance(request.getLatitude(), request.getLongitude(), store.latitude, store.longitude),
eqAddressTest(request.getGu()),
eqStoreName(request.getStoreName()),
checkOpen(request.isOpen()),
checkHolidayOpen(request.isHolidayBusiness()),
checkNightdOpen(request.isNightBusiness()),
eqEnglish(request.getEnglish()),
eqChinese(request.getChinese()),
eqJapanese(request.getJapanese())
)
.offset(page * size)
.limit(size)
.fetchResults();
return new PageImpl<>(results.getResults(), PageRequest.of(page, size), results.getTotal());
}
private BooleanExpression eqAddress(String gu) {
if (gu == null) {
return null;
}
return store.address.like("%" + gu + "%");
}
private BooleanExpression eqAddressTest(String gu) {
if (gu == null) {
return null;
}
return gu != null ? store.address.like("%" + gu + "%") : null;
}
private BooleanExpression eqStoreName(String storeName) {
return storeName != null ? store.name.like("%" + storeName + "%") : null;
}
private BooleanExpression checkOpen(boolean open) {
if (open) {
LocalDate currentDate = LocalDate.now();
DayOfWeek dayOfWeek = currentDate.getDayOfWeek();
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
String currentDateTime = LocalDateTime.now().format(timeFormatter);
//String currentDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("17:00"));
if (dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY) {
BooleanExpression startCondition = Expressions.booleanTemplate(
"TIME({0}) <= TIME_FORMAT(SUBSTRING_INDEX(weekdays_time, ' ', -1), '%H:%i')",
currentDateTime
);
BooleanExpression endCondition = Expressions.booleanTemplate(
"TIME({0}) >= TIME_FORMAT(SUBSTRING_INDEX(SUBSTRING_INDEX(weekdays_time, ' ~', 1), ' ', -1), '%H:%i')",
currentDateTime
);
BooleanExpression equalCondition = Expressions.booleanTemplate(
"TIME_FORMAT(SUBSTRING_INDEX(weekdays_time, ' ', -1), '%H:%i') = TIME_FORMAT(SUBSTRING_INDEX(SUBSTRING_INDEX(weekdays_time, ' ~', 1), ' ', -1), '%H:%i')"
);
return startCondition.and(endCondition).or(equalCondition);
} else if (dayOfWeek == DayOfWeek.SATURDAY) {
BooleanExpression startCondition = Expressions.booleanTemplate(
"TIME({0}) <= TIME_FORMAT(SUBSTRING_INDEX(saturday_time, ' ', -1), '%H:%i')",
currentDateTime
);
BooleanExpression endCondition = Expressions.booleanTemplate(
"TIME({0}) >= TIME_FORMAT(SUBSTRING_INDEX(SUBSTRING_INDEX(saturday_time, ' ~', 1), ' ', -1), '%H:%i')",
currentDateTime
);
BooleanExpression equalCondition = Expressions.booleanTemplate(
"TIME_FORMAT(SUBSTRING_INDEX(saturday_time, ' ', -1), '%H:%i') = TIME_FORMAT(SUBSTRING_INDEX(SUBSTRING_INDEX(saturday_time, ' ~', 1), ' ', -1), '%H:%i')"
);
return startCondition.and(endCondition).or(equalCondition);
} else if (dayOfWeek == DayOfWeek.SUNDAY) {
BooleanExpression startCondition = Expressions.booleanTemplate(
"TIME({0}) <= TIME_FORMAT(SUBSTRING_INDEX(sunday_time, ' ', -1), '%H:%i')",
currentDateTime
);
BooleanExpression endCondition = Expressions.booleanTemplate(
"TIME({0}) >= TIME_FORMAT(SUBSTRING_INDEX(SUBSTRING_INDEX(sunday_time, ' ~', 1), ' ', -1), '%H:%i')",
currentDateTime
);
BooleanExpression equalCondition = Expressions.booleanTemplate(
"TIME_FORMAT(SUBSTRING_INDEX(sunday_time, ' ', -1), '%H:%i') = TIME_FORMAT(SUBSTRING_INDEX(SUBSTRING_INDEX(sunday_time, ' ~', 1), ' ', -1), '%H:%i')"
);
return startCondition.and(endCondition).or(equalCondition);
}
}
return null;
}
private BooleanExpression checkHolidayOpen(boolean holidayBusiness) {
return holidayBusiness == true ? store.holidayTime.isNotNull() : null;
}
private BooleanExpression checkNightdOpen(boolean nightBusiness) {
return nightBusiness == true ? store.nightPharmacy.eq(1) : null;
}
private BooleanExpression eqEnglish(int english) { return english == 1 ? store.english.eq(1) : null;
}
private BooleanExpression eqChinese(Integer chinese) {
return chinese == 1 ? store.chinese.eq(1) : null;
}
private BooleanExpression eqJapanese(Integer japanese) {
return japanese == 1 ? store.japanese.eq(1) : null;
}
private BooleanExpression withinDistance(String baseLatitude, String baseLongitude, NumberPath<Double> latitude, NumberPath<Double> longitude) {
if (baseLatitude == null) {
return null;}
else {
// Double b_latitude = Double.parseDouble(baseLatitude);
// Double b_longitude = Double.parseDouble(baseLongitude);
Double baseRadius = 1.0;
NumberExpression<Double> distance = distance(baseLatitude, baseLongitude, latitude, longitude);
return distance.loe(baseRadius);
}
}
private NumberExpression<Double> distance(String baseLatitude, String baseLongitude, NumberPath<Double> latitude, NumberPath<Double> longitude) {
if (baseLatitude == null) {
return null;
} else {
double earthRadius = 6371; // 지구 반지름 (단위: km)
double baseLatitudeRad = Math.toRadians(Double.parseDouble(baseLatitude));
double baseLongitudeRad = Math.toRadians(Double.parseDouble(baseLongitude));
return Expressions.numberTemplate(Double.class,
"({0} * acos(cos({1}) * cos(radians({3})) * cos(radians({4})-{2}) + sin({1}) * sin(radians({3}))))",
earthRadius, baseLatitudeRad, baseLongitudeRad, latitude, longitude);
}
}