[큰소리 프로젝트] 4. 특정 기간 예약 리스트 조회 쿼리 개선

2026. 10. 4. 15:14·팀 프로젝트/[2025] 큰소리 웹 페이지
반응형

1. 증상

데이터베이스를 supabase 로 이전하고 나서, 운영 데이터를 복사하여 넣은 뒤, 개발 서버에 우선 붙여 테스트 해보았다.

 

개발 서버의 예약 화면

 

그런데 문제가 생겼다.

현재 운영 서버에서 지정한 '예약 가능 시간' 범위는 "금요일 오후 6시부터 오후 8시"

그런데 그 이외 시간들에 대해서 현재 예약 가능한 상태로 프론트 ui 가 그려지고 있었다.

 

운영 서버의 예약 화면

 

즉, 원래대로라면 이렇게 그려져야 하는데, 예약 가능 시간 정보를 못 가져오는 문제가 발생하였다.

 

2. 원인 찾기

우선 프론트에서 확인할 수 있는 로그를 확인해보았다.

 

개발서버에서 출력중인 로그를 확인한 결과 5초로 걸어둔 timeout 리밋을 벗어나서 에러가 나고 있었다.

특정 날짜의 예약 시간 데이터를 조회하는데 5초가 넘게 걸린다니..?

동아리에서만 쓰는 프로젝트라 데이터가 그렇게 많지도 않은데 이상하게 느껴졌다.

 

 

개발자 콘솔의 네트워크 탭에서 timeout 이 발생한 api 를 확인해보았다.

2026년 10월의 예약 리스트를 조회하는 api 로 보인다.

 

해당 api 를 postman 에서 호출해보았다.

무려 8.99초가 소요되었다.

 

 

동일한 api 를 기존 운영 환경 주소로 호출했을 때는 59ms 가 소요되었다.

로직은 바뀐 것이 없고, 환경만 바뀌었을 뿐인데 응답 시간이 무려 152배가 증가했다.

 

먼저 배포하면서 환경이 바뀐 것은 2가지였다.

 

1. 서버 인스턴스가 aws 에서 oci 로 바뀌었고

2. 데이터베이스 인스턴스가 aws rds 에서 supabase 로 바뀌었다.

 

아무리 프리티어를 사용했다고 해도, 응답 시간이 100배 이상 느려질 정도로 oci 환경이 열악하지는 않을 것이라고 예상했다.

그래서 로컬 환경에서 데이터베이스 접속 환경만 supabase 로 바꾸어서 동일 api 를 호출해보았다.

 

 

호출 결과 로컬에서도 9.69초라는 매우 긴 응답 시간을 보여준다.

 

 

그리고 동일 api 를 호출하고 supabase 에서 쿼리 실행에 걸리는 시간을 보았다.

쿼리 실행을 전담한 프로세스의 실행 시간이 유사하게 10초였다.

 

이는 데이터베이스에 접근하는 어플리케이션 로직에 문제가 있을 것이라고 판단해서, 해당 api 로직을 확인해보기로 하였다.

 

public List<DailyAvailableResponse> findDailyAvailableByMonth(String yearMonth) {

    LocalDate start = DateUtil.parseMonthToFirstDate(yearMonth);
    LocalDate end = start.plusMonths(2);

    return Stream.iterate(start, date -> date.isBefore(end), date -> date.plusDays(1))
            .map(this::convertDateToDailyAvailableResponse).toList();
}

private DailyAvailableResponse convertDateToDailyAvailableResponse(LocalDate date) {
    return dailyScheduleRepository.findByDate(date)
            .map(DailyAvailableResponse::from)
            .orElseGet(() -> weeklyScheduleRepository.findByDayOfWeek(date.getDayOfWeek())
                    .map(schedule -> DailyAvailableResponse.of(date, schedule))
                    .orElseGet(() -> DailyAvailableResponse.createInactiveDate(date)));
}

 

로직을 읽어보니 문제 원인이 보였다.

현재 로직은 년월을 받아서 해당 월의 1일부터 그 다음 월의 말일까지 모든 날짜를 '순회'하면서, convertDateToDailyAvailaableResponse() 메서드를 호출한다.

 

이 메서드 안에서는 dailyScheduleRepository 로부터 특정 날짜의 스케줄을 가져오고, 만약 해당 날짜의 스케줄이 없다면, 해당 일의 요일에 해당하는 스케줄을 가져왔다.

 

여기에서 '스케줄' 이라는 개념은, 동아리 연습실을 다른 동아리와 공유하며 사용하고 있어서 특정 요일, 특정 시간대만 우리가 사용하는 규칙이 있다.

이 연습실 사용 시간 스케줄을 정의한 것이 Schedule 이다.

이때 스케줄에는 주간 고정 스케줄이 있고, 특정 날짜에만 적용되는 (예를 들어 학기중 평일에는 오후 5시부터 사용이 가능하지만, 공휴일에는 오전 10시부터 사용이 가능하다) 스케줄의 경우에는 daily schedule 이라는 데이터로 별도 관리한다.

 

그래서 이 로직은 먼저 일일 스케줄을 찾고, 이게 없으면 주간 스케줄을 찾아서 해당 주간 요일의 스케줄을 반환하는 로직이다.

 

문제점은 데이터베이스 접근 횟수에 있었다.

 

 

서버에서 hibernate 접근 로그를 보면 select 문이 api 호출 한번에 총 122번 나간 것을 확인할 수 있다.

Hibernate: 
    select
        ds1_0.date,
        ds1_0.end_time,
        ds1_0.is_active,
        ds1_0.start_time 
    from
        daily_schedule ds1_0 
    where
        ds1_0.date=?
Hibernate: 
    select
        ws1_0.day_of_week,
        ws1_0.end_time,
        ws1_0.is_active,
        ws1_0.start_time 
    from
        weekly_schedule ws1_0 
    where
        ws1_0.day_of_week=?

 

그리고 쿼리는 daily_schedule 테이블에서 먼저 찾고, 없으면 weekly_schedule 테이블에서 스케줄 데이터를 찾는 구조가 2달치 (61 * 2)에 대해 반복되고 있었다.

 

따라서 수정 방향은 daily schedule 테이블과 weekly_schedule 을 한번씩만 벌크로 조회한 뒤, 조회 데이터를 어플리케이션 레벨에서 map 으로 반환해서 조회하는 방법으로 가면 데이터베이스와의 네트워킹 비용을 줄일 수 있겠다는 결론이 나왔다.

 

그런데 이걸 보고나니 두 가지 의문점이 들었다.

 

첫 번째로, 쿼리가 비효율적으로 작성되었던 것은 동일한데 왜 aws 에서는 같은 문제가 발생하지 않았던 것인지에 대한 문제이다.

이는 예상하기에, rds를 사용함에 있어 public ip 연결 없이 인스턴스와 priavte network 로 직접 연결되어있어 통신 비용이 크지 않았기 때문이라고 추측하였다.

 

 

두 번째로, 먼저 현재 우리 테이블의 weekly_schedule 테이블은 day_of_week 를 pk 로 하는, 7개 고정 데이터를 가지고 있는 테이블이다. 따라서 weekly_schedule 의 경우에는 기존에 조회한 요일 데이터가 있다면 엔티티 컨텍스트에 있는 1차 캐시를 활용해서 쿼리를 추가로 날릴 필요가 없다. 그런데도 계속 매 요일마다 추가로 쿼리를 날리고 있었다.

 

그 차이가 발생한 원인은 쿼리 메서드의 사용 방법 차이에 있었다.

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class WeeklySchedule {

    @Id
    @Enumerated(EnumType.STRING)
    private DayOfWeek dayOfWeek;
    
    ...
}

 

엔티티가 이렇게 정의되어 있는 상태일 때

 

Optional<WeeklySchedule> findByDayOfWeek(DayOfWeek dayOfWeek);

 

레포지토리에서 조회 메서드를 이렇게 지정하면, 이 쿼리는 Id 기반이 아닌 컬럼 필드 조건 기반으로 인식한다.

즉, 쉽게 말하면 

 

// 직접 정의한 쿼리 메서드
Optional<WeeklySchedule> findByDayOfWeek(DayOfWeek dayOfWeek);

// JPA 가 기본 제공하는 메서드
Optional<WeeklySchedule> findById(DayOfWeek dayOfWeek);

 

이 두 쿼리는 겉으로 보기에 완전 동일하지만, JPA가 처리하기에 위는 day of week 를 일반 컬럼처럼 다루고

아래는 JPA가 처리하기에 Id 로 명확히 인식해서 다룬다.

 

그리고 JPA의 1차 캐시를 사용할 때는, 엔티티의 pk 를 기반으로 엔티티 객체를 식별해서 반환하기 때문에 아래 쿼리를 사용해야만 dayOfWeek 를 pk로 인식하여 1차 캐시를 활용한다.

 

private DailyAvailableResponse convertDateToDailyAvailableResponse(LocalDate date) {
    return dailyScheduleRepository.findByDate(date)
            .map(DailyAvailableResponse::from)
            .orElseGet(() -> weeklyScheduleRepository.findById(date.getDayOfWeek())
                    .map(schedule -> DailyAvailableResponse.of(date, schedule))
                    .orElseGet(() -> DailyAvailableResponse.createInactiveDate(date)));
}

 

그래서 이렇게 findById 를 사용하도록 수정한 뒤, 동일 api 를 다시 테스트 해보면

 

 

응답 시간이 5.55s 로 약 44.4% 응답시간이 개선되었다.

실제 쿼리가 나간 것도

 

 

68개로, weekly 조회 쿼리 7개, daily 조회 쿼리 61 개가 나간 것을 확인할 수 있다.

 

3. 해결

하지만 여전히 응답 시간은 5초가 넘는 시간으로 매우 오래걸리고, 이 원인은 daily schedule 을 반복하며 조회하는데 있기 때문에, 다음과 같이 조회 로직을 수정하였다.

 

public List<DailyAvailableResponse> findDailyAvailableByMonth(String yearMonth) {

    LocalDate start = DateUtil.parseMonthToFirstDate(yearMonth);
    LocalDate end = start.plusMonths(2);

    Map<LocalDate, DailySchedule> dailySchedules = dailyScheduleRepository.findAllByDateBetween(start, end).stream()
            .collect(Collectors.toMap(DailySchedule::getDate, Function.identity()));

    Map<DayOfWeek, WeeklySchedule> weeklySchedule = weeklyScheduleRepository.findAll().stream()
            .collect(Collectors.toMap(WeeklySchedule::getDayOfWeek, Function.identity()));

    return Stream.iterate(start, date -> date.isBefore(end), date -> date.plusDays(1))
            .map(date -> {
                DailySchedule dailySchedule = dailySchedules.get(date);
                if (dailySchedule != null) {
                    return DailyAvailableResponse.from(dailySchedule);
                }

                WeeklySchedule dayOfWeekSchedule = weeklySchedule.get(date.getDayOfWeek());
                if (dayOfWeekSchedule != null) {
                    return DailyAvailableResponse.of(date, dayOfWeekSchedule);
                }

                return DailyAvailableResponse.createInactiveDate(date);
            }).toList();
}

 

수정 후 로컬에서 테스트를 진행해보면

 

 

응답 시간이 347ms 로 크게 개선되었다.

 

start
Hibernate: 
    select
        ds1_0.date,
        ds1_0.end_time,
        ds1_0.is_active,
        ds1_0.start_time 
    from
        daily_schedule ds1_0 
    where
        ds1_0.date between ? and ?
Hibernate: 
    select
        ws1_0.day_of_week,
        ws1_0.end_time,
        ws1_0.is_active,
        ws1_0.start_time 
    from
        weekly_schedule ws1_0
end

 

실제로 나간 쿼리도 2개로 감소하였다.

반응형
저작자표시 비영리 변경금지 (새창열림)

'팀 프로젝트 > [2025] 큰소리 웹 페이지' 카테고리의 다른 글

[큰소리 프로젝트] 3. AWS에서 오라클 클라우드로 서버 이전하기  (0) 2026.09.26
[큰소리 프로젝트] 2. 디자인 & 역할 분담 & 시스템 만들기  (1) 2025.01.28
[큰소리 프로젝트] 1. 팀 빌딩 & 기획  (2) 2025.01.04
'팀 프로젝트/[2025] 큰소리 웹 페이지' 카테고리의 다른 글
  • [큰소리 프로젝트] 3. AWS에서 오라클 클라우드로 서버 이전하기
  • [큰소리 프로젝트] 2. 디자인 & 역할 분담 & 시스템 만들기
  • [큰소리 프로젝트] 1. 팀 빌딩 & 기획
에버듀
에버듀
개발은 좋은데 뭘로 개발할까
  • 에버듀
    Blog. 에버듀
    에버듀
  • 전체
    오늘
    어제
    • 분류 전체보기 (622) N
      • 자기계발 (50)
        • 회고 & 생각 정리 (28)
        • 대외활동 (11)
        • 동아리 (7)
        • 자격증 (3)
        • 머니 스터디 (1)
      • CS (335)
        • 자료구조 (19)
        • 어셈블리 (41)
        • 멀티미디어응용수학 (7)
        • 컴퓨터 구조 (29)
        • 알고리즘 분석 (4)
        • 컴퓨터 네트워크 (38)
        • 프로그래밍언어론 (15)
        • HCI 윈도우즈프로그래밍 (26)
        • 기초데이터베이스 (29)
        • 운영체제 (23)
        • 오토마타 (24)
        • 문제해결기법 (11)
        • 블록체인 (22)
        • 소프트웨어공학 (21)
        • 기계학습심화 (12)
        • 컴퓨터그래픽스와 메타버스 (8)
        • 분산시스템특론 (6)
      • 개인 프로젝트 (43)
        • 토이 프로젝트 (3)
        • [2020] 카카오톡 봇 (9)
        • [2021] 코드악보 공유APP (22)
        • [2022] 유튜브 뮤직 클론코딩 (9)
        • [2025] 한글 SQL 데이터베이스 (0)
      • 팀 프로젝트 (24) N
        • [2020] 인공지능 숫자야구 (4)
        • [2022] OSAM 온라인 해커톤 (10)
        • [2024] GDSC 프로젝트 트랙 (6)
        • [2025] 큰소리 웹 페이지 (4) N
      • 알고리즘 (PS) (107)
        • BOJ (101)
        • Programmers (5)
        • 알고리즘 이모저모 (1)
      • Infra (12)
        • AWS (1)
        • Oracle Cloud (8)
        • Firebase (2)
        • Network (1)
      • WEB(BE) (8)
        • express.js (1)
        • Spring & Spring Boot (7)
      • WEB(FE) (2)
        • html, css, js (1)
        • React.js (1)
      • Tool & Language (6)
        • Edit Plus (1)
        • Git (1)
        • Python3 (2)
        • Java (2)
      • Android (18)
        • Java (6)
        • Flutter (12)
      • Window (2)
        • Visual Studio 없이 WPF (1)
        • MFC (1)
      • 독서 (14)
        • Inside Javascript (7)
        • Database Internals (6)
        • 한 글 후기 (1)
  • 링크

    • github
    • website
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
에버듀
[큰소리 프로젝트] 4. 특정 기간 예약 리스트 조회 쿼리 개선
상단으로

티스토리툴바