Post View

블로그에 검색 기능을 추가했습니다.

오랜만에 새로운 기능을 추가한 것 같습니다!

최근에는 정말 뼈대만 있던 관리자 페이지 위주로 수정을 했었기 때문에 실제로는 크게 수정된 내용이 없어보입니다.
그래서 이번에는 주말을 이용해서 블로그 검색 기능을 추가했습니다.

현재는 포스트와 댓글에 대해서 검색을 할 수 있도록 해두었으며, 추후 업로드 된 파일 등 여러가지 컨텐츠들을 검색하는 통합검색 기능으로 확장해나갈 예정입니다.

검색 기능을 위해 SearchController를 추가 생성했고, 각 컨텐츠(포스트, 코멘트)의 서비스를 담당하는 클래스에 Searchable이라는 인터페이스를 구현했습니다.

/**
     * 블로그 내 컨텐츠를 검색한다.
     *  - 포스트(제목, 내용)
     *  - 코멘트(작성자, 내용)
     *  - 파일(파일명, 파일설명)
     *
     *  1. 권한을 검사한다.
     *  2. 검색할 수 있는 컨텐츠의 서비스를 찾는다.
     *  3. 검색한다.
     */
    @RequestMapping("")
    public String search(@RequestParam(required = true) String q, Model model) throws InvalidRequestException {
        if(q.length() < 2) {
            throw new InvalidRequestException("검색어는 2자 이상 입력해주세요.");
        }

        String[] queries = q.split(" ");

        List<Searchable> searcher = new ArrayList<>();
        Map<String, SearchDTO> searchDtos = new HashMap<>();

        searcher.add((Searchable)postService);
        searcher.add((Searchable)commentService); 

        for(int i = 0; i < searcher.size(); i++) {
            SearchDTO searchDto = searcher.get(i).search(queries);
 
            searchDtos.put(searchDto.getTitle( ), escapeSearchData(searchDto)); 
        }

        model.addAttribute("searchedItems", searchDtos);

        String escapeQuery = HtmlUtil.escapeHtml(q);

        template.setSubTitle("\"" + escapeQuery + "\" 검색");
        template.setDescription("검색어에 해당하는 컨텐츠를 검색합니다.");
        template.getCss().add("<link rel=\"stylesheet\" href=\"/css/module/search.css\">");
        template.setSearchQuery(escapeQuery);

        return "search/query";
    }

코드를 설명하자면, https://www.kurien.net/search?q=대책과 같은 URL을 통해 검색어(q)를 전달받습니다.
여기서 전달된 q가 2자 미만이라면 검색되는 양(부하)이 많을 것 같아 제한을 두었습니다.
2자 이상이라면 전달받은 검색어를 공백을 기준으로 나눠서 String 배열(queries)를 만들어줍니다.

이제 검색어에 해당하는 컨텐츠를 찾아줄 각 서비스를 searcher라는 List에 추가해줍니다.
SearchController가 서비스에게 요청할 것은 Searchable에 있던 search method 뿐이므로 각 서비스를 Searchable 인터페이스의 형태로 업 캐스팅하여 searcher에 추가합니다.

그 다음 반복문을 통해서 "searcher.get(i).search(queries);"로 서비스별 컨텐츠를 검색하고, 검색된 데이터를 SearchDTO로 전달받습니다.
전달받은 SearchDTO를 컨텐츠 내부에 있는 HTML 태그를 제거하고 설명의 길이를 조정해주는 escapeSearchData method를 적용한 뒤 각 서비스의 title(searchDto.getTitle())를 key로 적용해서 view로 전달합니다.

추가적으로 template에 적용될 제목, 설명, 전달받은 검색 결과 등을 적용해주었는데, 이 때도 XSS 공격을 막기 위해 HTML을 escape 처리 했습니다.

    @Override
    public SearchDTO search(String[] queries) {
        List<Map<String, Object>> contents = new ArrayList<>();
        SearchDTO searchDto = new SearchDTO();

        List<Post> posts = postDao.search(queries);

        for(Post post : posts) {
            Map<String, Object> content = new LinkedHashMap<>();

            content.put("id", post.getPostNo());
            content.put("name", post.getPostAuthor());
            content.put("title", post.getPostSubject());
            content.put("description", post.getPostContent());

            contents.add(content);
        }

        searchDto.setTitle("POST");
        searchDto.setContents(contents);

        return searchDto;
    }

이 부분은 Searchable Interface를 구현한 PostService의 search method입니다.
여기서는 전달받은 queries로 postDao를 통해 DB 검색을 하고, 검색된 post들을 반복문을 통해 SearchDTO의 Contents에 넣어줍니다.

설명은 주요 기능이라고 생각되는 두 부분만 진행했으며, 전체적인 코드는 아래의 두 링크를 통해 확인해주세요!

mybatis에서 query 별로 like 문을 통한 검색을 하도록 해놔서 시간이 지나면 성능적으로 문제가 발생하겠지만,
해당 이슈가 나오면 해결하는 방법도 공부가 될거라고 생각하여 일단 기능 개발 위주로 진행했습니다.

혹시라도 궁금한 사항이 있으시다면 댓글 남겨주시기 바랍니다.

Comments