Skip to content

fix: 조회 API N+1 쿼리 및 트랜잭션 오버헤드 제거(eliminate N+1 queries and implicit transaction overhead in schedule inquiry API)#39

Merged
leonroars merged 9 commits into
mainfrom
fix_schedule-api-n1-query
Feb 8, 2026
Merged

fix: 조회 API N+1 쿼리 및 트랜잭션 오버헤드 제거(eliminate N+1 queries and implicit transaction overhead in schedule inquiry API)#39
leonroars merged 9 commits into
mainfrom
fix_schedule-api-n1-query

Conversation

@leonroars

@leonroars leonroars commented Feb 8, 2026

Copy link
Copy Markdown
Owner

fix: 조회 API N+1 쿼리 및 트랜잭션 오버헤드 제거 (eliminate N+1 queries and implicit transaction overhead in schedule inquiry API)

🇰🇷 한글
mysql> SELECT event_time, command_type, CONVERT(argument USING utf8) AS query_text FROM mysql.general_log WHERE command_type = 'Query' ORDER BY event_time DESC LIMIT 150;
+----------------------------+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| event_time                 | command_type | query_text                                                                                                                                                                                                                                                                                                                                                                                  |
+----------------------------+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 2026-02-08 09:17:42.611056 | Query        | SET GLOBAL general_log = OFF                                                                                                                                                                                                                                                                                                                                                                |
| 2026-02-08 09:17:41.532639 | Query        | ROLLBACK                                                                                                                                                                                                                                                                                                                                                                                    |
| 2026-02-08 09:17:41.531712 | Query        | select csje1_0.concert_schedule_id,csje1_0.availability,csje1_0.concert_id,csje1_0.created_at,csje1_0.datetime,csje1_0.reservation_end_at,csje1_0.reservation_start_at,csje1_0.updated_at from `concertschedule` csje1_0 where csje1_0.reservation_start_at<='2026-02-08 09:17:41.53049' and csje1_0.reservation_end_at>='2026-02-08 09:17:41.53049'                                        |
| 2026-02-08 09:17:40.371555 | Query        | COMMIT                                                                                                                                                                                                                                                                                                                                                                                      |
| 2026-02-08 09:17:40.370612 | Query        | select rje1_0.reservation_id,rje1_0.concert_schedule_id,rje1_0.created_at,rje1_0.expired_at,rje1_0.price,rje1_0.seat_id,rje1_0.status,rje1_0.updated_at,rje1_0.user_id from `reservation` rje1_0 where rje1_0.status='PREEMPTED' and rje1_0.expired_at<'2026-02-08 09:17:40.370165'                                                                                                         |
| 2026-02-08 09:17:40.369727 | Query        | SELECT @@session.transaction_isolation                                                                                                                                                                                                                                                                                                                                                      |
| 2026-02-08 09:17:40.136570 | Query        | SET SESSION TRANSACTION READ WRITE                                                                                                                                                                                                                                                                                                                                                          |
| 2026-02-08 09:17:40.135910 | Query        | COMMIT                                                                                                                                                                                                                                                                                                                                                                                      |
| 2026-02-08 09:17:40.135053 | Query        | select cje1_0.concert_id,cje1_0.artist,cje1_0.created_at,cje1_0.name,cje1_0.updated_at from `concert` cje1_0 where cje1_0.concert_id=808261158700165387                                                                                                                                                                                                                                     |
| 2026-02-08 09:17:40.134247 | Query        | SET SESSION TRANSACTION READ ONLY                                                                                                                                                                                                                                                                                                                                                           |
| 2026-02-08 09:17:40.133040 | Query        | select csje1_0.concert_schedule_id,csje1_0.availability,csje1_0.concert_id,csje1_0.created_at,csje1_0.datetime,csje1_0.reservation_end_at,csje1_0.reservation_start_at,csje1_0.updated_at from `concertschedule` csje1_0 where csje1_0.reservation_start_at<='2026-02-08 09:17:40.131811' and csje1_0.reservation_end_at>='2026-02-08 09:17:40.131811' and csje1_0.availability='AVAILABLE' |
| 2026-02-08 09:17:39.945341 | Query        | SHOW GLOBAL STATUS                                                                                                                                                                                                                                                                                                                                                                          |
+----------------------------+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
12 rows in set, 1 warning (0.00 sec)

배경

프론트엔드의 N+1 API 호출을 제거하기 위해 서버 측 DTO Aggregation을 도입한 이후, 동일 조건 부하 테스트에서 처리량이 약 30% 하락한 것을 확인했습니다.
(이전 약 900 RPS → 변경 후 약 630 RPS에서 포화)

원인

계층별 소거 진단(DB CloudWatch → 앱 서버 리소스 → HikariCP → MySQL QPS → General Log)을 통해 두 가지 복합 원인을 규명했습니다.

1. N+1 쿼리 패턴

핸들러 메서드에서 공연 일정 목록 조회 후, 각 일정마다 getConcert()를 루프 호출하여 건별 SELECT가 발생했습니다.

2. Spring Data JPA 트랜잭션 오버헤드

getConcert() 서비스 메서드에 @Transactional이 없었으나, SimpleJpaRepository의 클래스 레벨 @Transactional(readOnly = true)에 의해 findById() 호출마다 독립 트랜잭션이 생성되었습니다. 이로 인해 건별 조회마다 SET SESSION TRANSACTION READ ONLYCOMMITSET SESSION TRANSACTION READ WRITE 부수 명령이 반복 발생했습니다.

두 문제가 곱해져 요청당 DB 커맨드가 SELECT 2개면 충분한 상황에서 **21개(N=5 기준)**까지 증가했습니다.

해결 방안 비교 및 결정

방안 접근 장점 단점 판단
JPA 연관관계 + JOIN FETCH @ManyToOne 매핑 후 JOIN FETCH SELECT 1개로 해결. 고부하 시 쿼리 절대량 차이 유의미 도메인 모델링 변경에 해당하며 팀 협의 필요 구조적으로 더 나은 방향이나, 당장은 변경 범위가 작은 쪽으로 해결 후 팀 협의 사항으로 분리
IN 절 배치 조회 findAllById() + @Transactional(readOnly = true) 엔티티 변경 없음. 즉시 적용 가능. 추가 구현 불필요 SELECT 2개 (1개가 아닌). IN 절 ID가 수천 개 이상이면 파싱 비용 증가 가능 채택
Hibernate @batchsize @BatchSize(size = N) 기존 코드 변경 최소화 @ManyToOne 연관관계 필요 → 현재 적용 불가 탈락
JPQL 세타 조인 연관관계 없이 FROM A, B WHERE A.id = B.id SELECT 1개. 엔티티 변경 없음 Object[] 반환으로 타입 안전성 낮음 보류

결정 근거:

  • Concert와 ConcertSchedule은 변경 빈도, 접근 패턴, 생명주기가 달라 별도 변경 단위로 분리하고 ID 참조하는 구조. 이 설계를 유지하면서 즉시 효과가 있는 해결을 우선
  • 연관관계 추가는 도메인 모델링 변경이므로 팀 협의 사항으로 남겨두고, 당장 두드러지는 불필요 쿼리 수를 줄이는 데 집중
  • 공연 도메인 특성상 "현 시점 예약 가능한 공연 일정"은 현실적으로 수십~수백 건이므로 IN 절 상한 도달 가능성 극히 낮음
  • 요청당 DB 커맨드 21개 → 5개로 절감 (76% 감소)

변경 내용

  • 건별 getConcert() 루프 호출을 findAllById() IN 절 배치 조회로 변경
  • 서비스 메서드에 @Transactional(readOnly = true) 적용하여 트랜잭션 경계 통합
  • ConcertScheduleWithConcert 도메인 조합 객체 추가 (서비스가 DTO를 반환하지 않도록 유지)
  • 핸들러 메서드의 orchestration 책임을 서비스로 이동

결과

항목 변경 전 (N=5) 변경 후
SELECT 수 6 (1+5) 2
트랜잭션 부수 명령 15 (5×3) 3 (1세트)
총 DB 커맨드 21 5
커맨드 감소율 - 76%
| 2026-02-08 17:17:15.278539 | Query        | SELECT event_time, command_type, CONVERT(argument USING utf8) AS query_text FROM mysql.general_log WHERE command_type = 'Query' ORDER BY event_time DESC LIMIT 150                                                                                                                                                                                                                          |
| 2026-02-08 17:17:12.878704 | Query        | SET SESSION TRANSACTION READ WRITE                                                                                                                                                                                                                                                                                                                                                          |
| 2026-02-08 17:17:12.877904 | Query        | COMMIT                                                                                                                                                                                                                                                                                                                                                                                      |
| 2026-02-08 17:17:12.876261 | Query        | select cje1_0.concert_id,cje1_0.artist,cje1_0.created_at,cje1_0.name,cje1_0.updated_at from `concert` cje1_0 where cje1_0.concert_id in (808385841816513892)                                                                                                                                                                                                                                |
| 2026-02-08 17:17:12.872730 | Query        | select csje1_0.concert_schedule_id,csje1_0.availability,csje1_0.concert_id,csje1_0.created_at,csje1_0.datetime,csje1_0.reservation_end_at,csje1_0.reservation_start_at,csje1_0.updated_at from `concertschedule` csje1_0 where csje1_0.reservation_start_at<='2026-02-08 17:17:12.869664' and csje1_0.reservation_end_at>='2026-02-08 17:17:12.869664' and csje1_0.availability='AVAILABLE' |
| 2026-02-08 17:17:12.871115 | Query        | SET SESSION TRANSACTION READ ONLY  

위와 같이 SELECT 쿼리 2개 + 트랜잭션 1 세트(SET SESSION.. 2개 + COMMIT) 으로 불필요한 트랜잭션 관련 부수 명령으로 인한 쿼리가 줄어들었음을 확인했습니다.

🇺🇸 English
mysql> SELECT event_time, command_type, CONVERT(argument USING utf8) AS query_text FROM mysql.general_log WHERE command_type = 'Query' ORDER BY event_time DESC LIMIT 150;
+----------------------------+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| event_time                 | command_type | query_text                                                                                                                                                                                                                                                                                                                                                                                  |
+----------------------------+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 2026-02-08 09:17:42.611056 | Query        | SET GLOBAL general_log = OFF                                                                                                                                                                                                                                                                                                                                                                |
| 2026-02-08 09:17:41.532639 | Query        | ROLLBACK                                                                                                                                                                                                                                                                                                                                                                                    |
| 2026-02-08 09:17:41.531712 | Query        | select csje1_0.concert_schedule_id,csje1_0.availability,csje1_0.concert_id,csje1_0.created_at,csje1_0.datetime,csje1_0.reservation_end_at,csje1_0.reservation_start_at,csje1_0.updated_at from `concertschedule` csje1_0 where csje1_0.reservation_start_at<='2026-02-08 09:17:41.53049' and csje1_0.reservation_end_at>='2026-02-08 09:17:41.53049'                                        |
| 2026-02-08 09:17:40.371555 | Query        | COMMIT                                                                                                                                                                                                                                                                                                                                                                                      |
| 2026-02-08 09:17:40.370612 | Query        | select rje1_0.reservation_id,rje1_0.concert_schedule_id,rje1_0.created_at,rje1_0.expired_at,rje1_0.price,rje1_0.seat_id,rje1_0.status,rje1_0.updated_at,rje1_0.user_id from `reservation` rje1_0 where rje1_0.status='PREEMPTED' and rje1_0.expired_at<'2026-02-08 09:17:40.370165'                                                                                                         |
| 2026-02-08 09:17:40.369727 | Query        | SELECT @@session.transaction_isolation                                                                                                                                                                                                                                                                                                                                                      |
| 2026-02-08 09:17:40.136570 | Query        | SET SESSION TRANSACTION READ WRITE                                                                                                                                                                                                                                                                                                                                                          |
| 2026-02-08 09:17:40.135910 | Query        | COMMIT                                                                                                                                                                                                                                                                                                                                                                                      |
| 2026-02-08 09:17:40.135053 | Query        | select cje1_0.concert_id,cje1_0.artist,cje1_0.created_at,cje1_0.name,cje1_0.updated_at from `concert` cje1_0 where cje1_0.concert_id=808261158700165387                                                                                                                                                                                                                                     |
| 2026-02-08 09:17:40.134247 | Query        | SET SESSION TRANSACTION READ ONLY                                                                                                                                                                                                                                                                                                                                                           |
| 2026-02-08 09:17:40.133040 | Query        | select csje1_0.concert_schedule_id,csje1_0.availability,csje1_0.concert_id,csje1_0.created_at,csje1_0.datetime,csje1_0.reservation_end_at,csje1_0.reservation_start_at,csje1_0.updated_at from `concertschedule` csje1_0 where csje1_0.reservation_start_at<='2026-02-08 09:17:40.131811' and csje1_0.reservation_end_at>='2026-02-08 09:17:40.131811' and csje1_0.availability='AVAILABLE' |
| 2026-02-08 09:17:39.945341 | Query        | SHOW GLOBAL STATUS                                                                                                                                                                                                                                                                                                                                                                          |
+----------------------------+--------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
12 rows in set, 1 warning (0.00 sec)

Background

After introducing server-side DTO aggregation to eliminate N+1 API calls from the frontend, load testing under identical conditions revealed a ~30% throughput drop (previous ~900 RPS → ~630 RPS saturation point).

Root Cause

Layered elimination diagnosis (DB CloudWatch → app server resources → HikariCP → MySQL QPS → General Log) identified two compounding issues:

1. N+1 Query Pattern

The handler method iterated over concert schedule results, invoking getConcert() per schedule, causing individual SELECT queries per iteration.

2. Spring Data JPA Implicit Transaction Overhead

Although getConcert() had no explicit @Transactional, SimpleJpaRepository's class-level @Transactional(readOnly = true) caused each findById() call to open an independent transaction. This resulted in repeated SET SESSION TRANSACTION READ ONLYCOMMITSET SESSION TRANSACTION READ WRITE auxiliary commands per call.

Combined, these issues inflated DB commands from 2 SELECTs to 21 commands per request (N=5).

Solution Comparison and Decision

Approach Method Pros Cons Verdict
JPA association + JOIN FETCH Add @ManyToOne, use JOIN FETCH Single SELECT. Meaningful query reduction at high RPS Constitutes a domain modeling change requiring team alignment Structurally better long-term, but scoped as separate team discussion. Prioritized immediate fix with minimal change scope
IN-clause batch fetch findAllById() + @Transactional(readOnly = true) No entity changes. Immediately applicable. No additional implementation needed 2 SELECTs instead of 1. Potential parsing cost if IN-clause IDs exceed thousands Adopted
Hibernate @batchsize @BatchSize(size = N) Minimal code changes Requires @ManyToOne association → not applicable Rejected
JPQL theta join FROM A, B WHERE A.id = B.id without associations Single SELECT. No entity changes Object[] return type, low type safety Deferred

Decision rationale:

  • Concert and ConcertSchedule differ in mutation frequency, access patterns, and lifecycle, hence separated as independent units of change with ID references. Prioritized an immediate fix that preserves this structure
  • Adding associations constitutes a domain modeling change requiring team alignment — scoped as a follow-up discussion rather than a unilateral decision
  • Domain characteristics naturally limit "currently reservable schedules" to tens or low hundreds, making IN-clause size limits practically unreachable
  • Reduced DB commands per request from 21 to 5 (76% reduction)

Changes

  • Replaced per-item getConcert() loop with findAllById() IN-clause batch fetch
  • Applied @Transactional(readOnly = true) to the service method to consolidate transaction boundaries
  • Added ConcertScheduleWithConcert domain combination object (maintaining the principle that services don't return DTOs)
  • Moved orchestration responsibility from handler to service layer

Result

Metric Before (N=5) After
SELECT count 6 (1+5) 2
Transaction overhead 15 (5×3) 3 (1 set)
Total DB commands 21 5
Reduction - 76%
| 2026-02-08 17:17:15.278539 | Query        | SELECT event_time, command_type, CONVERT(argument USING utf8) AS query_text FROM mysql.general_log WHERE command_type = 'Query' ORDER BY event_time DESC LIMIT 150                                                                                                                                                                                                                          |
| 2026-02-08 17:17:12.878704 | Query        | SET SESSION TRANSACTION READ WRITE                                                                                                                                                                                                                                                                                                                                                          |
| 2026-02-08 17:17:12.877904 | Query        | COMMIT                                                                                                                                                                                                                                                                                                                                                                                      |
| 2026-02-08 17:17:12.876261 | Query        | select cje1_0.concert_id,cje1_0.artist,cje1_0.created_at,cje1_0.name,cje1_0.updated_at from `concert` cje1_0 where cje1_0.concert_id in (808385841816513892)                                                                                                                                                                                                                                |
| 2026-02-08 17:17:12.872730 | Query        | select csje1_0.concert_schedule_id,csje1_0.availability,csje1_0.concert_id,csje1_0.created_at,csje1_0.datetime,csje1_0.reservation_end_at,csje1_0.reservation_start_at,csje1_0.updated_at from `concertschedule` csje1_0 where csje1_0.reservation_start_at<='2026-02-08 17:17:12.869664' and csje1_0.reservation_end_at>='2026-02-08 17:17:12.869664' and csje1_0.availability='AVAILABLE' |
| 2026-02-08 17:17:12.871115 | Query        | SET SESSION TRANSACTION READ ONLY  

As shown above, the request now completes with 2 SELECT queries + 1 transaction set (2 SET SESSION.. + COMMIT), confirming the elimination of redundant transaction auxiliary commands.

Summary by CodeRabbit

  • New Features
    • The concert-schedules endpoint now returns concert details bundled with each schedule, simplifying responses and reducing extra lookups.
  • Bug Fixes / Stability
    • Improved handling when concert data is missing: schedules without matching concert info are filtered out to avoid incomplete entries.
  • Tests
    • Updated tests to cover the combined schedule+concert responses and the filtering behavior.

@coderabbitai

coderabbitai Bot commented Feb 8, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@leonroars has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 56 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This change adds a composite record ConcertScheduleWithConcert, replaces schedule-only retrieval with batch-loaded schedule+concert results, updates repository/service/application/controller to use the composite type, and adjusts tests to exercise the new API and behavior.

Changes

Cohort / File(s) Summary
New Domain Model
src/main/java/com/slam/concertreservation/domain/concert/model/ConcertScheduleWithConcert.java
Adds a Java record that holds a ConcertSchedule and its associated Concert.
Repository Layer
src/main/java/com/slam/concertreservation/domain/concert/repository/ConcertRepository.java, src/main/java/com/slam/concertreservation/infrastructure/persistence/jpa/impl/ConcertRepositoryImpl.java
Adds findAllById(List<Long> ids) to fetch multiple Concert entities and an implementation delegating to the JPA repository and mapping entities to domain objects.
Service Layer
src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java
Replaces getAvailableConcertSchedule(LocalDateTime) with getAvailableConcertScheduleWithConcert(LocalDateTime) that: fetches available schedules, collects concert IDs, batch-loads concerts, maps schedules to ConcertScheduleWithConcert, and filters missing concerts.
Application & Controller
src/main/java/com/slam/concertreservation/application/facade/ConcertReservationApplication.java, src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java
Updates application API method name/signature to return List<ConcertScheduleWithConcert> and adapts controller mapping to build responses directly from the composite record.
Tests
src/test/java/com/slam/concertreservation/application/NonConcurrentConcertReservationIntegrationTest.java, src/test/java/com/slam/concertreservation/domain/concert/service/ConcertServiceUnitTest.java
Updates integration test to use the new API and adds unit tests for getAvailableConcertScheduleWithConcert, including behavior when some concerts are missing.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Controller as "Controller\n(HTTP)"
    participant App as "ConcertReservationApplication"
    participant Service as "ConcertService"
    participant Repo as "ConcertRepository"
    participant DB as "Database / JPA"

    Client->>Controller: GET /available-schedules
    Controller->>App: requestAvailableSchedulesWithConcert()
    App->>Service: getAvailableConcertScheduleWithConcert(now)
    Service->>Repo: findAvailableSchedules(presentDateTime)
    Repo->>DB: query schedules
    DB-->>Repo: schedules
    Repo-->>Service: List<ConcertSchedule>
    Service->>Service: extract distinct concertIds
    Service->>Repo: findAllById(concertIds)
    Repo->>DB: query concerts by ids
    DB-->>Repo: concerts
    Repo-->>Service: List<Concert>
    Service->>Service: map concerts by id & pair each schedule
    Service-->>App: List<ConcertScheduleWithConcert>
    App-->>Controller: composite list
    Controller-->>Client: HTTP 200 with mapped responses
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I hopped through code with nimble paws,
Paired schedules with concerts—no more claws.
Batch-fetching carrots, one tidy tray,
Each schedule finds its concert today.
Hop, map, and return — a joyful stay! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: eliminating N+1 queries and transaction overhead in the schedule inquiry API through batch fetching and transaction boundary consolidation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix_schedule-api-n1-query

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/test/java/com/slam/concertreservation/application/NonConcurrentConcertReservationIntegrationTest.java (1)

176-189: ⚠️ Potential issue | 🟠 Major

Test registers a schedule with concertId=1L but never creates a corresponding Concert.

With the new batch-fetch logic, ConcertService.getAvailableConcertScheduleWithConcert() now calls findAllById to load the Concert for each schedule. Since no Concert with id=1L is created in this test, the concert lookup will return an empty result, likely causing the schedule to be dropped from the ConcertScheduleWithConcert list (or paired with null). This test may pass incidentally if other tests create that concert first, making it order-dependent and fragile.

Consider creating a concert explicitly before registering the schedule:

Proposed fix
         void shouldSuccessfullyGetAvailableConcertSchedule() {
             // given
             // Create an available schedule (start time <= now)
+            Concert concert = concertReservationApplication.registerConcert(concertName, artistName);
             ConcertSchedule expected = concertReservationApplication.registerConcertSchedule(1L,
-                    concertDateTime.plusDays(1), LocalDateTime.now().minusMinutes(1), reservationEndAt.plusDays(1),
+                    concert.getId(),
+                    concertDateTime.plusDays(1), LocalDateTime.now().minusMinutes(1), reservationEndAt.plusDays(1),
                     1000);

Note: registerConcertSchedule takes concertId as the first parameter, so use the actual concert's ID instead of the hardcoded 1L.

🤖 Fix all issues with AI agents
In
`@src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java`:
- Around line 284-295: In the batch mapping inside ConcertService (the code
using concertRepository.findAllById(...), the schedules.stream().map(... -> new
ConcertScheduleWithConcert(...))), guard against missing concerts by checking
concerts.get(schedule.getConcertId()) and fail fast: if null throw the same
UnavailableRequestException used previously (include schedule.getConcertId() /
schedule id in the message for context). Update the mapping logic so each
schedule either resolves to a ConcertScheduleWithConcert with a non-null Concert
or immediately throws UnavailableRequestException when the concert is absent.

In
`@src/main/java/com/slam/concertreservation/infrastructure/persistence/jpa/impl/ConcertRepositoryImpl.java`:
- Around line 30-36: The current findAllById implementation
(ConcertRepositoryImpl.findAllById) can return fewer Concerts than requested
which leads to nulls when matching schedules; update the code that builds
schedule responses to either (a) validate that the returned list from
findAllById contains all requested IDs (compare size or missing IDs) and throw a
clear exception before calling ConcertScheduleResponse.from, or (b) filter out
ConcertSchedule entries that reference missing concerts by checking the map
built from
concertJpaRepository.findAllById(ids).stream().map(ConcertJpaEntity::toDomain)
for null/missing keys and skipping those schedules before invoking
ConcertScheduleResponse.from(schedule, concert); choose one approach and apply
it where ConcertScheduleResponse.from is called to prevent NullPointerException.
🧹 Nitpick comments (3)
src/test/java/com/slam/concertreservation/domain/concert/service/ConcertServiceUnitTest.java (1)

327-352: Good coverage for the happy path.

The test correctly verifies that getAvailableConcertScheduleWithConcert pairs schedules with their concerts. Consider adding a test for the edge case where findAllAvailable returns schedules but findAllById returns fewer concerts (e.g., data inconsistency), to document the expected behavior — silent drop vs. exception.

src/main/java/com/slam/concertreservation/domain/concert/service/ConcertService.java (2)

284-288: Use a static import or regular import for Collectors instead of inline FQN.

java.util.stream.Collectors.toMap is referenced with a fully-qualified name while the rest of the file uses standard imports. This is a minor inconsistency.

✏️ Suggested fix

Add at the import section:

import java.util.stream.Collectors;

Then simplify:

         Map<Long, Concert> concerts = concertRepository.findAllById(concertIds)
                 .stream()
                 .collect(
-                        java.util.stream.Collectors.toMap(Concert::getId, concert -> concert));
+                        Collectors.toMap(Concert::getId, concert -> concert));

270-296: Consider short-circuiting when there are no available schedules.

If schedules is empty, the subsequent findAllById call with an empty list is a wasted round-trip (even though it's harmless). An early return avoids the unnecessary DB call and stream processing.

✏️ Suggested early return
         List<ConcertSchedule> schedules = concertScheduleRepository.findAllAvailable(presentDateTime);
+
+        if (schedules.isEmpty()) {
+            return List.of();
+        }
 
         // 예약 가능한 공연 ID 목록 추출

Comment on lines +30 to +36
@Override
public List<Concert> findAllById(List<Long> ids) {
return concertJpaRepository.findAllById(ids)
.stream()
.map(ConcertJpaEntity::toDomain)
.toList();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n -A 30 'getAvailableConcertScheduleWithConcert' --type=java -g '!*Test*'

Repository: leonroars/slam

Length of output: 7769


🏁 Script executed:

rg -n 'class ConcertScheduleWithConcert' --type=java -A 15

Repository: leonroars/slam

Length of output: 40


🏁 Script executed:

rg -n 'ConcertScheduleWithConcert' --type=java | head -20

Repository: leonroars/slam

Length of output: 2909


🏁 Script executed:

cat -n src/main/java/com/slam/concertreservation/domain/concert/model/ConcertScheduleWithConcert.java

Repository: leonroars/slam

Length of output: 402


🏁 Script executed:

rg -n -B 5 -A 10 'getAvailableConcertSchedulesWithConcert' src/main/java/com/slam/concertreservation/interfaces/ConcertReservationController.java

Repository: leonroars/slam

Length of output: 694


🏁 Script executed:

rg -n -B 3 -A 15 'class ConcertScheduleResponse' --type=java -g '!*Test*' | head -50

Repository: leonroars/slam

Length of output: 2441


findAllById doesn't guarantee all requested IDs are found — will cause NullPointerException.

Spring Data JPA's findAllById returns only entities that exist. If a ConcertSchedule references a concertId that doesn't exist in the DB, concerts.get(schedule.getConcertId()) returns null. This null Concert is then passed to ConcertScheduleResponse.from(schedule, concert), which calls concert.getName() and concert.getArtist() (lines 25-26 in ConcertScheduleResponse), causing a NullPointerException at runtime.

Either validate that all Concert IDs exist before creating results, or filter out schedules with missing concerts.

🤖 Prompt for AI Agents
In
`@src/main/java/com/slam/concertreservation/infrastructure/persistence/jpa/impl/ConcertRepositoryImpl.java`
around lines 30 - 36, The current findAllById implementation
(ConcertRepositoryImpl.findAllById) can return fewer Concerts than requested
which leads to nulls when matching schedules; update the code that builds
schedule responses to either (a) validate that the returned list from
findAllById contains all requested IDs (compare size or missing IDs) and throw a
clear exception before calling ConcertScheduleResponse.from, or (b) filter out
ConcertSchedule entries that reference missing concerts by checking the map
built from
concertJpaRepository.findAllById(ids).stream().map(ConcertJpaEntity::toDomain)
for null/missing keys and skipping those schedules before invoking
ConcertScheduleResponse.from(schedule, concert); choose one approach and apply
it where ConcertScheduleResponse.from is called to prevent NullPointerException.

@leonroars
leonroars merged commit bf9c350 into main Feb 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant