<aside> 💡 목차

</aside>

💥 기존 : 초기에 개발되어 있던 형태


스크린샷 2024-03-18 오후 2.00.28.png

매칭 로직 동작에 걸렸던 시간 : 513초

처음엔 프로젝트 마감 시간에 쫓겨, 평범하고 빠르게 만들었습니다. 만들고 나서 300명을 대상으로 테스트 했더니, 예상보다 성능이 너무 안나왔고 완성된 코드에서도 만족스럽지 않았습니다.

저희 앱은 이전 회차의 매칭이 마감되고 다시 신청자들을 매칭 시키는데 2시간이라는 은행 점검시간 개념의 매칭 점검 시간을 갖습니다.

팀 내에서는 점검 시간이 있기 때문에 매칭 로직이 조금 느려도 상관없다고 했지만, 저는 이것이 프로젝트의 장기적인 지속을 위해, 더 나은 방법이 있을지 항상 고민했습니다.

그래서 조금 더 시간을 내어, 알고리즘의 병목 지점을 식별하고, 데이터 처리 방식을 최적화하여, 제가 할 수 있는 범위 안에서 성능을 향상시킬 수 있는 방안을 찾기 시작했습니다.

제가 성능 향상을 위해 고민했던 과정을 아래에서 기술하고자 합니다.

🤝 [매칭 로직]

@Business
@RequiredArgsConstructor
public class MatchBusiness {
    private final MemberService memberService;
    private final ChatService chatService;
    private final MatchConverter matchConverter;
    private final MatchSaveBusiness matchSaveBusiness;
    private final ApplicantService applicantService;
    private static final int MAX_TRIES = 200;
    private Map<String, Queue<Participant>> languageQueuesWithCandidates = new HashMap<>();
    private int maxMatches = 3;
    private final List<String> languages =  List.of("KOREAN","ENGLISH","JAPANESE","CHINESE");
    private List<Participant> matchingParticipants;

    public void matchingAllApplicant() throws Exception {
        long start = System.currentTimeMillis();

        //신청자 Entities
        List<ApplicantEntity> applicantEntities = applicantService.findAllParticipants();

        //ApplicantEntity -> Participant 매칭에 사용되는 객채로 변환
        matchingParticipants = matchConverter.toParticipants(applicantEntities);

        //선호언호 별 Queue 에  Participant 추가
        languageQueuesWithCandidates = createNewLanguageQueuesWithCandidates();

        //제 1 선호 언어 Queue 를 통한 최대 3명 매칭
        matchParticipantsWithCandidatesWhoHasFirstPreferLanguages();

        //제 1 선호 언어 Queue 를 통해 모두 3명 매칭이 안되었을 경우
        //제 2 선호 언어 Queue 를 통해 남은 매칭을 진행
        if (!isAllMatched()) {matchParticipantsWithCandidatesWhoHasSecondPreferLanguages();}

        //제 1,2 선호 언어 Queue 를 통해 매칭을 시도했지만,
        // 아직 3명의 매칭이 안된 인원들에 대해서 랜덤 매칭
        if (!isAllMatched()) {matchParticipantsWhoHasLessThanThreeMatchesWithCandidatesWhoHasLessThanFourMatches();} //랜덤

        // 신청자는 두 종류로 나눠진다
        // ( 3명 매칭된 신청자, 선호언어가 맞지않아 3명 미만으로 매칭된 신청자)
        // 두 종류의 신청자를 랜덤 매칭하여 1인당 최소 3명, 최대 4명의 매칭이 되도록한다.
        matchParticipantsWithSpecialFriends();

        // 매칭 저장
        matchSaveBusiness.saveMatchingResult(matchingParticipants);

        long finish = System.currentTimeMillis();
        long timeMs = finish - start;
        System.out.println("매칭 인원 수 : " + applicantEntities.size() + "명");
        System.out.println("구 버전 매칭 로직의 동작에 걸린 시간 : " + timeMs + "ms");
    }

    public void matchParticipantsWithCandidatesWhoHasFirstPreferLanguages() {
        for (Participant participant : matchingParticipants) {
            Queue<Participant> candidates = languageQueuesWithCandidates.get(participant.getFirstPreferLanguage());
            matchParticipantWithCandidates(participant, candidates);
        }
    }

    public void matchParticipantsWithCandidatesWhoHasSecondPreferLanguages() {
        List<Participant> participants = getParticipantsWithLessThanNumberOfMatches(3);
        languageQueuesWithCandidates = createNewLanguageQueuesWithCandidates();
        for (Participant participant : participants) {
            Queue<Participant> candidates = languageQueuesWithCandidates.get(participant.getSecondPreferLanguage());
            matchParticipantWithCandidates(participant, candidates);
        }
        maxMatches++;
    }

    public void matchParticipantsWhoHasLessThanThreeMatchesWithCandidatesWhoHasLessThanFourMatches() {
        List<Participant> participants = getParticipantsWithLessThanNumberOfMatches(3);
        Queue<Participant> candidates = new LinkedList<>(getParticipantsWithLessThanNumberOfMatches(4));
        for (Participant participant : participants) {
            matchParticipantWithCandidates(participant, candidates);
        }
    }

    public void matchParticipantsWithSpecialFriends() {
        List<Participant> participants = getParticipantsWithLessThanNumberOfMatches(4);
        Queue<Participant> candidates = new LinkedList<>(participants);
        for (Participant participant : participants) {
            matchParticipantWithCandidates(participant, candidates);
        }
    }

    public void matchParticipantWithCandidates(Participant participant, Queue<Participant> candidates) {
        int tries = 0;
        while (participant.getNumberOfMatches() < maxMatches && tries < MAX_TRIES && !candidates.isEmpty()) {
            Participant partner = candidates.poll();
            tries++;
            if (isValidMatching(participant, partner)) {
                addMatching(participant, partner);
                if (partner.getNumberOfMatches() < maxMatches) {
                    candidates.add(partner);
                }
            } else {
                candidates.add(partner);
            }
        }
    }

    public void addMatching(Participant participant, Participant partner) {
        participant.addToMatchingList(new ServiceModelMatching(partner));
        partner.addToMatchingList(new ServiceModelMatching(participant ));
    }

    public Map createNewLanguageQueuesWithCandidates() {
        Map<String, Queue<Participant>> languageQueues = createLanguageQueues(languages);
        addParticipantToLanguageQueues(languageQueues);
        return languageQueues;
    }

    public Map<String, Queue<Participant>> createLanguageQueues(List<String> languages) {
        Map<String, Queue<Participant>> languageQueues = new HashMap<>();
        for (String language : languages) {
            languageQueues.put(language, new LinkedList<>());
        }
        return languageQueues;
    }

    public void addParticipantToLanguageQueues(Map<String, Queue<Participant>> languageQueues) {
        for (Participant participant : matchingParticipants) {
            String language = participant.getFirstPreferLanguage();
            if (!languageQueues.containsKey(language)) {
                languageQueues.put(language, new LinkedList<>());
            }
            languageQueues.get(language).add(participant);
        }
    }

    public List<Participant> getParticipantsWithLessThanNumberOfMatches(int numberOfMatches) {
        return matchingParticipants.stream()
                .filter(p -> p.getNumberOfMatches() < numberOfMatches)
                .collect(Collectors.toList());
    }

    public boolean isValidMatching(Participant participant, Participant matchedParticipant) {
        return matchedParticipant != participant &&
                !isServiceModelMatchigListContainsParticipantId(participant.getServiceModelMatchingList(), matchedParticipant.getId()) &&
                !isServiceModelMatchigListContainsParticipantId(matchedParticipant.getServiceModelMatchingList(), participant.getId()) &&
                matchedParticipant.getNumberOfMatches() < maxMatches;
    }

    public boolean isAllMatched() {
        return matchingParticipants.stream().allMatch(p -> p.getNumberOfMatches() >= 3);
    }

    private boolean isServiceModelMatchigListContainsParticipantId(List<ServiceModelMatching> serviceModelMatchingList, Long participantId){
        for(ServiceModelMatching serviceModelMatching : serviceModelMatchingList){
            if(serviceModelMatching.getPartner().getId() == participantId) return true;
        }

        return false;
    }
}

문제점 : 너무 긴 변수명


다른분이 작업하셨던 이전 매칭 도메인 코드를 보자마자, 너무 긴 변수명이 처음으로 눈에 띄었습니다.

물론 변수명으로 최대한 많은 정보를 주어 이해도를 향상시킬 수는 있겠지만, 너무 긴것 같아 다음 과 같은 문제점을 떠올렸습니다.(또한 너무 많은 코드라인 ! 😱)

문제점 : 객체지향적이지 않다.