검색 기능 (Search Service) - fitpassTeam/fitpass GitHub Wiki
키워드 기반 통합 검색 시스템으로 헬스장(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 인터페이스
대상 | 설명 | 검색 기준 | 페이징 기본값 |
---|---|---|---|
Gym | 헬스장 정보 | 헬스장 이름 (name ) |
20개 / 페이지 |
Trainer | 트레이너 정보 | 트레이너 이름 (name ) |
20개 / 페이지 |
Post | 게시물 정보 | 제목 (title ) + 내용 (content ) |
20개 / 페이지 |
검색 타입 | SQL 조건 | 설명 |
---|---|---|
부분 일치 | LIKE %keyword% |
키워드가 포함된 모든 결과 검색 |
대소문자 무시 | 기본 설정 | MySQL 기본 collation 활용 (예: utf8mb4_general_ci ) |
상태 필터링 | postStatus <> 'DELETED' |
삭제된 게시물 제외 |
GET /search/gyms/v1?keyword={검색어}&page={페이지}&size={크기}
처리 과정 :
-
Controller (
SearchGymController.searchGym
)- 검색 키워드 파라미터 수신
- 페이징 정보 처리 (기본: page=0, size=20)
- Service 계층 호출
-
Service (
SearchGymService
)-
키워드 통계 저장:
saveSearchKeywordGym(keyword)
- 기존 키워드 존재 시 → 카운트 증가
- 새로운 키워드 시 → 신규 생성 (count=1)
-
캐시 확인:
@Cacheable(cacheNames = "gymSearch")
-
데이터베이스 검색:
gymRepository.findByNameContaining(keyword, pageable)
-
DTO 변환:
Page<Gym>
→Page<GymResDto>
-
키워드 통계 저장:
-
Repository (
GymRepository
)- JPA 쿼리:
findByNameContaining
- 자동 페이징 처리
- JPA 쿼리:
GET /search/trainers?keyword={검색어}&page={페이지}&size={크기}
처리 과정:
- 헬스장 검색과 동일한 플로우
-
캐시 키:
keyword + '_' + pageNumber + '_' + pageSize
- 검색 기준: 트레이너 이름 부분 일치
-
DTO 변환:
TrainerResponseDto.fromEntity(trainer)
GET /search/posts?keyword={검색어}&page={페이지}&size={크기}
처리 과정 :
- 검색 범위: 제목(title) + 내용(content)
- 상태 필터: 삭제되지 않은 게시물만 검색
-
커스텀 쿼리:
@Query
를 통한 복합 검색 조건 -
DTO 변환:
PostResponseDto.from(post)
@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: 게시물 검색 키워드
- 각각 독립적인 테이블과 통계 관리
@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
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 자료구조 활용
// 요청
GET /search/gyms/v1?keyword=피트니스&page=0&size=10
// 응답
{
"data": {
"content": [
{
"id": 1,
"name": "스마트 피트니스",
"address": "서울시 강남구",
"phoneNumber": "010-1234-5678"
}
],
"totalElements": 25
}
}
// 1. 헬스장 검색
searchGymService.searchGym("헬스", pageable);
// 2. 해당 헬스장의 트레이너 검색
searchTrainerService.searchTrainer("김코치", pageable);
// 3. 관련 게시물 검색
searchPostService.searchPost("다이어트", pageable);
- 3개 도메인 통합 검색 지원
- 일관된 API 인터페이스
- 공통 페이징 처리
- Redis 메모리 캐싱
- 효율적인 키 생성 전략
- 페이징을 통한 대용량 처리
- 검색 키워드 통계 수집
- 도메인별 독립적 통계 관리
- 인기 검색어 분석 기반 구축
- 새로운 검색 대상 쉽게 추가
- 캐시 전략 도메인별 커스터마이징
- 검색 알고리즘 개선 여지