Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REFACTOR] 검색 디렉토리 분리 및 쿼리 리팩토링 #341

Open
wants to merge 24 commits into
base: main
Choose a base branch
from

Conversation

ayoung-dev
Copy link
Collaborator

resolved :

📌 과제 설명

  • 커뮤니티 게시글/봉사활동 모집글 검색 기능 분리
  • 검색 쿼리 리팩토링

👩‍💻 요구 사항과 구현 내용

  • 검색 기능이 포함된 community/recruitboard 기능 search 디렉토리로 이동

  • elastic search 서버 연결 O -> elastic search 검색 / 서버 연결 X -> rdb 검색

  • communityBoardResponseDto 필드에 맞게 CommunityDocument 필드 추가

  • recruitBoardDetailResponseDto, recruitBoardWithCenterResponseDto 필드에 맞게 RecruitBoardDocument 필드 추가

  • findAll() 쿼리 -> findAllByDeletedFalse() 로 변경 (elastic search index 저장 시 deleted = true인 필드 제외)

  • center id로 기관명 조회하는 쿼리 추가 (elastic search index 저장 시 사용)

  • 기존) @query 어노테이션으로 elastic search 검색 -> 동적 쿼리로 검색

  • 기존) elastic search index에서 반환값으로 id를 list로 받아와 rdb에서 재검색 -> responseDto 필드에 맞게 반환값 변경

  • 위치 검색 elastic search의 geo_point(지리 공간 분석) 쿼리로 리팩토링

  • 유사어(오타) 검색 허용 쿼리 추가

  • 검색에 가중치 쿼리 추가 (현재 제목, 본문, 닉네임(기관명) 순서대로 가중치)

✅ PR 포인트 & 궁금한 점

  • 현재 controller에서 elastic search 연결 상태 확인하고 각각 rdb 검색, elastic 검색 service랑 연결하는 형식으로 되어 있습니다.
    elastic 검색은 모두 search 디렉토리에, rdb 검색은 controller는 search에 service단 밑으로는 각 communityboard, recruitboard 디렉토리에 있습니다.
    최대한 범수님의 코드를 건드리지 않고, elastic search 검색과 구분하려는 목적이었는데 이렇게 해도 괜찮을지 의견 물어보고 싶어요.

  • recruit board의 경우 responseDTO에 담기는 정보가 많아 고민을 했는데 모든 정보를 elastic search index에 저장하고 결과 바로 반환 vs. elastic search에는 최소한의 정보만 담고 나머지는 rdb에서 한 번 더 검색 중 고민을 해봤는데
    모두 저장할 경우 빠른 시간, 대신 비용 많이 듬 / 후자는 비용은 줄어들지만 한 번 더 검색해야해서 시간이 오래 걸림.
    어차피 local에서만 elastic search 사용하기 때문에 responseDTO에 담기는 정보들 모두 elastic search index에 저장하였습니다.

  • 기존 @query 어노테이션 -> 동적 쿼리 작성 / query 작성 후 nativeQuery로 한 번 더 감싸서 쿼리 날리는 형식으로 되어있습니다!

  • 코드가 너무 쌓여서 새로 바뀐 user로의 리팩토링은 브런치 새로 파서 진행하겠습니다

- elastic search enabled = true -> elastic search 검색
- elastic search enabled = false -> rdb 검색
- elastic search healthcheck를 통한 서버 연결 확인 기능 추가
- elastic search enabled = true -> test 실행 (local에서 테스트 시 사용 예정)
- elastic search enabled = false -> test ignore
- elastic search 서버 연결 여부에 따른 분기 추가
- writerNickname, createdAt 필드 추가
- elastic search 검색 조건에 writerNickname 및 가중치 추가
- ResponseDto에 fromDocument 추가
- 기존) elastic search에서 검색 결과 글 id 반환 -> id로 rdb에서 검색 후 communityBoardView 반환
  수정) elastic search 검색 결과인 communityDocument 전체 반환
- elasticsearch 저장을 위한 전체 게시글 조회 메서드 변경
- RecruitBoardWithCenterResponseDto, RecruitBoardDetailResponseDto에 포함되는 필드 모두 추가
- RecruitBoardWithCenterResponseDto, RecruitBoardDetailResponseDto에 fromDocument 추가
- elasticsearch index 저장을 위한 전체 데이터 조회 시 deleted 조건 추가
- 기존) elastic search에서 검색 결과 글 id 반환 -> id로 rdb에서 검색 후 반환
  수정) elastic search 검색 결과인 recruitBoardDocument 전체 반환
- 근처 모집글 검색의 경우 elastic search의 geo_point로 매핑하여 검색하도록 리팩토링
- 유사어 검색 쿼리 추가
- 특정 기관 모집글 조회 RecruitBoardQueryApiController로 이동 (추후 개발 후 다시 이동)
@ayoung-dev ayoung-dev self-assigned this Jan 27, 2025
@ayoung-dev ayoung-dev linked an issue Jan 27, 2025 that may be closed by this pull request
2 tasks
github-actions[bot]

This comment was marked as resolved.

@github-actions github-actions bot changed the title [Refactor] 검색 디렉토리 분리 및 쿼리 리팩토링 [BUILD FAIL] [Refactor] 검색 디렉토리 분리 및 쿼리 리팩토링 Jan 27, 2025
@github-actions github-actions bot closed this Jan 27, 2025
@ayoung-dev ayoung-dev reopened this Jan 27, 2025
github-actions[bot]

This comment was marked as resolved.

@github-actions github-actions bot changed the title [BUILD FAIL] [Refactor] 검색 디렉토리 분리 및 쿼리 리팩토링 [BUILD FAIL] [BUILD FAIL] [Refactor] 검색 디렉토리 분리 및 쿼리 리팩토링 Jan 27, 2025
@github-actions github-actions bot closed this Jan 27, 2025
@ayoung-dev ayoung-dev reopened this Jan 27, 2025
@ayoung-dev ayoung-dev changed the title [BUILD FAIL] [BUILD FAIL] [Refactor] 검색 디렉토리 분리 및 쿼리 리팩토링 [Refactor] 검색 디렉토리 분리 및 쿼리 리팩토링 Jan 27, 2025
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
76.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@ayoung-dev ayoung-dev changed the title [Refactor] 검색 디렉토리 분리 및 쿼리 리팩토링 [REFACTOR] 검색 디렉토리 분리 및 쿼리 리팩토링 Jan 27, 2025
Copy link
Collaborator

@m-a-king m-a-king left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

낯선 코드가 많이 보이네요. 고생하셨습니다 ㅠㅠ

파일 체인지와 커밋을 전전하며 리뷰를 작성하느라 올바르지 않은 피드백이 있을 수 있는데 확인 부탁드리겠습니다!

recruit board의 경우 responseDTO에 담기는 정보가 많아 고민을 했는데 모든 정보를 elastic search index에 저장하고 결과 바로 반환 vs. elastic search에는 최소한의 정보만 담고 나머지는 rdb에서 한 번 더 검색 중 고민을 해봤는데
모두 저장할 경우 빠른 시간, 대신 비용 많이 듬 / 후자는 비용은 줄어들지만 한 번 더 검색해야해서 시간이 오래 걸림.
어차피 local에서만 elastic search 사용하기 때문에 responseDTO에 담기는 정보들 모두 elastic search index에 저장하였습니다.

기존 @query 어노테이션 -> 동적 쿼리 작성 / query 작성 후 nativeQuery로 한 번 더 감싸서 쿼리 날리는 형식으로 되어있습니다!

이 부분은 제가 엘라스틱 서치를 학습하지 않아서 이해가 잘 안되는데, 조금 더 자세히 설명을 들을 수 있을까요?

Comment on lines +80 to +92
private <T> Optional<T> findDynamicField(UUID id, Path<T> field) {

return Optional.ofNullable(
queryFactory
.select(field)
.from(center)
.where(
center.id.eq(id),
isNotDeleted()
)
.fetchOne()
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋아요!
ByExp 혹은 ByCenterId 를 메서드 이름 뒤에 붙이는 건 어떨까요?

사실 저번에 동적 쿼리를 위한 헬퍼 메서드 만들다가 조건 개수나 정렬, 페이징 같은 것 때문에 시간이 부족해서 포기했었는데 이번 기회에 조금 더 생각해 볼 수 있을 것 같습니다~

public boolean isElasticsearchRunning() {
try {
HealthResponse healthResponse = elasticsearchClient.cluster().health();
return healthResponse.status() != HealthStatus.Red;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

진짜 별 것 아닌데, != red 대신 == green도 가능하다면 조금 더 가독성이 좋을 것 같아요! 어떻게 생각하시나요??

Comment on lines +30 to +50
@GetMapping("/community-boards/search")
@Operation(summary = "커뮤니티 게시글 키워드 검색", description = "키워드를 포함한 커뮤니티 게시글 목록을 조회합니다.")
public ApiResponse<Page<CommunityBoardResponseDto>> getCommunityBoardsBySearch(
@RequestParam String keyword,
Pageable pageable
) {
if (elasticsearchHealthChecker.isElasticsearchRunning()) {
return ApiResponse.ok(
200,
communityBoardDocumentUseCase.get().getCommunityBoardBySearch(keyword, pageable.getPageNumber()),
"커뮤니티 게시글 검색 리스트 조회 성공"
);
} else {
return ApiResponse.ok(
200,
communityBoardQueryUseCase.getCommunityBoards(keyword, pageable.getPageNumber()),
"커뮤니티 게시글 검색 리스트 조회 성공"
);
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config를 활용해서 전략패턴으로 구현체를 결정시키는 방법은 어떨까요?

혹시 런타임에 동적으로 health 상태가 변경될 search service에 대응하려면, 컨트롤러와 서비스 사이에 하나의 계층을 더 구분하거나, 현 상태로도 괜찮을 것 같습니다!

현 상태로 이어가려면 early return 구조이므로 else가 없어도 될 것 같습니다!

@ConditionalOnProperty(name = "elastic.search.enabled", havingValue = "true")
public interface CommunityBoardDocumentRepository extends ElasticsearchRepository<CommunityBoardDocument, Long> {
List<CommunityBoardDocument> findAll();
@Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"title^3\", \"content^2\", \"writerNickname\"], \"fuzziness\": \"AUTO\"}}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Query("{\"multi_match\": {\"query\": \"?0\", \"fields\": [\"title^3\", \"content^2\", \"writerNickname\"], \"fuzziness\": \"AUTO\"}}")
@Query("""
{
"multi_match": {
"query": "?0",
"fields": ["title^3", "content^2", "writerNickname"],
"fuzziness": "AUTO"
}
}
""")

이런 방법은 어떨까요? 혹시 문제가 생기는지 여쭤보고 싶습니다~


import java.util.List;

@ConditionalOnProperty(name = "elastic.search.enabled", havingValue = "true")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엘라스틱 서치 활성화 확인을 위한 어노테이션을 커스텀 해보시는 건 어떤지 제안드려요~

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

역경이 느껴지네요

Comment on lines +23 to +30
@Scheduled(cron = "${spring.schedules.cron.updateCommunityBoardDocuments}")
public void updateCommunityBoardDocuments() {
List<CommunityBoard> communityBoards = communityBoardQueryUseCase.getAllCommunityBoards();

if (elasticsearchHealthChecker.isElasticsearchRunning()) {
communityBoardDocumentUseCase.saveCommunityBoardDocuments(communityBoards);
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헬스 체크가 앞으로 오도록 리팩토링하면 더 좋을 것 같아요!

어노테이션을 활용한 컨디션 체크와 의존하는 헬스 체커를 활용한 체크는 이중 확인이라고 생각하면 될까요?

private static BooleanExpression isNotDeleted() {
return center.deleted.isFalse();
}

private <T> Optional<T> findDynamicField(UUID id, Path<T> field) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 신기하네요 생각하지 못했던 방법이라 감탄했습니다

communityBoardDocumentUseCase.saveCommunityBoardDocuments(communityBoards);
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개행 표시가 나옵니다!

return boards.map(CommunityBoardResponseDto::fromDocument);
}

@Transactional
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@transactional 구분해주신거 좋은거 같아요

Copy link
Collaborator

@7zrv 7zrv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

꼼꼼하게 작성하신게 느껴집니다 고생하셨습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[REFACTOR] 검색 도메인 분리 및 Qualifier 적용
3 participants