diff --git a/src/main/java/com/piuda/callcare/domain/druginfo/service/query/DrugSearchQueryService.java b/src/main/java/com/piuda/callcare/domain/druginfo/service/query/DrugSearchQueryService.java index 6643627..a1532f4 100644 --- a/src/main/java/com/piuda/callcare/domain/druginfo/service/query/DrugSearchQueryService.java +++ b/src/main/java/com/piuda/callcare/domain/druginfo/service/query/DrugSearchQueryService.java @@ -8,12 +8,15 @@ import com.piuda.callcare.global.exception.CallCareException; import com.piuda.callcare.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataAccessException; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +@Slf4j @Service @Transactional(readOnly = true) @RequiredArgsConstructor @@ -23,15 +26,27 @@ public class DrugSearchQueryService { private final DrugInfoQueryService drugInfoQueryService; private final DrugInfoConverter drugInfoConverter; - // Elasticsearch 약품명 검색 (자동완성·오타·중간 단어·초성 통합, 관련도 순, 최대 20건) + /** + * Elasticsearch 약품명 검색 (자동완성·오타·중간 단어·초성 통합, 관련도 순, 최대 20건). + *

+ * ES 연결 실패·타임아웃 등 저장소 접근 예외({@link DataAccessException})가 나면 + * MySQL {@code LIKE} 검색(폴백)으로 자동 전환한다. 파라미터 검증 실패({@link CallCareException})는 + * 이 catch 대상이 아니므로 그대로 400으로 전파된다. + */ public List search(String keyword) { if (keyword == null || keyword.isBlank()) { throw new CallCareException(ErrorCode.INVALID_PARAMETER); } - return drugSearchRepository.searchByItemName(keyword.trim(), PageRequest.of(0, 20)) - .stream() - .map(drugInfoConverter::toSearchResponse) - .toList(); + String trimmed = keyword.trim(); + try { + return drugSearchRepository.searchByItemName(trimmed, PageRequest.of(0, 20)) + .stream() + .map(drugInfoConverter::toSearchResponse) + .toList(); + } catch (DataAccessException e) { + log.warn("ES 약품 검색 실패 - MySQL 폴백으로 전환. keyword={}", trimmed, e); + return drugInfoQueryService.searchByKeyword(trimmed); + } } // 선택한 약의 자동 입력 데이터 반환 (drugName, drugType, memo 자동 생성) diff --git a/src/test/java/com/piuda/callcare/domain/druginfo/service/query/DrugSearchQueryServiceTest.java b/src/test/java/com/piuda/callcare/domain/druginfo/service/query/DrugSearchQueryServiceTest.java new file mode 100644 index 0000000..d4fa93e --- /dev/null +++ b/src/test/java/com/piuda/callcare/domain/druginfo/service/query/DrugSearchQueryServiceTest.java @@ -0,0 +1,90 @@ +package com.piuda.callcare.domain.druginfo.service.query; + +import com.piuda.callcare.domain.druginfo.converter.DrugInfoConverter; +import com.piuda.callcare.domain.druginfo.document.DrugDocument; +import com.piuda.callcare.domain.druginfo.dto.response.DrugSearchResponse; +import com.piuda.callcare.domain.druginfo.repository.DrugSearchRepository; +import com.piuda.callcare.global.exception.CallCareException; +import com.piuda.callcare.global.exception.ErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.data.domain.PageRequest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +@DisplayName("DrugSearchQueryService 단위 테스트 — ES 장애 시 MySQL 자동 폴백") +class DrugSearchQueryServiceTest { + + @InjectMocks + private DrugSearchQueryService drugSearchQueryService; + + @Mock private DrugSearchRepository drugSearchRepository; + @Mock private DrugInfoQueryService drugInfoQueryService; + @Mock private DrugInfoConverter drugInfoConverter; + + @Test + @DisplayName("정상 케이스: ES가 정상이면 MySQL 폴백을 타지 않는다") + void search_uses_es_when_healthy() { + DrugDocument document = DrugDocument.builder().itemSeq("1").itemName("타이레놀").build(); + DrugSearchResponse response = new DrugSearchResponse("1", "타이레놀", null, null, null, null); + given(drugSearchRepository.searchByItemName(anyString(), any())).willReturn(List.of(document)); + given(drugInfoConverter.toSearchResponse(document)).willReturn(response); + + List result = drugSearchQueryService.search("타이레놀"); + + assertThat(result).containsExactly(response); + then(drugInfoQueryService).should(never()).searchByKeyword(anyString()); + } + + @Test + @DisplayName("장애 케이스: ES 접근 예외가 나면 MySQL LIKE 검색으로 폴백한다") + void search_falls_back_to_mysql_when_es_fails() { + DrugSearchResponse fallbackResponse = new DrugSearchResponse("1", "타이레놀", null, null, null, null); + given(drugSearchRepository.searchByItemName(anyString(), any())) + .willThrow(new DataAccessResourceFailureException("ES 연결 실패")); + given(drugInfoQueryService.searchByKeyword("타이레놀")).willReturn(List.of(fallbackResponse)); + + List result = drugSearchQueryService.search("타이레놀"); + + assertThat(result).containsExactly(fallbackResponse); + then(drugInfoQueryService).should().searchByKeyword("타이레놀"); + } + + @Test + @DisplayName("예외 케이스: 빈 키워드는 폴백 없이 즉시 INVALID_PARAMETER") + void search_throws_on_blank_keyword_without_fallback() { + assertThatThrownBy(() -> drugSearchQueryService.search(" ")) + .isInstanceOf(CallCareException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.INVALID_PARAMETER); + + then(drugSearchRepository).should(never()).searchByItemName(anyString(), any()); + then(drugInfoQueryService).should(never()).searchByKeyword(anyString()); + } + + @Test + @DisplayName("경계: 검색어 앞뒤 공백은 잘라서 폴백 서비스에도 그대로 전달된다") + void search_trims_keyword_before_fallback() { + given(drugSearchRepository.searchByItemName(anyString(), any())) + .willThrow(new DataAccessResourceFailureException("ES 연결 실패")); + given(drugInfoQueryService.searchByKeyword("타이레놀")).willReturn(List.of()); + + drugSearchQueryService.search(" 타이레놀 "); + + then(drugSearchRepository).should().searchByItemName("타이레놀", PageRequest.of(0, 20)); + then(drugInfoQueryService).should().searchByKeyword("타이레놀"); + } +}