티스토리 뷰

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

 

 

매칭 로직 동작에 걸렸던 시간 : 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;
    }
}

문제점 : 너무 긴 변수명


리펙토링 전, 매칭 도메인 코드를 보자마자 너무 긴 변수명이 처음으로 눈에 띄었습니다.

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

  • 가독성이 저하되었습니다.
  • 추후 타이핑하기 어려울 수 있습니다.
  • 긴 문장을 일일이 읽어봐야한다는 문제점이 있습니다.

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


단일 책임 원칙 위배

매칭 비즈니스 클래스가 너무 많은 책임을 지고 있습니다.

  • 매칭 로직과 매칭 결과 저장의 혼합
    • 매칭 로직을 처리하는 과정과 결과를 데이터베이스에 저장하는 로직이 같은 클래스내에 있습니다.
  • 언어별 큐를 직접 관리하고있습니다.
    • languageQueuesWithCandidates의 관리와 관련된 로직도 MatchBusiness 클래스에 포함되어 있습니다.
  • 스케쥴링을 책임지고 있습니다.
    • @Scheduled(cron = "0 0 23 ? * TUE,THU") 같은, 시간에 직접적으로 영향을 받는 메서드가 포함되어 있습니다.

캡슐화 부족

  • 자료를 직접적으로 조작
    • languageQueuesWithCandidates와 같은 데이터를 해당 객체 내부에서 처리하지 않고, 매칭 로직이 이를 직접 가져와서 처리하고 있습니다.
  • 접근제어자를 사용하지 않음
    • 외부로 노출될 필요가 없는 메서드까지 public으로 명시해놓았습니다.

상속과 다형성의 부족

1선호 언어 매칭, 2선호 언어 매칭, 랜덤 매칭 로직은 서로 비슷한 동작 방식을 가지고 있으므로 그룹화 할 수 있습니다. 다양한 전략으로 쉽게 교체할 수 있도록 MatchingType인터페이스를 도입하고 이를 구현하는 여러 클래스를 만드는 방식을 떠올렸습니다.

문제점 : 하드 코딩된 상수의 사용


현재 매칭 서비스에서 MAX_TRIES, maxMatches = 3, @Scheduled(cron = "0 0 23 ? * TUE,THU")와 같은 여러 하드 코딩된 상수들을 사용하고 있습니다. 위와 같은 상수는 매칭 시도의 최대 횟수, 최대 매칭 수, 매칭 작업의 스케줄링 등을 정의하는 데 사용되고 있었습니다.

유연성 부족, 유지보수의 어려움

  • 유연성 부족: 시스템이 다양한 운영 환경에 대응하기 위해서는 설정값의 변경이 필수적이지만, 현재의 하드 코딩된 상수 방식은 이러한 변경을 어렵게 만들수 있다고 판단했습니다.
  • 유지보수의 어려움: 추후에 이러한 값들을 변경해야 할 경우, 코드 내부를 직접 수정해야 하며, 이는 유지보수의 부담을 증가시킨다고 판단했습니다.

💾 [매칭 결과 저장 로직]

@Business
@RequiredArgsConstructor
public class MatchSaveBusiness {
    private final ChatService chatService;
    private final ApplicantService applicantService;
    private final MemberService memberService;
    private final MatchService matchService;
    private final ApplicantRepository applicantRepository;

    public void saveMatchingResult(List<Participant> participants) throws Exception {
        ApplicantEntity tmpApplicantEntity = applicantService.findByMemberEntity(memberService.findById(participants.get(0).getId()));
        Instant matchingDate = applicantService.getDateWillMatched(tmpApplicantEntity);

        for (Participant participant : participants) {
            // 신청자 엔티티
            ApplicantEntity nowApplicantEntity = applicantService.findByMemberEntity(memberService.findById(participant.getId()));

            // 신청자 매칭완료 상태변경
            applicantService.updateIsMatched(nowApplicantEntity);

            // 멤버 엔티티 매칭완료 상태변경
            memberService.updateMatched(nowApplicantEntity.getMemberEntity());

            // 매칭 객체리스트
            List<ServiceModelMatching> serviceModelMatchings = participant.getServiceModelMatchingList();

            // 매칭 주인으로 저장될 멤버엔티티
            MemberEntity matchingMemberEntity = memberService.findById(participant.getId());

            for (ServiceModelMatching serviceModelMatching : serviceModelMatchings) {
                // 매칭 파트너로 저장될 멤버엔티티
                MemberEntity matchedMemberEntity = memberService.findById(serviceModelMatching.getPartner().getId());

                // 매칭 파트너가 주인이고 매칭 주인이 파트너인 매칭 엔티티 조회
                Optional<MatchingEntity> partnerIsMasterMatchEntity = matchService.findByMatchedMemberAndMatchingMemberReverseWithMatchingDate(matchedMemberEntity,matchingMemberEntity,matchingDate);

                //매칭 파트너가 주인인 매칭 엔티티가 있을 경우,
                // 저장된 채팅룸으로 매칭 엔티티에 저장
                if(partnerIsMasterMatchEntity.isPresent()){
                    MatchingEntity matchingEntity = MatchingEntity.builder()
                            .matchingMember(matchingMemberEntity)
                            .matchedMember(matchedMemberEntity)
                            .matchingDate(matchingDate)
                            .chattingRoomEntity(partnerIsMasterMatchEntity.get().getChattingRoomEntity()).build();
                    matchService.save(matchingEntity);
                }

                //매칭 파트너가 주인인 매칭 엔티티가 없을 경우,
                // 새로운 채팅룸엔티티를 매칭 엔티티에 저장
                else{
                    ChattingRoomEntity chattingRoomEntity = ChattingRoomEntity.builder()
                            .status(ChattingRoomEntity.RoomStatus.OPEN)
                            .build();
                    ChattingRoomEntity chattingRoom = chatService.register(chattingRoomEntity);

                    MatchingEntity matchingEntity = MatchingEntity.builder()
                            .matchingMember(matchingMemberEntity)
                            .matchedMember(matchedMemberEntity)
                            .matchingDate(matchingDate)
                            .chattingRoomEntity(chattingRoom).build();
                    matchService.save(matchingEntity);
                }
            }
        }

        updateAllApplicantsIsMatchedToMatched();
    }

    private void updateAllApplicantsIsMatchedToMatched() {
        List<ApplicantEntity> allParticipants = applicantService.findAllParticipants();
        for(ApplicantEntity applicantEntity : allParticipants) {
            applicantEntity.updateIsMatched(ApplicantEntity.Status.MATCHED);
        }
        applicantRepository.saveAll(allParticipants);
    }
}

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

매칭 결과 저장 외에도, 다른 서비스와 상호작용 하고 있으며 저장에 필요한 여러 데이터를 다시 조회하고있습니다. 이는 단일 책임원칙에 위배된다고 판단했습니다.

  • applicantService.findByMemberEntity(memberService.findById(participants.get(0).getId()))를 이용해 매칭 신청 정보를 여러번 가져오고 있습니다.

그 외, 비즈니스 로직이 DB에 접근하여 데이터를 조회 하는 횟수가 많아 속도가 많이 느렸습니다.

매칭서비스가 채팅서비스와 강하게 결합되어있다.

매칭 서비스와 채팅 서비스 간에 강한 결합도가 존재하는 문제를 발견했습니다.

현재 구조에서 매칭 서비스는 매칭이 완료된 후, 채팅 서비스를 직접 호출하여 채팅방을 생성하고 있습니다. 이러한 방식은 두 서비스 간의 결합도를 높여 시스템의 유연성과 확장성을 저해하며, 한 서비스의 변경이 다른 서비스에 영향을 미치는 원인이 되고 있었습니다.

문제점 : 기본키 생성 비용

현재 매칭 도메인의 엔티티는 모두 @Id@GeneratedValue(strategy = GenerationType.IDENTITY)를 사용하여 기본키를 할당하는 방식을 가지고 있습니다.

IDENTITY 전략은 각 INSERT 연산 후 생성된 ID를 데이터베이스로부터 반환 받아야 하기 때문에, 추가적인 데이터베이스 라운드트립이 필요하게 됩니다. 이 과정에서 발생하는 오버헤드는 특히 대량의 데이터를 처리할 때 성능 저하의 원인이 될 수 있다고 판단했습니다.

✨ 리펙토링 : 이제 개선해보자

리펙토링 후, 개선된 매칭로직의 동작에 걸린 시간 : 106초

3분 26초라고 나와있는데, 테스트를 위해 필요한 인원 300명을 회원가입 하는 동작이 포함되어있기 때문입니다.

결과적으로,

매칭 로직 동작부터 매칭 결과 저장까지 걸리는 시간이

513초에서 106초로 감소하여 79.2%의 성능 향상이 있었습니다.

 

🤝 [매칭 로직]

@Component
public class MatchingBusiness {
    private final MatchingRuleProperties matchingRuleProperties;

    private MatchingTypeGroup matchingTypeGroup;
    private ParticipantGroup participantGroup;
    private LanguageQueue languageQueue;

    public MatchingBusiness(final MatchingRuleProperties matchingRuleProperties) {
		    ...
    }

    public void operateMatching(final MatchingOperateRequest matchingOperateRequest) {
        initialize(matchingOperateRequest);

        matchingTypeGroup.matchParticipants(participantGroup, languageQueue);
    }

    public List<Participant> getMatchedParticipants() {
        return participantGroup.getParticipants();
    }

    private void initialize(final MatchingOperateRequest matchingOperateRequest) {
        ...
    }
}

개선점 : 객체지향적 설계

책임의 분리

MatchingBusiness 클래스는 매칭 로직 처리, 결과 저장, 언어별 큐 관리, 스케줄링 등 다수의 책임을 지고 있었습니다. 이는 객체지향 설계 원칙 중 단일 책임 원칙(SRP)에 위배되는 형태였습니다.

리펙토링을 통해 MatchingTypeGroup, ParticipantGroup, LanguageQueue 등의 클래스를 도입하였습니다. 이는 각각의 클래스가 하나의 책임만을 지도록 하여, 매칭 프로세스의 다양한 측면을 더욱 명확하고 관리하기 쉬운 방식으로 분리하였습니다.

이제 MatchingBusiness 클래스는 다음과 같은 클래스를 가지게 되었습니다.

  • MatchingTypeGroup 
    • MatchingType개선점:
    • MatchingType 인터페이스와 이를 구현하는 여러 매칭 타입 클래스(FirstPreferLanguageType, SecondPreferLanguageType, RandomType, SpecialType)를 도입함으로써, 각 매칭 타입별 로직을 분리하고 다형성을 활용하여 구조의 유연성을 높였습니다. 이를 통해 새로운 매칭 타입의 추가나 기존 로직의 변경이 훨씬 용이해졌으며, 코드의 재사용성과 확장성이 개선되었습니다. 
public class MatchingTypeGroup {
    private final List<MatchingType> matchingTypes;

    private MatchingTypeGroup(final List<MatchingType> matchingTypes) {
        this.matchingTypes = matchingTypes;
    }

    public static MatchingTypeGroup init(final MatchingRuleProperties matchingRuleProperties) {
        return new MatchingTypeGroup(
                List.of(new FirstPreferLanguageType(matchingRuleProperties),
                        new SecondPreferLanguageType(matchingRuleProperties),
                        new RandomType(matchingRuleProperties),
                        new SpecialType(matchingRuleProperties)));
    }

    public void matchParticipants(ParticipantGroup participantGroup, LanguageQueue languageQueue) {
        matchingTypes.forEach(types -> types.doMatch(participantGroup, languageQueue));
    }
}

 

public interface MatchingType {
    void doMatch(ParticipantGroup participantGroup, LanguageQueue languageQueue);
}

 

개선점:

MatchingType 인터페이스와 이를 구현하는 여러 매칭 타입 클래스(FirstPreferLanguageType, SecondPreferLanguageType, RandomType, SpecialType)를 도입함으로써, 각 매칭 타입별 로직을 분리하고 다형성을 활용하여 구조의 유연성을 높였습니다. 이를 통해 새로운 매칭 타입의 추가나 기존 로직의 변경이 훨씬 용이해졌으며, 코드의 재사용성과 확장성이 개선되었습니다

 

public class ParticipantGroup {
    private final List<Participant> participants;
    private final MatchingRuleProperties matchingRuleProperties;
    private Relationship relationship;

    private ParticipantGroup(final List<Participant> participants,
                             final MatchingRuleProperties matchingRuleProperties) {
        ...
    }

    public static ParticipantGroup from(final MatchingOperateRequest matchingOperateRequest,
                                        final MatchingRuleProperties matchingRuleProperties) {
        ...
        return new ParticipantGroup(participants, matchingRuleProperties);
    }

    public void matchAllWith(CandidateGroup candidateGroup) {
        for (Participant participant : participants) {
            tryMatchBetween(participant, candidateGroup);
        }
    }

    public void matchEachWith(LanguageQueue languageQueue, MatchingMode matchingMode) {
        for (Participant participant : participants) {
            Language preferLanguage = participant.getPreferLanguage(matchingMode);
            CandidateGroup candidateGroup = languageQueue.getCandidateGroupByLanguage(preferLanguage);
            tryMatchBetween(participant, candidateGroup);
        }
    }

    public void updateToSpecialRelationshipMode() {
        this.relationship = Relationship.SPECIAL;
    }

    public ParticipantGroup getParticipantsLessThan(final int numberOfPartner) {
        List<Participant> filteredParticipants = participants.stream()
                .filter(participant -> participant.getNumberOfPartners() < numberOfPartner).toList();
        return new ParticipantGroup(filteredParticipants, matchingRuleProperties);
    }

    public List<Participant> getParticipantsByLanguage(Language language) {
        return participants.stream().filter(participant -> participant.firstPreferLanguage().equals(language)).toList();
    }

    public List<Participant> getParticipantsToSave() {
        return participants;
    }

    private void tryMatchBetween(final Participant participant, final CandidateGroup candidates) {
        ...
    }

    private boolean canContinueMatching(final Relationship relationship,
                                        final Participant participant,
                                        int tries,
                                        final CandidateGroup candidates) {
        return !isExceededMaxPartners(relationship, participant) && !isExceedMaxTries(tries) && !candidates.isEmpty();
    }

    private void addMatching(final Participant participant, final Participant partner) {
        participant.addPartner(relationship, partner);
        partner.addPartner(relationship, participant);
    }

    private boolean isValidMatching(final Relationship relationship,
                                    final Participant participant,
                                    final Participant partner) {
        return participant != partner &&
                !hasMetBetween(participant, partner) &&
                !isPartnerBetween(participant, partner) &&
                !hasBlockedBetween(participant, partner) &&
                !isExceededMaxPartners(relationship, partner);
    }

    private boolean isExceededMaxPartners(final Relationship relationship, final Participant participant) {
        if (relationship.equals(Relationship.NORMAL)) {
            return participant.getNumberOfPartners() >= matchingRuleProperties.getMaxNormalPartners(); // 4
        }
        return participant.getNumberOfPartners() >= matchingRuleProperties.getMaxPartners(); // 5
    }

    private boolean hasMetBetween(final Participant participant, final Participant partner) {
        return participant.hasMetPreviousRound(partner) || partner.hasMetPreviousRound(participant);
    }

    private boolean isPartnerBetween(final Participant participant, final Participant partner) {
        return participant.isPartnerWith(partner) || partner.isPartnerWith(participant);
    }

    private boolean hasBlockedBetween(final Participant participant, final Participant partner) {
        return participant.hasBlocked(partner) || partner.hasBlocked(participant);
    }

    private boolean isExceedMaxTries(int tries) {
        return tries > matchingRuleProperties.getMaxTries();
    }
}

기존의 매칭 시스템은 참가자들에 대한 정보를 과도하게 알고 있었으며, 참가자 객체를 직접 조작하는 방식으로 매칭 로직이 구현되어 있었습니다. 이는 캡슐화에 위배되며, 유지보수성 및 확장성에 부정적인 영향을 끼쳤습니다.

개선점 :

해당 문제를 해결하기 위해, 매칭 로직의 대부분을 새롭게 도입된 ParticipantGroup 클래스로 이전하였습니다. 이 클래스로 참가자들의 집합을 관리하며, 참가자들간에 필요한 모든 연산을 캡슐화했습니다.

  • 참가자 일급 컬렉션: ParticipantGroup클래스는 참가자들의 컬렉션을 캡슐화하고, 매칭 로직을 해당 클래스 내부로 이동시켰습니다. 이를 통해 매칭 로직이 참가자 객체를 직접 조작하는 것이 아닌, 참가자 컬렉션을 통해 간접적으로 관리되도록 했습니다.
  • 로직의 집중화: 매칭 로직이 ParticipantGroup 내부에 집중됨으로써, 매칭과 관련된 모든 연산과 조건이 한 곳에서 관리되어 코드의 가독성과 유지보수성이 향상되었습니다.
  • 매칭 규칙 주입: 기존에 매칭 규칙을 상수로 정의했던 방법에서, MatchingRuleProperties를 통해 외부에서 주입받는 방식으로 변경하여 매칭 로직의 유연성을 크게 증가시켰습니다.

💾 [매칭 결과 저장 로직]

private void saveMatchingResult(final MatchingRound matchingRound, final List<Participant> participants) {
    participants.stream()
            .flatMap(participant -> participant.partners().stream()
                    .map(partner -> MatchingResult.from(matchingRound, participant, partner)))
            .forEach(this::matchBetween);
    eventPublisher.createChatRoom(participants);
    eventPublisher.sendNotification(participants);
}

private void matchBetween(final MatchingResult matchingResult) {
    matchingResultRepository.save(matchingResult);
    matchingResult.matchEach();
}

개선점 : 인조 식별자 → 식별자 직접 할당

기존에 매칭 결과를 저장하는 로직에서는 각 매칭에 대해 인조 식별자인 매칭ID를 사용했습니다.

매칭 테이블(매칭ID, 매칭_회원ID, 매칭된_회원ID, 채팅방ID) 기본키 : {매칭ID}

 

이 방식은 식별자 생성에 따른 추가적인 데이터베이스 연산을 필요로 했으며, 성능상의 제약을 초래할 수 있었습니다.

 

💡 MySQL에서 @Id@GeneratedValue(strategy = GenerationType.IDENTITY)를 사용하는 경우, 실제로는 데이터를 삽입하는 INSERT 연산 후에 생성된 ID 값을 데이터베이스로부터 받아오기 위한 추가적인 조회 연산이 필요합니다.

 

 

그렇다면, 식별자를 직접 할당할 수 있는 상황이라면 ID를 생성하는데에 필요한 오버헤드를 줄일 수 있지 않을까? 라고 판단했고, 테이블 구조를 다음과 같이 변경했습니다.

매칭 테이블(매칭_회차, 매칭_회원ID, 매칭된_회원ID) 기본키 : {매칭_회차, 매칭_회원ID}

이 구조는 아래와 같은 이점을 제공했습니다.

  • ID생성 오버헤드 감소 : 인조 식별자 생성 과정에서 발생하는 데이터베이스의 추가적인 조회 연산이 필요 없어, 전체적인 성능 개선을 이룰 수 있었습니다
    • 조회 성능 향상 : 사용자가 주로 조회하는 매칭 회차와 회원ID 기반으로 기본키를 구성함으로써, 데이터 조회 시 성능이 87%향상되었습니다. 

버전 매칭 결과 조회에 걸린 시간 : 41.8초 (1명당 평균 139ms 소요) (1번 회원부터, 300번 회원까지 한명씩 조회하는데 걸린 총 시간)

 

개선된 버전 매칭 결과 조회에 걸린 시간 : 5.2초 (1명당 평균 17ms 소요) (1번 회원부터, 300번 회원까지 한명씩 조회하는데 걸린 총 시간)

 

개선점 : 매칭 서비스에서 채팅 서비스와 결합도를 낮추기

이벤트 처리방식

매칭 서비스에 채팅 시스템이 강하게 결합되어 있는 모습을 보곤, 추후 시스템이 성장할때 복잡성, 확장성, 유지보수 등 여러 문제에 직면할 것에 대해 경계했습니다.

따라서, 지금보다 더 나은 채팅 처리 방식을 모색하게 되었고, 그 결과 이벤트 처리 방식이 선택되었습니다.

다음과 같은 이점을 누릴 수 있기 때문에 이벤트 처리 방식을 선택했습니다.

  • 결합도 감소 : 직접적인 의존성을 제거할 수 있습니다. 매칭 서비스는 채팅 서비스로 이벤트를 발행만 하면 되므로, 채팅 서비스의 구현 세부사항을 알 필요가 없습니다.
  • 확장성 및 유연성 향상 : 이벤트 기반 시스템으로 추후 새로운 기능을 쉽게 추가할 수 있는 구조를 만들수 있습니다.
  • 비동기 처리 : 매칭 서비스가 채팅 서비스와의 통신을 비동기적으로 수행할 수 있게 해주어, 시스템의 전체적인 응답성과 성능을 향상시킬 수 있습니다.
@Component
public class MatchingEventPublisher {
    private final ApplicationEventPublisher eventPublisher;

    public MatchingEventPublisher(final ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void createChatRoom(List<Participant> participants) {
        MemberPairGroup matchedMemberPairGroup = MemberPairGroup.fromParticipants(participants);
        Set<MemberPair> matchedMemberPairs = matchedMemberPairGroup.getMemberPairs();
        eventPublisher.publishEvent(new ChatRoomCreationEvent(matchedMemberPairs));
    }

    public void expireChatRoom(List<MatchingResult> matchingResults) {
        MemberPairGroup expiredMemberPairGroup = MemberPairGroup.fromMatchingResults(matchingResults);
        Set<MemberPair> expiredMemberPairs = expiredMemberPairGroup.getMemberPairs();
        eventPublisher.publishEvent(new ChatRoomExpireEvent(expiredMemberPairs));
    }

    public void sendNotification(List<Participant> participants) {
        List<Participant> participantsHasPartner = participants.stream()
                .filter(Participant::hasPartner).toList();
        MulticastMessage multicastMessage = MatchingNotificationMessage.createMulticastMessage(participantsHasPartner);
        eventPublisher.publishEvent(multicastMessage);
    }
}
    eventPublisher.createChatRoom(participants);
    eventPublisher.sendNotification(participants);

이벤트 처리 방식은 저희 프로젝트의 요구사항에 잘 부합했습니다.

특히, 매칭 서비스와 채팅 서비스 간의 비동기적인 상호작용을 가능하게 하여, 매칭 서비스가 매칭 로직에 집중할 수 있게 되었습니다. 채팅 서비스는 매칭 완료 이벤트를 구독하여, 필요한 채팅방을 독립적으로 생성하게 됨으로써, 서비스 각각이 자신의 핵심 기능에 집중할 수 있는 환경을 조성하였습니다.

✅ 결론

이번 리펙토링 과정을 통해, 전체 매칭 시스템의 성능을 79.2% 향상 시킬 수 있었습니다. 이 과정을 통해, 단순히 코드를 작성하는 것을 넘어, 지속 가능하고 확장 가능한 소프트웨어를 만드는 것이 얼마나 중요한지를 깨달았습니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함