내 API 응답시간은 왜 이렇게 오래 걸릴까?

2025. 3. 3. 17:12·삽질로그

1. 들어가며

최근 스타트업에서 사업장의 운영시간에 따라 구장의 예약 슬롯을 한 시간 단위로 생성하는 API를 만들게 되었다.
이 기능은 다음과 같은 경우에 실행된다.

✅ 구장 생성 시
✅ 사업장 운영시간 변경 시

그러나, 현재 코드에는 성능 이슈가 발생하고 있었다. 예약 슬롯이 많아질수록 DB 부하가 증가하고, 실행 시간이 길어지는 문제가 있었다. 이번 글에서는 기존 코드의 문제점을 분석하고, 어떤 부분이 비효율적인지 살펴보겠다.

2. 기존 코드 분석

예약 슬롯을 생성하는 기존 로직은 다음과 같다.
public void appendSlotsOfMonth(Long fieldId) {
    Set uniqueStartTimes = new HashSet<>();
    FieldDetailDao dao = fieldService.findFieldByIdJoinSite(fieldId)
        .orElseThrow(() -> new FieldException(FieldErrorCode.FIELD_NOT_FOUND));

    for (int createDate = 0; createDate < 30; createDate++) {
        LocalDateTime today = LocalDate.now().atStartOfDay().plusDays(createDate);

        if (isWeekend(today.toLocalDate())) {
            appendSlotsOfWeek(dao.getSite().getWeekendOperatingStartTime(),
                dao.getSite().getWeekendOperatingEndTime(), dao.getField(), createDate,
                dao.getField().getWeekendPrice(), uniqueStartTimes);
            continue;
        }
        appendSlotsOfWeek(dao.getSite().getWeekdayOperatingStartTime(), dao.getSite().getWeekdayOperatingEndTime(),
            dao.getField(), createDate, dao.getField().getWeekdayPrice(), uniqueStartTimes);
    }
}
  • 한 달 동안의 예약 슬롯을 생성
  • 요일에 따라 평일/주말 운영시간, 금액을 다르게 설정
  • 기존 예약 슬롯을 저장한 Set<LocalDateTime> uniqueStartTimes를 사용하여 중복 검증

3. 문제점 분석

개별적으로 중복 검사 수행

각 슬롯에 대해 개별적으로 DB를 조회하여 중복 여부를 확인한다.
List validSlots = reservationSlots.stream()
    .filter(slot -> !reservationSlotRepository.existsReservationSlot(field.getId(), slot.getStartAt(), slot.getEndAt()))
    .toList();
  • 한 개의 슬롯이 추가될 때마다 별도의 SELECT 쿼리 실행
  • 대량 데이터 처리 시 성능 저하 발생

하루 24개 슬롯 × 30일이면, 최대 720번의 DB 조회가 실행됨 → 성능 저하!

saveAll() 호출 문제

한 달치 예약 슬롯을 생성하면, saveAll()을 30번 개별 호출한다.
if (!validSlots.isEmpty()) {
    reservationSlotRepository.saveAll(validSlots);
}

 

DB 트랜잭션 오버헤드 증가 → 성능 저하

운영시간 변경 시 기존 슬롯 삭제 로직

private void deleteReservationSlot(ReservationSlot reservationSlot) {
    Payment payment = reservationSlotPaymentRepository.findPaymentByReservationSlotId(reservationSlot.getId());
    if (payment != null && payment.getStatus().equals(PaymentStatus.CANCELLED)) {
        reservationSlot.delete(); // Soft Delete
        return;
    }
    if (!reservationSlot.getIsReserved() && payment == null) {
        reservationSlotRepository.deleteById(reservationSlot.getId()); // Hard Delete
    }
}
  • 예약 슬롯마다 개별적으로 findPaymentByReservationSlotId() 실행 → N+1 문제 발생
  • deleteById()를 개별 실행 → 트랜잭션 오버헤드 증가

4. 기존 코드 실행 시 응답시간

구장 생성 시 응답 시간

📌 테스트 조건

  • 신규 구장을 생성하면서 해당 구장의 예약 슬롯을 한 달치(30일) 생성
  • 평일 운영시간: 00:00 - 24:00 (하루 24개 슬롯 생성)
  • 주말 운영시간: 00:00 - 24:00 (하루 24개 슬롯 생성)
  • 생성되는 슬롯 개수: 24개 x 30일 = 720개

 

📌 쿼리 실행 개수

  • existsReservationSlot() 호출 횟수: 720회
  • saveAll() 호출 횟수: 30회

 

📌 실행 결과

{
  "로그 타입" : "RESPONSE",
  "응답 메서드" : "class com.dalliza.ownerapi.domains.field.controller.FieldController.uploadField",
  "응답 데이터" : [ {
    "headers" : { },
    "body" : {
      "code" : "2000",
      "message" : "요청에 성공하였습니다.",
      "data" : {
        "field" : {
          "id" : 23
        }
      }
    },
    "statusCode" : "OK",
    "statusCodeValue" : 200
  } ],
  "응답 시간(ms)" : 44711
}

사업장 운영시간 변경 시 응답 시간

📌 테스트 조건

  • 기존 예약 슬롯을 삭제한 후, 새로운 예약 슬롯을 생성
  • 평일 운영시간: 00:00 - 24:00 -> 09:00 - 21:00
  • 주말 운영시간: 00:00 - 24:00 -> 10:00 - 22:00
  • 연관된 모든 구장(3개)의 예약 슬롯을 재생성
  • 삭제된 슬롯 개수: 24개 x 30일 x 3개 구장 = 2,160개
  • 새로 생성된 슬롯 개수: 14개 x 30일 x 3개 구장 = 1,260개
  •  

📌 쿼리 실행 개수

  • 기존 슬롯 삭제
    • findPaymentByReservationSlotId() 호출 횟수: 2,160회
    • deleteById() 개별 실행: 2,160회
  • 새로운 슬롯 생성
    • existsReservationSlot() 호출 횟수: 720회
    • saveAll() 호출 횟수: 30회

 

📌 실행 결과

{
  "로그 타입" : "RESPONSE",
  "응답 메서드" : "class com.dalliza.ownerapi.domains.site.controller.SiteOperateController.updateOperatingTime",
  "응답 데이터" : [ {
    "headers" : { },
    "body" : {
      "code" : "2000",
      "message" : "요청에 성공하였습니다.",
      "data" : { }
    },
    "statusCode" : "OK",
    "statusCodeValue" : 200
  } ],
  "응답 시간(ms)" : 293329
}

 


5. 분석 및 결론

테스트 케이스 실행된 쿼리 개수 응답 시간 (ms)
구장 생성 (720개 슬롯) SELECT 720회 + INSERT 30회 44,711 ms
운영시간 변경 (2,160개 삭제 + 1,260개 생성) SELECT 3,420회 + DELETE 2,160회 + INSERT 90회 293,329 ms

응답 시간이 긴 이유

  • 구장 생성 시 문제점
    • existsReservationSlot()를 개별 호출하여 쿼리 720번 실행
    • saveAll()을 30번 개별적으로 호출하여 트랜잭션 오버헤드 발생
  • 운영시간 변경 시 문제점
    • 기존 슬롯 삭제 시 각 슬롯마다 개별적으로 findPaymentByReservationSlotId()를 실행 (2,160회) → N+1 문제(한 번의 쿼리(N)로 가져올 수 있는 데이터를 추가적인 쿼리(+1)로 조회하는 문제)
    • deleteById()를 2,160번 개별 실행하여 트랜잭션 오버헤드 증가
    • 새로운 슬롯 생성 시 existsReservationSlot()을 1,260번 호출하여 DB 부하 발생
    • saveAll()을 각 구장별로 개별 실행 (90번)

최종 결론

  • 현재 코드에서 예약 슬롯을 생성 및 삭제하는 과정에서 비효율적인 쿼리 호출이 많다.
  • 구장 생성 시 쿼리를 최적화하고, 운영시간 변경 시 삭제 로직을 개선해야 한다.
  • 개별적인 deleteById()를 사용하지 말고 deleteAllByIdInBatch()를 활용하여 삭제 성능을 개선해야 한다.
  • saveAll()을 개별적으로 호출하지 않고 Batch Insert를 적용하여 한 번에 저장해야 한다.

 

🚀 다음 글에서 JdbcTemplate.batchUpdate()를 활용한 성능 최적화 방법을 다룬다.
🔗 👉 2025.03.03 - [삽질로그] - Batch Insert로 API 응답 속도 175배 개선하기
 

Batch Insert로 API 응답 속도 175배 개선하기

1. 들어가며지난 글에서 예약 슬롯을 생성하는 API에서 발생한 성능 문제점을 분석했다.🔗 이전 글: 2025.03.03 - [삽질로그] - 내 API 응답시간은 왜 이렇게 오래 걸릴까?  내 API 응답시간은 왜 이렇

mingking2.tistory.com

 

'삽질로그' 카테고리의 다른 글

Spring Boot 비동기 처리 = 스레드풀?  (0) 2025.03.16
여러 요청이 동시에 들어오면, Spring Boot는 어떻게 처리할까?  (2) 2025.03.09
RDS 보안 정책 때문에 접속 불가?  (0) 2025.03.04
복잡한 웹소켓 핸들러, Event로 깔끔하게 해결하기  (0) 2025.03.03
Batch Insert로 API 응답 속도 175배 개선하기  (0) 2025.03.03
'삽질로그' 카테고리의 다른 글
  • 여러 요청이 동시에 들어오면, Spring Boot는 어떻게 처리할까?
  • RDS 보안 정책 때문에 접속 불가?
  • 복잡한 웹소켓 핸들러, Event로 깔끔하게 해결하기
  • Batch Insert로 API 응답 속도 175배 개선하기
mingking2
mingking2
에러와 삽질 속에서 성장하는 응애 개발자의 성장 일지
  • mingking2
    Mingking의 삽질 기록
    mingking2
  • 전체
    오늘
    어제
    • 분류 전체보기 (19)
      • 삽질로그 (10)
      • 스타트업 응애기 (1)
      • 프로젝트 도전기 (2)
      • DB 모르는 백엔드 탈출기 (5)
      • 네트워크 (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    MVC
    WebSocket
    spring
    n:m
    @JoinColumn
    CompletableFuture
    Tomcat
    JPA
    batch
    RDB
    플로우차트
    Sync
    context
    연관관계
    DB
    server
    orm
    Java
    Async
    servlet
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
mingking2
내 API 응답시간은 왜 이렇게 오래 걸릴까?
상단으로

티스토리툴바