Skip to content

Conversation

@kckc0608
Copy link
Member

작업한 내용

  • 공고 목록 매칭 카운트 로직 수정

관련 이슈

close #491

참고 사항

@kckc0608 kckc0608 self-assigned this Nov 26, 2025
@kckc0608 kckc0608 added the refactor 리팩토링 label Nov 26, 2025
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @kckc0608, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 공고 현황 목록에서 매칭 및 지원자 수를 계산하는 로직을 리팩토링합니다. 기존에는 JPA Repository의 JPQL 쿼리 내에서 직접 카운트를 수행했지만, 이제는 Repository는 순수하게 Recruitment 엔티티를 조회하고, 서비스 계층에서 조회된 엔티티를 기반으로 ApplicationRepositoryWorkApplicationRepository를 활용하여 매칭 및 지원자 수를 계산하도록 변경되었습니다. 이는 데이터 조회와 비즈니스 로직의 분리를 명확히 하고, 향후 로직 변경에 대한 유연성을 높이기 위함입니다.

Highlights

  • 매칭 카운트 로직 변경: 공고 목록의 매칭 및 지원자 수 계산 로직이 JPA Repository의 JPQL 쿼리에서 서비스 계층으로 이동되었습니다.
  • Repository 쿼리 간소화: RecruitmentRepository의 쿼리들이 SocialWorkerRecruitmentResponse DTO를 직접 반환하는 대신 Recruitment 엔티티를 반환하도록 변경되었으며, COUNTGROUP BY 절이 제거되었습니다.
  • 서비스 계층에서 카운트 계산: SocialWorkerMatchingServiceElderlyService에서 ApplicationRepository의 새로운 countByRecruitment 메서드와 WorkApplicationRepository를 사용하여 지원자 수와 매칭 수를 직접 계산하도록 수정되었습니다.
  • DTO 구조 개선: SocialWorkerRecruitmentResponse DTO의 생성자 대신 정적 팩토리 메서드 of를 도입하여 객체 생성 방식을 개선했습니다.
  • 테스트 코드 업데이트: 변경 된 로직에 맞춰 테스트 코드가 업데이트되었으며, 특히 매칭 및 지원자 수에 대한 검증 로직이 추가되었습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

이 PR은 공고에 대한 매칭 및 지원자 수 계산 로직을 리팩토링합니다. 로직이 데이터베이스 쿼리에서 서비스 계층으로 이동되었습니다. 이러한 변경은 유연성을 높일 수 있지만, 현재 구현은 SocialWorkerMatchingServiceElderlyService에서 N+1 쿼리로 인해 심각한 성능 문제를 야기합니다. 각 공고에 대해 여러 데이터베이스 쿼리가 실행되어, 특히 공고가 많은 페이지에서 성능 저하를 초래할 것입니다. 이러한 성능 문제를 해결해야 합니다. 또한, RecruitmentRepository에 메서드의 반환 타입이 쿼리 결과와 일치하지 않는 잠재적인 버그가 있습니다.

@kckc0608
Copy link
Member Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

이번 PR은 공고 목록의 매칭 및 지원자 수 계산 로직을 서비스 계층으로 이동하여 리팩토링했습니다. 이를 통해 기존에 matchingCount가 잘못 계산되던 버그를 수정한 점은 매우 좋습니다. 테스트 코드 또한 새로운 로직에 맞게 잘 보강되었습니다.

다만, 새로운 구현 방식이 SocialWorkerMatchingServiceElderlyService에서 N+1 쿼리 문제를 발생시키는 것으로 보입니다. 각 공고마다 지원자 수를 계산하기 위해 개별적으로 쿼리를 실행하고 있어, 데이터가 많아질 경우 심각한 성능 저하를 유발할 수 있습니다. 이 문제에 대한 구체적인 해결 방안을 각 파일에 대한 리뷰 코멘트로 남겼으니 확인 부탁드립니다.

추가적으로, 자동 매칭 수(matchingCount)를 계산하는 로직이 메모리 상에서 모든 WorkApplication을 순회하며 수행되는데, WorkApplication 데이터가 많아질 경우 이 부분도 성능에 영향을 줄 수 있습니다. 이 점도 추후 개선을 고려해보시면 좋겠습니다.

Comment on lines +97 to +109
List<WorkApplication> workApplications = workApplicationRepository.findAllActiveWorkApplication();

return recruitmentRepository
.findAllByInstitutionAndRecruitmentStatusIn(
socialworker.getNursingInstitution(), recruitmentStatus, pageable)
.map(recruitment -> {
long applicationCount = applicationRepository.countByRecruitment(recruitment);
long matchingCount = workApplications.stream()
.filter(workApplication -> matchingDomainService.isMatched(workApplication, recruitment))
.count();

return SocialWorkerRecruitmentResponse.of(recruitment, applicationCount, matchingCount);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

high

현재 구현은 recruitment 목록을 순회하며 각 recruitment에 대해 applicationRepository.countByRecruitment(recruitment)를 호출하고 있습니다. 이는 페이지에 포함된 공고 수만큼 추가 쿼리를 발생시키는 N+1 문제를 유발하여 성능 저하의 원인이 될 수 있습니다. 예를 들어, 페이지 크기가 20이라면 1개의 공고 목록 쿼리와 20개의 지원자 수 카운트 쿼리가 실행되어 총 21개의 쿼리가 발생합니다.

searchRecruitmentList 메서드(128-138 라인)에서도 동일한 문제가 발견되었습니다.

이 문제를 해결하기 위해, 공고 목록을 가져온 후 해당 공고들에 대한 지원자 수를 한 번의 쿼리로 가져오는 것을 권장합니다.

ApplicationRepository에 다음과 같은 메서드를 추가할 수 있습니다:

@Query("SELECT a.recruitment.id, count(a) FROM Application a WHERE a.recruitment IN :recruitments GROUP BY a.recruitment.id")
Map<Long, Long> countMapByRecruitmentIn(List<Recruitment> recruitments);

그리고 서비스 로직을 다음과 같이 수정할 수 있습니다:

// 예시 코드
Page<Recruitment> recruitmentPage = recruitmentRepository
        .findAllByInstitutionAndRecruitmentStatusIn(
                socialworker.getNursingInstitution(), recruitmentStatus, pageable);
List<Recruitment> recruitments = recruitmentPage.getContent();

if (recruitments.isEmpty()) {
    return Page.empty();
}

Map<Long, Long> applicationCounts = applicationRepository.countMapByRecruitmentIn(recruitments);
List<WorkApplication> workApplications = workApplicationRepository.findAllActiveWorkApplication();

return recruitmentPage.map(recruitment -> {
    long applicationCount = applicationCounts.getOrDefault(recruitment.getId(), 0L);
    long matchingCount = workApplications.stream()
            .filter(workApplication -> matchingDomainService.isMatched(workApplication, recruitment))
            .count();

    return SocialWorkerRecruitmentResponse.of(recruitment, applicationCount, matchingCount);
});

이렇게 하면 데이터베이스 호출을 크게 줄여 성능을 개선할 수 있습니다.

Comment on lines +168 to +177
List<SocialWorkerRecruitmentResponse> responses = recruitmentRepository.findAllByElderly(elderly).stream()
.map(recruitment -> {
long applicationCount = applicationRepository.countByRecruitment(recruitment);
long matchingCount = workApplications.stream()
.filter(workApplication -> matchingDomainService.isMatched(workApplication, recruitment))
.count();

return SocialWorkerRecruitmentResponse.of(recruitment, applicationCount, matchingCount);
})
.toList();
Copy link
Contributor

Choose a reason for hiding this comment

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

high

이 부분에서도 recruitment 목록을 순회하며 각 recruitment에 대해 applicationRepository.countByRecruitment(recruitment)를 호출하고 있어 N+1 문제가 발생합니다. 한 어르신에게 여러 공고가 있을 수 있으므로, 불필요한 데이터베이스 조회가 여러 번 발생할 수 있습니다.

SocialWorkerMatchingService에서 제안된 것과 유사하게, 공고 목록에 대한 지원자 수를 한 번의 쿼리로 가져와서 처리하는 방식으로 리팩토링하는 것을 권장합니다.

// 예시 코드
List<Recruitment> recruitments = recruitmentRepository.findAllByElderly(elderly);
if (recruitments.isEmpty()) {
    return List.of();
}

// ApplicationRepository에 추가적인 메서드가 필요합니다.
Map<Long, Long> applicationCounts = applicationRepository.countMapByRecruitmentIn(recruitments);
List<WorkApplication> workApplications = workApplicationRepository.findAllActiveWorkApplication();

List<SocialWorkerRecruitmentResponse> responses = recruitments.stream()
        .map(recruitment -> {
            long applicationCount = applicationCounts.getOrDefault(recruitment.getId(), 0L);
            long matchingCount = workApplications.stream()
                    .filter(workApplication -> matchingDomainService.isMatched(workApplication, recruitment))
                    .count();

            return SocialWorkerRecruitmentResponse.of(recruitment, applicationCount, matchingCount);
        })
        .toList();

Copy link
Member Author

Choose a reason for hiding this comment

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

@gemini-code-assist 제안한 구현 방법은 에러가 납니다. data repository 에서 두개 id 를 select 하면 내부적으로 이를 튜플에 담기 때문에 Map<String, Object[]> 형태로 뱉어냅니다. Map<Long, Long> 형식으로 두면 이를 캐스팅하는 과정에서 에러가 나요.
하지만 수정 방향에는 동의합니다. 별도 이슈에서 수정하겠습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

안녕하세요, kckc0608님. 제안해주신 피드백 감사합니다. Map<Long, Long> 캐스팅 관련하여 지적해주신 부분이 맞습니다. Spring Data JPA의 쿼리 결과 처리 방식에 따라 Map<String, Object[]> 형태로 반환될 수 있으며, 이 경우 직접적인 Map<Long, Long>으로의 캐스팅은 오류를 발생시킬 수 있습니다.N+1 문제를 해결하려는 방향에 동의해주셔서 감사합니다. 별도 이슈로 해당 부분을 개선해주시면 좋을 것 같습니다. 더 효율적인 데이터 조회를 위한 리팩토링은 성능 개선에 큰 도움이 될 것입니다.

@kckc0608 kckc0608 merged commit 51e3cd7 into develop Nov 26, 2025
1 check passed
@kckc0608 kckc0608 mentioned this pull request Nov 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

♻️ 매칭 건수 조회 로직 작성

2 participants