검색 기능 (Search Service) - fitpassTeam/fitpass GitHub Wiki

검색 기능 (Search Service)

개요

키워드 기반 통합 검색 시스템으로 헬스장(Gym), 트레이너(Trainer), 게시물(Post)을 대상으로 검색할 수 있습니다. Redis 캐싱을 통한 성능 최적화와 검색 키워드 통계 수집 기능을 포함합니다.

전체 구조 개요

domain/search/
├── controller/
│   ├── SearchGymController.java           # 헬스장 검색 API
│   ├── SearchTrainerController.java       # 트레이너 검색 API
│   └── SearchPostController.java          # 게시물 검색 API
├── service/
│   ├── SearchGymService.java             # 헬스장 검색 비즈니스 로직
│   ├── SearchTrainerService.java         # 트레이너 검색 비즈니스 로직
│   ├── SearchPostService.java            # 게시물 검색 비즈니스 로직
│   ├── DummyDataInitializer.java         # 테스트 데이터 생성 (주석처리)
│   ├── GymNameGenerator.java             # 헬스장 이름 생성기
│   └── PostDummyInitializer.java         # 게시물 더미 데이터 생성기
├── entity/
│   ├── SearchKeywordGym.java             # 헬스장 검색 키워드 통계
│   ├── SearchKeywordTrainer.java         # 트레이너 검색 키워드 통계
│   └── SearchKeywordPost.java            # 게시물 검색 키워드 통계
├── repository/
│   ├── SearchGymRepository.java          # 헬스장 검색 키워드 레포지토리
│   ├── SearchTrainerRepository.java      # 트레이너 검색 키워드 레포지토리
│   └── SearchPostRepository.java         # 게시물 검색 키워드 레포지토리
└── config/
    └── CacheConfig.java                  # Redis 캐싱 설정

주요 기능

통합 검색 시스템

  • 헬스장, 트레이너, 게시물 통합 검색
  • 키워드 기반 부분 일치 검색
  • 페이징 처리를 통한 대용량 데이터 지원

성능 최적화

  • Redis 캐싱을 통한 검색 결과 캐싱
  • 커스텀 키 생성기를 통한 효율적인 캐시 키 관리
  • 메모리 기반 캐시 매니저 활용

검색 통계 수집

  • 검색 키워드별 사용 빈도 추적
  • 인기 검색어 분석을 위한 카운트 시스템
  • 키워드별 독립적인 통계 관리

확장 가능한 구조

  • 도메인별 독립적인 검색 서비스
  • 새로운 검색 대상 쉽게 추가 가능
  • 일관된 API 인터페이스

검색 대상 및 기준

검색 대상 (Search Targets)

대상 설명 검색 기준 페이징 기본값
Gym 헬스장 정보 헬스장 이름 (name) 20개 / 페이지
Trainer 트레이너 정보 트레이너 이름 (name) 20개 / 페이지
Post 게시물 정보 제목 (title) + 내용 (content) 20개 / 페이지

검색 방식 (Search Methods)

검색 타입 SQL 조건 설명
부분 일치 LIKE %keyword% 키워드가 포함된 모든 결과 검색
대소문자 무시 기본 설정 MySQL 기본 collation 활용 (예: utf8mb4_general_ci)
상태 필터링 postStatus <> 'DELETED' 삭제된 게시물 제외

검색 처리 흐름

1. 헬스장 검색

GET /search/gyms/v1?keyword={검색어}&page={페이지}&size={크기}

처리 과정 :

  1. Controller (SearchGymController.searchGym)

    • 검색 키워드 파라미터 수신
    • 페이징 정보 처리 (기본: page=0, size=20)
    • Service 계층 호출
  2. Service (SearchGymService)

    • 키워드 통계 저장: saveSearchKeywordGym(keyword)
      • 기존 키워드 존재 시 → 카운트 증가
      • 새로운 키워드 시 → 신규 생성 (count=1)
    • 캐시 확인: @Cacheable(cacheNames = "gymSearch")
    • 데이터베이스 검색: gymRepository.findByNameContaining(keyword, pageable)
    • DTO 변환: Page<Gym>Page<GymResDto>
  3. Repository (GymRepository)

    • JPA 쿼리: findByNameContaining
    • 자동 페이징 처리

2. 트레이너 검색

GET /search/trainers?keyword={검색어}&page={페이지}&size={크기}

처리 과정:

  • 헬스장 검색과 동일한 플로우
  • 캐시 키: keyword + '_' + pageNumber + '_' + pageSize
  • 검색 기준: 트레이너 이름 부분 일치
  • DTO 변환: TrainerResponseDto.fromEntity(trainer)

3. 게시물 검색

GET /search/posts?keyword={검색어}&page={페이지}&size={크기}

처리 과정 :

  • 검색 범위: 제목(title) + 내용(content)
  • 상태 필터: 삭제되지 않은 게시물만 검색
  • 커스텀 쿼리: @Query를 통한 복합 검색 조건
  • DTO 변환: PostResponseDto.from(post)

검색 키워드 통계 시스템

SearchKeyword 엔티티 구조

@Entity
public class SearchKeywordGym extends BaseEntity {
    private Long id;                    // 키워드 ID
    private String keyword;             // 검색 키워드 (unique)
    private int count;                  // 검색 횟수
    private LocalDateTime createdAt;    // 최초 검색일
    private LocalDateTime updatedAt;    // 최근 검색일
}

키워드 통계 처리 로직

public void saveSearchKeywordGym(String keyword) {
    searchGymRepository.findByKeyword(keyword)
        .ifPresentOrElse(
            SearchKeywordGym::increaseCount,    // 기존: 카운트 +1
            () -> searchGymRepository.save(     // 신규: 새로 생성
                new SearchKeywordGym(keyword)
            )
        );
}

키워드별 독립 관리

  • SearchKeywordGym: 헬스장 검색 키워드
  • SearchKeywordTrainer: 트레이너 검색 키워드
  • SearchKeywordPost: 게시물 검색 키워드
  • 각각 독립적인 테이블과 통계 관리

캐싱 시스템

캐시 설정 (CacheConfig)

@Bean
@Primary
public CacheManager cacheManager() {
    ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
    cacheManager.setCacheNames(Arrays.asList(
        "gymSearch",         // 헬스장 검색 캐시
        "postSearch",        // 게시물 검색 캐시
        "trainerSearch",     // 트레이너 검색 캐시
        "popularKeywords"    // 인기 키워드 캐시
    ));
    return cacheManager;
}

캐시 키 생성 전략

헬스장 검색 (커스텀 키 생성기)

@Bean("customKeyGenerator")
public KeyGenerator customKeyGenerator() {
    return (target, method, params) -> {
        String keyword = (String) params[0];
        Pageable pageable = (Pageable) params[1];
        
        return "keyword:" + keyword +
               ":page:" + pageable.getPageNumber() +
               ":size:" + pageable.getPageSize();
    };
}

트레이너/게시물 검색 (SpEL 표현식)

@Cacheable(
    value = "trainerSearch",
    key = "#keyword + '_' + (#pageable != null ? #pageable.pageNumber : 0) + '_' + (#pageable != null ? #pageable.pageSize : 20)"
)

캐시 적용 효과

  • 첫 번째 검색: 데이터베이스 쿼리 실행 + 결과 캐싱
  • 동일 검색: 캐시에서 즉시 반환 (성능 향상)
  • 메모리 기반: 애플리케이션 재시작 시 캐시 초기화

데이터베이스 쿼리 최적화

헬스장 검색 쿼리

SELECT g FROM Gym g 
WHERE g.name LIKE %:keyword%
ORDER BY g.id
LIMIT :size OFFSET :offset

게시물 검색 쿼리 (복합 조건)

SELECT p FROM Post p 
WHERE p.postStatus <> 'DELETED' 
  AND (p.title LIKE %:keyword% OR p.content LIKE %:keyword%)
ORDER BY p.id
LIMIT :size OFFSET :offset

트레이너 검색 쿼리

SELECT t FROM Trainer t 
WHERE t.name LIKE %:keyword%
ORDER BY t.id
LIMIT :size OFFSET :offset

API 명세

헬스장 검색

MethodEndpoint설명권한GET/search/gyms/v1헬스장 검색PUBLIC

트레이너 검색

MethodEndpoint설명권한GET/search/trainers트레이너 검색PUBLIC

게시물 검색

MethodEndpoint설명권한GET/search/posts게시물 검색PUBLIC

공통 요청 파라미터

파라미터 타입 필수 설명 기본값
keyword String 선택 검색할 키워드 -
page int 선택 페이지 번호 (0부터 시작) 0
size int 선택 페이지당 항목 수 20

응답 형식

{
  "statusCode": 200,
  "message": "검색이 성공적으로 완료되었습니다.",
  "data": {
    "content": [
      {
        "id": 1,
        "name": "검색된 항목명",
        "description": "상세 정보"
      }
    ],
    "pageable": {
      "pageNumber": 0,
      "pageSize": 20
    },
    "totalElements": 150,
    "totalPages": 8,
    "first": true,
    "last": false
  }
}

검색 생명주기

검색 요청 ─→ 키워드 통계 저장
    │              │
    ▼              ▼
캐시 확인 ─→ 캐시 히트? ─→ 즉시 반환
    │              │
    ▼              ▼
캐시 미스 ─→ 데이터베이스 검색
    │              │
    ▼              ▼
결과 캐싱 ─→ 클라이언트 응답
    │              │
    ▼              ▼
통계 업데이트 ─→ 검색 완료

에러 처리 및 예외 상황

검색 실패 처리

  • 빈 키워드: 빈 결과 반환 (에러 X)
  • 페이징 오류: 기본값 적용 (page=0, size=20)
  • 데이터베이스 오류: 캐시된 이전 결과 반환 (가능한 경우)

캐시 장애 처리

  • 캐시 실패: 데이터베이스 직접 조회로 폴백
  • 메모리 부족: 자동 캐시 정리 (LRU 방식)
  • 캐시 일관성: 애플리케이션 재시작 시 자동 초기화

성능 최적화 방안

현재 구현된 최적화

  • 메모리 캐싱: 동일 검색어 반복 요청 최적화
  • 페이징 처리: 대용량 데이터 효율적 처리
  • 인덱스 활용: name 컬럼 인덱스 자동 생성

추가 최적화 가능 영역

  • Full-Text Search: MySQL FULLTEXT 인덱스 도입
  • ElasticSearch: 대용량 검색 성능 향상
  • Redis 캐싱: 분산 캐시로 확장
  • 검색어 자동완성: Trie 자료구조 활용

사용 예시

1. 헬스장 검색

// 요청
GET /search/gyms/v1?keyword=피트니스&page=0&size=10

// 응답
{
  "data": {
    "content": [
      {
        "id": 1,
        "name": "스마트 피트니스",
        "address": "서울시 강남구",
        "phoneNumber": "010-1234-5678"
      }
    ],
    "totalElements": 25
  }
}

2. 통합 검색 시나리오

// 1. 헬스장 검색
searchGymService.searchGym("헬스", pageable);

// 2. 해당 헬스장의 트레이너 검색  
searchTrainerService.searchTrainer("김코치", pageable);

// 3. 관련 게시물 검색
searchPostService.searchPost("다이어트", pageable);

주요 특징 요약

통합성

  • 3개 도메인 통합 검색 지원
  • 일관된 API 인터페이스
  • 공통 페이징 처리

성능

  • Redis 메모리 캐싱
  • 효율적인 키 생성 전략
  • 페이징을 통한 대용량 처리

분석

  • 검색 키워드 통계 수집
  • 도메인별 독립적 통계 관리
  • 인기 검색어 분석 기반 구축

확장성

  • 새로운 검색 대상 쉽게 추가
  • 캐시 전략 도메인별 커스터마이징
  • 검색 알고리즘 개선 여지
⚠️ **GitHub.com Fallback** ⚠️