자기전에 항상 개인 사이드프로젝트와 맹 사장님과의 프로젝트 스터디 때문에
초성검색 기능을 실제로 구현해본적이 없어서 구글링 해봤는데, 다양한 방법들이
존재하였습니다.

제가 사용한 방법은 각 자음에 대한 유니코드 값을 얻은 다음, 해당 자음으로 시작되는 문자들의 범위 값을 저장해두고 활용하는 방법입니다.

예를 들어, 'ㄱ'에 대한 유니코드는 10진수로 12593 이고 'ㄱ' 이 포함된 글자부터 '가'
부터 '깋' 까지 유니코드는 10진수로 44032부터 44619 까지입니다.

그러면 이 값을 이용하여 사용자가 이용한 초성들을 이용해 초성 검색을 수행 할수 있습니다.
이를 위해서 다음과 같이 유틸리티 클래스를 작성해봤습니다.

배열을 검색하고, 재귀 호출 메소드가 있어서 다소 복잡해 보입니다만, 특별한 알고리즘이 포함된것은 아닙니다.

package com.search;

public final class Constants {
    private Constants() {} 
    
    public static final class Unicode {
        private Unicode() {}
        
        public static final int[][] CHOSUNG_RANGES = {
            {0x3131, 0xAC00, 0xAE4B}, // ㄱ
            {0x3132, 0xAE4C, 0xB097}, // ㄲ
            {0x3134, 0xB098, 0xB2E3}, // ㄴ
            {0x3137, 0xB2E4, 0xB77F}, // ㄷ
            {0x3138, 0xB780, 0xB9CB}, // ㄸ
            {0x3139, 0xB9CC, 0xBC17}, // ㄹ
            {0x3141, 0xBC18, 0xBE63}, // ㅁ
            {0x3142, 0xBE64, 0xC0AF}, // ㅂ
            {0x3143, 0xC0B0, 0xC2FB}, // ㅃ
            {0x3145, 0xC2FC, 0xC547}, // ㅅ
            {0x3146, 0xC548, 0xC793}, // ㅆ
            {0x3147, 0xC794, 0xC9DF}, // ㅇ
            {0x3148, 0xC9E0, 0xCC2B}, // ㅈ
            {0x3149, 0xCC2C, 0xCE77}, // ㅉ
            {0x314A, 0xCE78, 0xD0C3}, // ㅊ
            {0x314B, 0xD0C4, 0xD30F}, // ㅋ
            {0x314C, 0xD310, 0xD55B}, // ㅌ
            {0x314D, 0xD55C, 0xD7A7}, // ㅍ
            {0x314E, 0xD7A8, 0xD9F3}  // ㅎ
        };
    }
}

그러면 이 값을 이용하여 사용자가 입력한 초성들을 이용해 초성검색을 수행 할수 있습니다.
이를 위해 다음과 같이 유틸클래스를 작성해보았습니다.

또한 특별한 알고리즘이 포함된것은 아닙니다.

package com.search;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

public class ChosungSearchService {
    private static final ConcurrentHashMap<Character, UnicodeRange> unicodeRangeCache = new ConcurrentHashMap<>();
    
    static {
       
        for (int[] range : Constants.Unicode.CHOSUNG_RANGES) {
            unicodeRangeCache.put((char)range[0], new UnicodeRange(range[1], range[2]));
        }
    }
    
    private record UnicodeRange(int start, int end) {
        boolean contains(char c) {
            return c >= start && c <= end;
        }
    }

    public List<String> search(String searchPattern, List<String> targets) {
        if (searchPattern == null || searchPattern.isEmpty() || targets == null || targets.isEmpty()) {
            return List.of(); 
        }

        return targets.parallelStream()
                     .filter(target -> matches(searchPattern, target))
                     .collect(Collectors.toList());
    }

    private boolean matches(String pattern, String target) {
        if (pattern.length() > target.length()) {
            return false;
        }

        for (int i = 0; i < pattern.length(); i++) {
            char patternChar = pattern.charAt(i);
            UnicodeRange range = unicodeRangeCache.get(patternChar);
            
            if (range == null || !range.contains(target.charAt(i))) {
                return false;
            }
        }
        return true;
    }
}

사용법은 다음과 같습니다.

package com.search;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Main {
    private static final ChosungSearchService searchService = new ChosungSearchService();
    private static final ScheduledExecutorService executorService = 
        Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors());

    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
            
            List<String> targets = List.of(
                "동해물과", "백두산이", "마르고", "닳도록", "하느님이", "보우하사", "우리나라만세",
                "무궁화", "삼천리", "화려강산", "대한사람", "대한으로", "길이", "보존하세"
            );

            while (true) {
                try {
                    System.out.print("검색할 초성을 입력하세요 (종료하려면 'exit' 입력): ");
                    String input = reader.readLine();

                    if ("exit".equalsIgnoreCase(input)) {
                        break;
                    }

                    if (input == null || input.trim().isEmpty()) {
                        System.out.println("검색할 초성을 입력하세요!!");
                        continue;
                    }

                    
                    var searchTask = () -> {
                        List<String> results = searchService.search(input.trim(), targets);
                        if (results.isEmpty()) {
                            System.out.println("검색 결과가 없습니다!!");
                        } else {
                            results.forEach(System.out::println);
                        }
                    };

                    
                    executorService.schedule(searchTask, 0, TimeUnit.MILLISECONDS);

                } catch (IOException e) {
                    System.err.println("입력 처리 중 오류가 발생했습니다: " + e.getMessage());
                }
            }
        } catch (IOException e) {
            System.err.println("시스템 오류가 발생했습니다: " + e.getMessage());
        } finally {
            executorService.shutdown();
            try {
                if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
                    executorService.shutdownNow();
                }
            } catch (InterruptedException e) {
                executorService.shutdownNow();
            }
        }
    }
}

서비스 클래스를 search 서비스를 호출합니다.

var searchTask = () -> {
                        List<String> results = searchService.search(input.trim(), targets);
                        if (results.isEmpty()) {
                            System.out.println("검색 결과가 없습니다!!");
                        } else {
                            results.forEach(System.out::println);
                        }
                    };
profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글