본문 바로가기

프로젝트/KDR

[KDR] 통합 검색 기능 구현하기 (ver1. 태그 검색)

프로젝트 내에서 검색창 구현을 담당하게 되었다.

처음 구상했을 때에 비해 많은 변화가 있었기 때문에 순차적으로 정리해보고자 한다.

 

앱 내 검색창의 특징을 간단하게 설명해보자면 아래와 같다.

1. 별명으로 장소 검색 가능

2. 따로 검색 버튼을 누르지 않아도, 자동완성처럼 아래에 결과가 뜸 (미입력 시간이 기준을 넘어가면 자동 검색)

3. 건물명 / (건물 +) 강의실명 / 편의시설명 / (건물 +) 편의시설명 등으로 검색 가능 (각각 다른 화면으로 연결됨)


DB 구조 (구버전)

DB 테이블 간단 구조

  • building (건물) : 건물 (ex. 교양관, 문과대학 등)
  • classroom (강의실) : 일반 강의실 (ex. 101호, 원형 강의실 등)
  • facility (시설) : 특수 시설 (ex. 자판기, 정수기, 라운지 등)

 

먼저, 1번 특성을 만족하기 위해 건물 및 강의실 각각에 대해 별명 테이블을 생성하여 일대다 관계로 설정해주었다.

 

학교 시설들의 경우 실제 이름과는 전혀 다른 이름으로 불리는 경우도 종종 있었기 때문에 보다 학생친화적인 검색 기능을 위해 nickname 데이터를 직접 입력해주는 방법을 선택하게 되었다. (Ex. 하나과학관 -> 하곽)

 

또한, 건물 밖에 존재하는 시설들에 대해서도 일관성있는 처리를 해주기 위해 "야외"라는 빌딩을 따로 설정하여 외부의 시설들을 야외 빌딩과 엮어주었다.


태그(Tag) 검색

위와 같이 건물, 강의실, 편의시설 정보를 서로 다른 테이블에서 관리하고 있었기 때문에, 들어온 키워드를 어떻게 분리하여 각각의 테이블로 조회 쿼리를 날릴 것인지에 대한 어려움이 있었다.

 

이에 대해 회의를 하던 중, 여러 테이블의 정보들을 동시에 검색하는 경우에는 건물을 먼저 태그로 입력하도록 강제하자는 의견이 나오게 되었다.

 

사용자 입장에서의 사용 흐름을 간단히 설명하자면 아래와 같다.

태그 검색 예시 이미지

1. 사용자가 건물 이름(별명)을 검색

2. 결과로 나오는 건물 중 원하는 건물을 선택 (건물 검색 결과 화면으로 이동)

3. 건물이 태그화 되면, 사용자가 강의실명을 검색

4. BE단에 태그화된 건물의 id & 강의실 검색어가 전달됨

5. 해당 건물 내부의 시설에 대해서만, 검색어와 일치하는 정보를 반환

 

이를 위해 FE에서 태그화된 building의 id를 선택적으로 받아, id 존재 여부에 따라 검색 과정을 분리하였다.

  • 건물id O - 해당 건물에 존재하는 place만을 검색
  • 건물id X - 통합 검색 (건물명 or 장소명)

전체적인 흐름을 나타내는 메인 코드는 다음과 같다. 

    @Transactional(readOnly = true)
    public List<GlobalSearchRes> globalSearch(Long buildingId, String word) {
        List<GlobalSearchRes> resList = new ArrayList<>();
        
        // 관련 정보를 미리 모두 가져옴 -> 조건 만족 시 resList에 추가
        List<Classroom> classrooms = getClassrooms(word);
        List<Facility> facilities = getFacilities(word);
        
        // 먼저 검색한 건물이 있을 때
        if(buildingId != null) {
            Building building = findBuilding(buildingId);
            for(Classroom classroom : classrooms) {
                if(classroom.getBuilding().equals(building)) resList.add(new GlobalSearchRes(classroom, PlaceType.CLASSROOM));
            }
            for(Facility facility : facilities) {
                if(facility.getBuilding().equals(building)) resList.add(new GlobalSearchRes(facility, PlaceType.FACILITY));
            }
        }
        else { // 건물 제약X : word가 전체라는 가정 하에 검색
            List<Building> buildings = getBuildings(word);
            for(Building building : buildings) {
                resList.add(new GlobalSearchRes(building, PlaceType.BUILDING));
            }
            for(Classroom classroom : classrooms) {
                resList.add(new GlobalSearchRes(classroom, PlaceType.CLASSROOM));
            }
            for(Facility facility : facilities) {
                resList.add(new GlobalSearchRes(facility, PlaceType.FACILITY));
            }
        }
        return resList;
    }

 

nickname을 갖는 건물/강의실과 달리, 시설 (facility)은 ENUM에 저장된 시설명을 기반으로 검색하는 것을 기본으로 하였는데 이 경우 카페, 식당과 같이 특수한 이름을 가지는 시설의 검색 결과는 확인하기 어렵다는 문제를 마주하게 되었다.

 

이에 따라 facility 테이블의 name에 특수명이 있는 경우 해당 이름으로 저장하고, 없는 경우에는 기본적으로는 지정된 시설명을 저장함으로써 검색의 범용성을 높였다.

 

결과적으로 검색 자체는 잘 되었지만, 여러 사람이 입력하는 데이터에 부가적인 규율들이 많이 생겨나 데이터 입력 측면에서의 효율이 떨어지고 종종 관련 문제가 발생하기도 하였다. 이에 대한 해결책은 다른 포스트에서 다시 다뤄보겠다.


태그 검색으로 인해 검색 알고리즘은 간단하게 구현해낼 수 있었지만, 사용자의 입장에서는 단순 검색을 위해 수행해야 하는 단계가 과도하게 많아진다는 문제가 있었다. 이후 프런트와 연동하여 실제로 검색창을 사용해보았을 때는 이런 번거로움을 훨씬 크게 체감할 수 있었다...

 

이외에도 다음과 같은 문제들이 있었다.

  • 태그화 되지 않은 “ㅇㅇ건물 101호”가 키워드로 들어올 경우 검색 불가능 (동일 문장이더라도 태그화되면 검색 가능)
  • 단순 검색 기능에서 너무 많은 parameter를 넘겨 받아야 함
  • building_id를 FE에서 text로 관리하는 형태였기 때문에, DB에 변경이 발생하면 매번 수동으로 수정해야 함

아이디어 회의에서는 괜찮아 보이더라도 실제로 사용해보면 부족한 점들이 계속해서 새롭게 보이는 것 같다.

특히 실제로 사용자들을 받을 계획으로 제작하고 있었던 만큼, 검색창을 완성한 이후에도 이러한 문제들을 해결하고 싶다는 생각이 마음 언저리에 남아있었다.

 

이에 따라 구현이 조금 어려워지더라도, 태그화 기능을 제거하고 단순 String 형태의 키워드를 받아 처리하는 방식으로 수정을 하게 되었다.

 

이 부분은 다음 포스트에서!