Web/HTML, CSS, JavaScript

Javascript - DB 초성으로 검색하기! (+ Performance, 함수형, ...)

JaeHoney 2022. 3. 26. 01:46
반응형

초성 검색

프로젝트를 진행하면서 DB에 저장된 name을 초성으로 검색하는 기능을 개발하게 되었습니다.

 

그런데, 관련 기능을 검색해봤는데 검색 결과가 전부 마음에 들지가 않더라구요..

 

초성이라는 것 자체가 한글이니까 국내 자료밖에 없었고 구 시대 자료밖에 없었는데, 코드(또는 SQL)이 비효율적인 측면이 너무 많았고, 유지보수 측면에서도 너무 안좋아서 직접 구현했습니다.

 

비즈니스 로직은 아래와 같습니다.

  1. 입력 값이 전부 초성인지 확인한다.
  2. 초성으로 name을 DB에서 검색한다.

1. Map 정의

먼저 Javascript의 Map을 아래와 같이 정의합니다. 보통 배열을 사용해서 ㄱ,ㄴ,ㄷ,ㄹ,ㅁ 등을 저장하던데 하나씩 탐색하니까 시간 복잡도가 많이 낭비됩니다. Set이나 Map을 사용하는 것이 훨씬 유리합니다.

const map = new Map<string, string[]>([
    ["ㄱ", ["가", "까"]],
    ["ㄲ", ["까", "나"]],
    ["ㄴ", ["나", "다"]],
    ["ㄷ", ["다", "따"]],
    ["ㄸ", ["따", "라"]],
    ["ㄹ", ["라", "마"]],
    ["ㅁ", ["마", "바"]],
    ["ㅂ", ["바", "빠"]],
    ["ㅃ", ["빠", "사"]],
    ["ㅅ", ["사", "싸"]],
    ["ㅆ", ["싸", "아"]],
    ["ㅇ", ["아", "자"]],
    ["ㅈ", ["자", "짜"]],
    ["ㅉ", ["짜", "차"]],
    ["ㅊ", ["차", "카"]],
    ["ㅋ", ["카", "타"]],
    ["ㅌ", ["타", "파"]],
    ["ㅍ", ["파", "하"]],
    ["ㅎ", ["하", "힣"]],
]);

Set이 아니라 Map을 사용한 이유는, Where절에 들어갈 데이터를 함께 저장하기 위해서 입니다.

 

2. isConsonants 정의

입력값이 모두 초성인지 확인하는 메서드를 정의합니다. 이 때는 some()을 사용하면 됩니다. some()은 배열 안의 어떤 요소라도 주어진 판별문을 통과하는지를 따지는 메서드입니다.

static isConsonants = (query) => {
    return ![...query].some(c => !map.has(c));
}

[...string]은 문자열을 잘게 자른 배열을 반환합니다. 즉, 문자열을 잘게 자른 요소 중 하나라도 초성이 아닐 경우 즉시 true를 반환합니다. 하지만 앞에 not(!)이 있으니, 결과는 false가 되겠습니다.

 

3. getWhereClause 정의

다음은 Where절을 init해서 반환하는 메서드를 정의해야 합니다. 이 때는 reduce()를 사용합니다. reduce()는 각 요소에 계산식을 적용한 것을 누적하고, 누적한 결과를 반환할 수 있습니다.

static getWhereClause = (query, column) => {
    const clause =  [...query].reduce((p, c, i) =>
        p + `SUBSTR(${column}, ${i + 1}, 1) >= '${map.get(c)[0]}' AND SUBSTR(${column}, ${i + 1}, 1) < '${map.get(c)[1]}' AND `, "");

    return clause.substring(0, clause.length - 5);
}

예를 들어 "ㄱㅁㅅ"라는 인자로 메소드를 실행하면, 아래와 같은 WHERE절이 문자열로 반환됩니다.

SUBSTR(name, 1, 1) >= 가 AND SUBSTR(name, 1, 1) < 까 AND
SUBSTR(name, 2, 1) >= 마 AND SUBSTR(name, 2, 1) < 바 AND
SUBSTR(name, 3, 1) >= 사 AND SUBSTR(name, 3, 1) < 싸

이제 문자열 형태의 조건절을 각자의 ORM이나 DB 관련 라이브러리로 사용하시면 됩니다.

 

저의 경우 Sequelize에서 사용했고, where절을 아래와 같이 작성했습니다.

where: Sequelize.literal(ConsonantUtils.getWhereClause(query, "name"))

 

4. 전체 코드

const map = new Map<string, string[]>([
    ["ㄱ", ["가", "까"]],
    ["ㄲ", ["까", "나"]],
    ["ㄴ", ["나", "다"]],
    ["ㄷ", ["다", "따"]],
    ["ㄸ", ["따", "라"]],
    ["ㄹ", ["라", "마"]],
    ["ㅁ", ["마", "바"]],
    ["ㅂ", ["바", "빠"]],
    ["ㅃ", ["빠", "사"]],
    ["ㅅ", ["사", "싸"]],
    ["ㅆ", ["싸", "아"]],
    ["ㅇ", ["아", "자"]],
    ["ㅈ", ["자", "짜"]],
    ["ㅉ", ["짜", "차"]],
    ["ㅊ", ["차", "카"]],
    ["ㅋ", ["카", "타"]],
    ["ㅌ", ["타", "파"]],
    ["ㅍ", ["파", "하"]],
    ["ㅎ", ["하", "힣"]],
]);

export class ConsonantUtils {
    static isConsonants = (query) => {
        return ![...query].some(c => !map.has(c));
    }

    static getWhereClause = (query, column) => {
        const clause =  [...query].reduce((p, c, i) =>
            p + `SUBSTR(${column}, ${i + 1}, 1) >= '${map.get(c)[0]}' AND SUBSTR(${column}, ${i + 1}, 1) < '${map.get(c)[1]}' AND `, "");

        return clause.substring(0, clause.length - 5);
    }
}

 


 

초성 검색 SQL문을 검색해보시면 SQL 함수나 프로시저를 정의해서 사용하는 내용이 많이 나옵니다.

 

초성 배열을 하나씩 전부 순회하고, 각각에 맞는 CASE의 WHEN절에서 조건절을 하드 코딩해서 적용하는 부분이 많던데, 그러면 performance는 물론이고 유지보수성도 좋지 않습니다.

 

제가 구성한 소스를 참고하시면 실행 과정이나 코드 둘 다 훨씬 간결하게 초성 검색을 구현할 수 있습니다.

 

감사합니다.

 

 

 

 

반응형