2022-moragora icon indicating copy to clipboard operation
2022-moragora copied to clipboard

[BE] 나의 미팅 조회 :: 리펙터링, 쿼리최적화

Open progress0407 opened this issue 2 years ago • 3 comments

Close #507

PR 타입

  • [x] 리팩토링

반영 브랜치

feature/be/find-all-by-me -> dev

요구사항

  • 나의 미팅 조회 /meetings/me
    • 리펙터링
      • 비즈니스 로직을 도메인으로 응집
    • 1+N 쿼리 최적화

변경사항

CompositionRepository 도입

성능 최적화를 고려하면서 엔티티를 조회할 경우, 단순히 조회하여 조립하는 것 뿐인데 흐름이 복잡해서 파악하는 게 힘든 경우가 있었어요.

따라서 Service <-> Repository간에 중간 Repository를 하나 두어 (Facade Pattern) 생성 로직을 응집했어요.

이 Repository의 역할은 아래와 같아요.

  • 복잡한 조회 로직 위임
  • 조회 쿼리 최적화
  • 반환 타입이 DTO가 아닌 Entity
  • Entity(Meeintg)가 가진 Repository와의 DTO 제거
  • 일급 컬렉션 MeetingAttendances 의존 제거

반환타입이 Entity인 이유

아무래도 일반 DTO를 반환하다 보면 미팅별 지각횟수 조회 란 개념이 엔티티가 아닌 각 API별 ResponseDTO로 중복됩니다. 따라서 반환 타입을 엔티티로 잡았습니다.

Entity(Meeintg)가 가진 Repository와의 DTO 제거

중간 Repository Layer가 하나 더 있기 때문에, Meeting의 Repositroy <-> Entity간 DTO(지각횟수)를 의존성을 이곳에서 제거할 수 있습니다.

일급 컬렉션 MeetingAttendances 의존 제거

나의 미팅 조회 API의 호출과정에서 일급 컬렉션을 의존하는 로직을 제거했어요.

쿼리 5번이 나가는 이유

image

  • Meeting + Participant 조회 (fetch join) 1

    • 각 조회 결과(Meeting)에 대해
      • Participant와 지각횟수 조회 1
      • 다가오는 이벤트 조회 1
  • 5번: 1 + 2 * (1+ 1)

리펙터링 전/후 코드 (Service Layer 중심)

리펙터링

    public MyMeetingsResponse findAllByUserId(final Long userId) {
        final List<Participant> participants = participantRepository.findByUserId(userId);

        final List<MyMeetingResponse> myMeetingResponses = participants.stream()
                .map(participant -> generateMyMeetingResponse(participant, serverTimeManager.getDate()))
                .collect(Collectors.toList());

        return new MyMeetingsResponse(myMeetingResponses);
    }
    private MyMeetingResponse generateMyMeetingResponse(final Participant participant, final LocalDate today) {
        final Meeting meeting = participant.getMeeting();
        final boolean isLoginUserMaster = participant.getIsMaster();

        final MeetingAttendances meetingAttendances = getMeetingAttendances(meeting, today);
        final boolean isCoffeeTime = meetingAttendances.isTardyStackFull();
        final int tardyCount = countTardyByParticipant(participant, meetingAttendances);

        final Optional<Event> upcomingEvent = eventRepository
                .findFirstByMeetingIdAndDateGreaterThanEqualOrderByDate(meeting.getId(), today);
        if (upcomingEvent.isEmpty()) {
            return MyMeetingResponse.of(
                    meeting, tardyCount, isLoginUserMaster, isCoffeeTime, false, null
            );
        }
        final Event event = upcomingEvent.get();
        final LocalTime startTime = event.getStartTime();
        final boolean isActive = event.isSameDate(today) && serverTimeManager.isAttendanceOpen(startTime);
        final LocalTime attendanceOpenTime = serverTimeManager.calculateOpenTime(startTime);
        final LocalTime attendanceClosedTime = serverTimeManager.calculateAttendanceCloseTime(startTime);
        return MyMeetingResponse.of(
                meeting, tardyCount, isLoginUserMaster, isCoffeeTime, isActive,
                EventResponse.of(event, attendanceOpenTime, attendanceClosedTime)
        );
    }
    private int countTardyByParticipant(final Participant participant, final MeetingAttendances meetingAttendances) {
        final ParticipantAttendances participantAttendances = meetingAttendances
                .extractAttendancesByParticipant(participant);
        return participantAttendances.countTardy();
    }
    private MeetingAttendances getMeetingAttendances(final Meeting meeting, final LocalDate today) {
        final List<Long> participantIds = meeting.getParticipantIds();
        final List<Attendance> foundAttendances = attendanceRepository
                .findByParticipantIdInAndEventDateLessThanEqual(participantIds, today);
        return new MeetingAttendances(foundAttendances, participantIds.size());
    }
MeetingAttendances // 생략 ...
ParticipantAttendances // 생략 ...

리펙터링

    public MyMeetingsResponse findAllByMe(final Long loginUserId) {
        final LocalDate today = serverTimeManager.getDate();
        final List<Meeting> meetings = compositionRepository.meetingsWithTardyCount(loginUserId);
        final List<MyMeetingResponse> myMeetingsResponse = createMyMeetingsResponse(loginUserId, today, meetings);

        return new MyMeetingsResponse(myMeetingsResponse);
    }

    private List<MyMeetingResponse> createMyMeetingsResponse(final Long loginUserId,
                                                             final LocalDate today,
                                                             final List<Meeting> meetings) {
        final List<MyMeetingResponse> myMeetingsResponse = new ArrayList<>();

        for (final Meeting meeting : meetings) {
            final Event upcomingEvent = findUpComingEvent(meeting, today);
            myMeetingsResponse.add(MyMeetingResponse.of(meeting, upcomingEvent, loginUserId, serverTimeManager));
        }

        return myMeetingsResponse;
    }
    private Event findUpComingEvent(final Meeting meeting, final LocalDate today) {
        return eventRepository
                .findFirstByMeetingIdAndDateGreaterThanEqualOrderByDate(meeting.getId(), today)
                .orElse(null);
    }

쿼리 최적화

최적화 전

image

최적화 후

image

논의하고 싶은 내용

MeetingAttendances 검증 제외와 이유

현재 Jacoco 유효성 검증에 Branches 가 0.8로 되어있는데

이 Branches는 분기문에 따른 테스트 커버리지를 측정하고 있어요

그러나 테스트를 추가했음에도 불구하고 0.75가 나오는데... 그 이유를 잘 모르겠네요 ㅠ,

image

image

image

image

(테스트를 추가했지만 branches 커버리지가 올라가진 않았네요!!)

곧 사라질 클래스이기도 하고 (ParticipantAttendances와 함께)...
MeetingAttendances를 검증 대상에서 배제하는 것에 대해 제안합니다. ㅠ

progress0407 avatar Nov 13 '22 11:11 progress0407

📊 checkmate-sonarqube-588 분석 결과 확인하기 링크

github-actions[bot] avatar Nov 13 '22 11:11 github-actions[bot]

📊 checkmate-sonarqube-588 분석 결과 확인하기 링크

github-actions[bot] avatar Nov 18 '22 07:11 github-actions[bot]

리뷰 반영했어요 :) 천천히 확인 하면 될 것 같아요

progress0407 avatar Nov 18 '22 08:11 progress0407