[Springboot] 검색 기능 개선하기(1) 띄어쓰기 없는 문자열로 검색했을 때 결과 반환하기

HeavyJ·2023년 6월 10일
1

자바/스프링부트

목록 보기
13/17

검색 기능의 어려움

검색 기능이야말로 정말 다양한 경우의 수가 나온다고 생각합니다.

사용자들이 검색 기능을 사용할 때 사용자마다 검색 스타일이 다른데, 스타일에 따라 일관된 결괏값을 반환해야 하므로 여러 경우의 수를 고려하여 검색 기능을 개선할 필요가 있습니다.

기존의 검색 로직

제 기존 검색 로직은 MySQL like 함수의 like "%검색어%" 입니다.

like 구문을 통해 긴 text로 저장된 데이터에서 원하는 검색어가 포함되는지를 확인할 수 있고 포함되어 있다면 해당 데이터를 반환합니다.

예를 들어, 제 DB에 '해리 스타일스 첫 내한 공연' 이라는 공연 제목 데이터가 저장되어 있다고 가정하겠습니다.

이럴 경우

select con_title from calendar_info_table where con_title like "%해리%";

select con_title from calendar_info_table where con_title like "%스타%";

select con_title from calendar_info_table where con_title like "%내한%";

위 세 sql 문을 실행하면 똑같이 해리 스타일스 첫 내한 공연을 반환합니다.

따라서, 이 like %% 함수를 사용하기 위해 저는 Query Method 중 contains 메소드를 사용했습니다.

Repository에서 선언하고 사용한 Query Method는 아래와 같습니다.

@Repository
public interface CalendarRepository extends JPARepository<Calendar, Long> {
	List<Calendar> findCalendarsConTitleContains(String searchKeyword);
}

로직의 문제점

그런데 이 로직에는 문제점이 존재합니다.
바로 띄어쓰기하지 않은 채로 검색할 경우입니다.
예를 들어, 해리스타일스라고 검색할 경우 결과가 반환되지 않습니다.

사용자 입장에서는 해리 스타일스를 검색했을 때와 해리스타일스를 검색했을 때 같은 결과가 검색돼야 한다고 생각했습니다.

그래서 이번 포스팅에서는 DB에는 띄어쓰기가 된 데이터이고 실제 검색 결과는 띄어쓰기가 없는 경우 해결 방법에 대해 작성해 보겠습니다.

띄어쓰기 없이 검색했을 때 올바른 결과 도출하기

상황

  • DB에는 해리 스타일스 첫 내한 공연이라고 저장이 되어 있음
  • 사용자는 해리스타일스라고 검색함

로직

  • 공백이 제거된 검색어와 공백이 제거된 DB 데이터끼리 like 연산을 진행

목표

  • 해리 스타일스 첫 내한 공연이 반환되도록 해결하기
  • DB 내용 수정하지 않기

이 문제점을 해결한 뚜렷한 방법을 구글링으로 찾기 힘들어서 일단은 하드 코딩으로 진행을 해봤습니다.

첫 번째로 생각한 방법 (하드코딩)

검색어의 띄어쓰기를 모두 제거하고 새로운 List나 Map과 같은 자료구조를 선언한 뒤 띄어쓰기를 모두 제거한 제목 데이터를 자료구조에 저장하여 비교하는 방법입니다.

먼저, 검색된 문자열의 띄어쓰기를 모두 제거합니다.

String replacedSearchKeyword = searchKeyword.replaceAll("\\s","");

자료 구조를 선언합니다. ex) Map을 만들어줍니다.

Map<Long, String> noTrimConTitleMap = new HashMap<>();

DB에 있는 모든 공연 정보 제목 데이터를 id(key), 띄어쓰기가 제거된 제목(value) 형태로 저장합니다.

for(Calendar calendar : calendarList){
	map.add(calendar.getId(), calendar.getConTitle().replaceAll("\\s","");
}

Map에서 검색결과에 해당하는 제목을 찾고
있다면 해당 데이터를 저장합니다.

// 검색된 공연 저장할 리스트 선언
List<Calendar> searchedCalendarList = new ArrayList<>();

// Map 순회문
for(Map.Entry<Long, String> entry : noTrimConTitlemap.entrySet()) {
	if(replacedSearchKeyWord.equals(entry.getValue())){
    	searchedCalendarList.add(calendarRepository.findById(entry.getKey());
    }
}

이 방법은 제가 그냥 하드코딩 관점에서 로직을 생각해 봤는데 이렇게 만들면 거의 협업을 포기해야 할 정도의 가독성이 나올 것 같네요.. ㅠㅠ
로직도 너무 복잡하고 가독성이 많이 안 좋네요
이건 안 될 것 같습니다...

해결한 방법 (MySQL Dialect + Querydsl)

결국 Java에서 공백이 제거된 데이터를 따로 저장하고 싶지 않았기 때문에 MySQL의 기능을 써야겠다는 생각이 들었습니다.

그래서 일단은 MySQL의 sql문을 통해서 결과가 나오도록 sql문법을 찾아봤습니다.

그러던중, MySQL의 replace 함수를 찾게 됐습니다.

replace 함수란 원하는 문자를 다른 문자로 치환해주는 함수입니다.
replace(값 혹은 컬럼, '변경할 값', '뭘로 변경할지')

그래서 replace 함수를 실제로 써보면 아래와 같습니다.

select replace(con_title,"해","하") from calendar_info_table where con_title like "%해리%";

결과를 보면 해리 스타일스 첫 내한공연하리 스타일스 첫 내한공연으로 치환됐습니다.

이걸 where 절에서 사용하면 띄어쓰기가 없는 검색어도 이 방법으로 찾을 수 있겠다고 확신이 들었습니다.

따라서, where 절에 적용해봤습니다.

select * from calendar_info_table where replace(con_title," ","") like "%해리스타%";


잘 나옵니다!!


이 MySQL의 로직을 Springboot 에서 사용하기 위해서는 2가지를 해야 합니다.
  • MySQL Dialect 설정
  • Querydsl 사용

MySQL Dialect 설정

Dialect는 JPA에서 다양한 DBMS를 사용하기 위해 사용하는 방법입니다.
JPA는 특정 DBMS에 종속되어 있지 않기 때문에 해당 DBMS에 맞는 sql 문을 생성할 수 있습니다.
이 이점을 사용하기 위해서 Dialect 설정을 해줘야 합니다.

Dialect는 방언이란 뜻인데 방언이라고 표현하는 이유는 각각의 DBMS는 기본적으로는 ANSI SQL을 따르지만 약간의 문법과 함수가 다른 경우가 있기 때문입니다.

  jpa:
    properties:
      hibernate:
        # jpa에서 어떤 데이터베이스에 맞춰 쿼리를 작성할 지 표시
        dialect: org.hibernate.dialect.MySQL8Dialect

MySQL의 함수와 같이 세부사항에 조건을 걸어줘야 할 경우 Querydsl은 좋은 선택지가 될 수 있습니다.
Q클래스를 이용하기 때문에 Type-safe하게 쿼리 작성이 가능하고 컴파일 타임에 오류를 확인할 수 있다는 장점이 있기 때문입니다.

Querydsl 사용

저는 기존의 Query Method를 Querydsl로 재정의 해주는 방식을 선택했습니다.

저는 Querydsl을 만들 대 Custom 인터페이스를 만들고 Impl 클래스를 만들어 줍니다.

CalendarRepsitoryCustom.interface

public interface CalendarRepositoryCustom {
    List<Calendar> findCalendarsByConTitleContains(String searchKeyword);

}

CalendarRepositoryImpl.class

@RequiredArgsConstructor
public class CalendarRepositoryImpl implements CalendarRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    QCalendar qCalendar = QCalendar.calendar;

    @Override
    public List<Calendar> findCalendarsByConTitleContains(String searchKeyword){
        return queryFactory
                .select(qCalendar)
                .from(qCalendar)
                .where(
                        Expressions.stringTemplate("function('replace',{0},{1},{2})",qCalendar.conTitle," ","").contains(searchKeyword)
                )
                .orderBy(qCalendar.conNo.asc())
                .fetch();
    }
}

Expressions.stringTemplate()을 사용했는데 이 문법에 대해 잠깐 알아보겠습니다.
Expressions.stringTemplate()은 Querydsl에서 제공하는 기능으로 sql의 함수를 사용해야할 때 sql 함수를 편리하게 호출할 수 있도록 해주는 메소드입니다.

그리고
function(('replace',{0},{1},{2}),qCalendar.conTitle," ","").contains(searchKeyword))로 sql문의 replace 함수를 호출하고 q클래스의 공연제목의 공백을 제거한 문자열에 해당 검색어(searchKeyWord)가 포함되어 있는지(like %%) 확인하는 where문을 걸어줍니다.

참고로 contains() 메소드는 자동으로 like %%를 설정해주기 때문에 편리하다는 장점이 있습니다.

Querydsl로 설정해준뒤 제대로 동작하는지 확인해봤습니다.

올바르게 반환이 되는 것을 확인할 수 있습니다!

검색어는 다양한 형태로 요청이 올 수 있다는 것을 염두해두고 개선해나갈 필요가 있을 것 같습니다!!

다음 포스팅에는 다른 경우의 검색 상황일 때에 대한 해결 방법에 대해 포스팅하겠습니다😆

profile
There are no two words in the English language more harmful than “good job”.

1개의 댓글

comment-user-thumbnail
2023년 6월 23일

정말 좋은 글입니다 ! 하지만 full text index에 대해서도 알아보시길 추천합니다

답글 달기