Skip to content

[FEAT] 백킹트랙 기반 연주 세션 시작 API 구현 - #85

Merged
on1yoneprivate merged 10 commits into
developfrom
feat/#73-start-playing-session
Jul 31, 2026
Merged

[FEAT] 백킹트랙 기반 연주 세션 시작 API 구현#85
on1yoneprivate merged 10 commits into
developfrom
feat/#73-start-playing-session

Conversation

@on1yoneprivate

@on1yoneprivate on1yoneprivate commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📍 개요

백킹트랙 기반 연주 세션 시작 API 구현

⛓️‍💥 관련 이슈


🛠️ 작업 내용

  • 백킹트랙 기반 연주 세션 시작 요청·응답 DTO 추가
  • 로그인 사용자 및 백킹트랙 조회 로직 구현
  • 백킹트랙 공개 범위에 따른 접근 권한 검증 추가
  • READY 상태의 연주 세션 생성 후 start()를 호출하여 IN_PROGRESS 상태로 전환
  • 백킹트랙 재생 정보 및 코드 진행 정보를 포함한 응답 구현
  • POST /api/playings 연주 시작 API 추가
  • 잘못된 백킹트랙 ID, 미존재 백킹트랙, 접근 권한 관련 예외 처리
  • 연주 시작 Service 및 Playing 엔티티 테스트 추가

🔥 리뷰 요청 사항

리뷰어가 중점적으로 확인해주었으면 하는 내용을 작성해주세요.

  • 연주 세션을 READY 상태로 생성한 뒤 start()를 통해 IN_PROGRESS로 전환하는 흐름이 적절한지
    • 대시보드 설정 후 연주를 바로 시작하지 않을 수 있다는 가정하에 READY → IN_PROGRESS로 설정해 두었습니다.

✅ 체크리스트

  • 코드 컨벤션을 준수했습니다.
  • 불필요한 코드 및 import를 제거했습니다.
  • 예외 처리를 적용했습니다.
  • 테스트를 완료했습니다.
  • 관련 Issue를 연결했습니다.

📎 참고 사항

Summary by CodeRabbit

  • 새 기능

    • 연주 세션을 시작하고 백킹 트랙 및 코드 진행 정보를 확인할 수 있습니다.
    • 연주 세션별 MIDI 이벤트를 저장할 수 있습니다.
    • MIDI 이벤트의 형식, 범위, 개수 및 순서를 검증합니다.
    • 연주 소유자와 접근 권한을 확인합니다.
  • 버그 수정

    • MIDI 데이터 정규화 및 연주 완료 처리를 개선했습니다.
    • 잘못된 요청과 접근 권한 오류를 더욱 세분화해 안내합니다.
  • 테스트

    • 연주 시작, MIDI 저장, 입력 검증 및 오류 상황 테스트를 추가했습니다.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@on1yoneprivate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 27ce1a51-d6d7-46b6-9643-334399604cc3

📥 Commits

Reviewing files that changed from the base of the PR and between f1dc6d4 and 7ac572c.

📒 Files selected for processing (2)
  • src/main/java/com/mr/domain/playing/exception/PlayingErrorStatus.java
  • src/main/java/com/mr/domain/playing/repository/PlayingRepository.java
📝 Walkthrough

Walkthrough

백킹트랙 기반 연주 세션 시작 및 MIDI 이벤트 저장 API를 추가했습니다. 요청·응답 DTO, 검증 오류, MIDI 정규화·완료 처리, 서비스·저장소·컨트롤러 연결과 테스트를 구현했습니다.

Changes

연주 세션 및 MIDI 처리

Layer / File(s) Summary
API 계약과 오류 상태
src/main/java/com/mr/domain/playing/constant/*, src/main/java/com/mr/domain/playing/dto/**, src/main/java/com/mr/domain/playing/exception/*, src/test/java/com/mr/domain/playing/dto/**
연주 시작·MIDI 저장 DTO, 이벤트 제한, 응답 매핑과 MIDI 전용 오류 상태를 추가했습니다. Bean Validation 동작을 테스트했습니다.
MIDI 정규화와 Playing 상태 전이
src/main/java/com/mr/domain/playing/entity/*, src/main/java/com/mr/domain/playing/exception/PlayingErrorStatus.java, src/test/java/com/mr/domain/playing/entity/PlayingTest.java
MIDI 이벤트를 검증·필터링·정렬하고 Playing을 완료 상태로 전환합니다. 소유자 검증과 종료 시각·재생 시간 계산을 적용했습니다.
서비스·저장소·컨트롤러 연결
src/main/java/com/mr/domain/playing/controller/*, src/main/java/com/mr/domain/playing/service/*, src/main/java/com/mr/domain/playing/repository/*, src/test/java/com/mr/domain/playing/controller/*, src/test/java/com/mr/domain/playing/service/*
인증 사용자와 백킹트랙을 검증하고 세션 시작 및 MIDI 저장을 API에 연결했습니다. 정상·예외 흐름을 테스트했습니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PlayingController
  participant PlayingService
  participant PlayingRepository
  participant Playing
  Client->>PlayingController: 세션 시작 또는 MIDI 저장 요청
  PlayingController->>PlayingService: 인증 사용자 ID와 요청 전달
  PlayingService->>PlayingRepository: 사용자·백킹트랙·Playing 조회
  PlayingService->>Playing: 세션 시작 또는 MIDI 완료 처리
  PlayingService->>PlayingRepository: Playing 저장
  PlayingService-->>PlayingController: 응답 DTO 반환
  PlayingController-->>Client: ApiResponse 반환
Loading

Possibly related PRs

  • Musereview/BE#72: MIDI 이벤트 API와 Playing 처리 흐름이 직접 연결됩니다.
  • Musereview/BE#15: Playing의 MIDI 완료 처리와 MidiEventData 검증을 확장합니다.
  • Musereview/BE#24: 백킹트랙과 코드 진행 데이터를 세션 시작 응답에 사용합니다.

Poem

이벤트가 시간순으로 서고
Playing은 연주를 시작해요
검증을 통과한 MIDI가
완료 상태에 저장됩니다 🎵

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning MIDI 이벤트 저장 API와 정규화·검증 로직은 직접 이슈 [#73]의 세션 시작 범위를 벗어납니다. MIDI 이벤트 저장 관련 코드와 테스트를 별도 pull request로 분리하거나, 이슈 [#73]에 해당 범위를 명시하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.01% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 백킹트랙 기반 연주 세션 시작 API라는 핵심 변경을 정확하고 간결하게 설명합니다.
Linked Issues check ✅ Passed 직접 이슈 [#73]의 세션 생성, 접근 권한 검증, 응답 구성, 예외 처리 요구사항을 구현했습니다.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/#73-start-playing-session
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#73-start-playing-session

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.

@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: 5

🧹 Nitpick comments (6)
src/test/java/com/mr/domain/playing/entity/PlayingTest.java (1)

29-44: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

startPlayingstartPlaying_success(Line 568-595)가 사실상 같은 시나리오입니다.

후자가 시작 시각의 구간(isBetween)까지 단정하는 상위 집합이므로, 앞의 테스트는 삭제해도 커버리지가 줄지 않습니다. 이름이 비슷한 테스트가 둘 있으면 나중에 어느 쪽을 고쳐야 할지 헷갈립니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/mr/domain/playing/entity/PlayingTest.java` around lines 29
- 44, Remove the redundant startPlaying test method near the top of PlayingTest,
keeping the more comprehensive startPlaying_success test that also validates the
startedAt time range. Do not alter the remaining test coverage or behavior.
src/test/java/com/mr/domain/playing/service/PlayingServiceTest.java (1)

607-648: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

mockUser()mockBackingTrack()은 아무도 호출하지 않습니다.

startPlaying_successsetUp()에서 만든 목에 직접 스터빙(Line 440-456)하고 있어 이 두 헬퍼는 순수 죽은 코드입니다. 게다가 안에 given(...)이 여러 개 있어 나중에 그대로 호출하면 MockitoExtension의 STRICT_STUBS 정책 때문에 UnnecessaryStubbingException으로 이어질 수 있습니다. 삭제하거나, 헬퍼로 통일하되 필요한 스터빙만 남기는 쪽을 권합니다.

참고: Mockito 공식 문서의 Strictness / UnnecessaryStubbingException 절이 이 동작을 잘 설명합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/mr/domain/playing/service/PlayingServiceTest.java` around
lines 607 - 648, Remove the unused mockUser() and mockBackingTrack() helper
methods from PlayingServiceTest, or replace the setUp() stubbing used by
startPlaying_success with these helpers while retaining only the stubbings
actually required by the test. Ensure no unused Mockito stubs remain under
MockitoExtension strict stubbing.
src/main/java/com/mr/domain/playing/entity/Playing.java (2)

238-239: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

hasInvalidEventOrder 검사는 사실상 도달할 수 없습니다.

MidiEventData의 private 생성자가 validateSequence/validateTimestampMs로 null·음수를 이미 거르므로(MidiEventData.java Line 55-83), 여기까지 온 MidiEventData 인스턴스의 sequence/timestampMs는 항상 non-null·비음수입니다. 즉 Line 238-239 분기는 커버할 수 없는 코드이며, 실제로 PlayingTest에도 이 분기를 겨냥한 테스트가 없습니다.

값 객체의 불변식을 신뢰해 제거하는 편이 의도를 더 잘 드러냅니다. 반대로 "역직렬화 등 우회 경로가 있으니 방어적으로 남긴다"가 의도라면, 주석 한 줄로 근거를 남겨 두면 다음 사람이 지우지 않습니다.

♻️ 제안 diff (제거안)
         if (midiData.stream().anyMatch(Objects::isNull)) {
             throw new GeneralException(MidiEventErrorStatus.INVALID_MIDI_EVENT);
         }
-
-        if (midiData.stream().anyMatch(Playing::hasInvalidEventOrder)) {
-            throw new GeneralException(MidiEventErrorStatus.INVALID_MIDI_EVENT);
-        }
-    private static boolean hasInvalidEventOrder(
-            MidiEventData event
-    ) {
-        return event.getTimestampMs() == null
-                || event.getTimestampMs() < 0
-                || event.getSequence() == null
-                || event.getSequence() < 0;
-    }

Also applies to: 338-345

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/playing/entity/Playing.java` around lines 238 -
239, Remove the unreachable hasInvalidEventOrder validation branches in the
relevant Playing methods, including the logic around the shown stream check and
the additionally referenced section. Rely on MidiEventData’s validated
value-object invariants for sequence and timestampMs; if defensive checks must
remain for bypassed construction paths, retain them only with a concise comment
documenting that rationale.

230-232: 🚀 Performance & Scalability | 🔵 Trivial

단일 jsonb 컬럼에 최대 10만 이벤트 — 운영 관점 사전 점검을 권합니다.

이벤트 하나가 JSON으로 대략 5070바이트라면 상한치에서 midi_data 한 행이 57MB에 달합니다. 아래 항목을 미리 확인해 두면 프로덕션에서 놀라는 일이 줄어듭니다.

  • 요청 본문 크기 제한(spring.servlet.multipart / server.max-http-request-header-size가 아닌 WAS 바디 한계, 리버스 프록시 client_max_body_size)
  • PostgreSQL TOAST 압축·해제 비용과 Playing 조회 시 이 컬럼이 함께 로딩되는 경로(목록 조회에서 midi_data를 제외할 수 있는지)
  • @Version 낙관적 락과 결합된 대형 행 UPDATE의 WAL 증가량

장기적으로는 MIDI 이벤트를 별도 테이블 또는 오브젝트 스토리지로 분리하는 편이 조회 성능과 백업 비용 모두에 유리합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/playing/entity/Playing.java` around lines 230 -
232, Review the 100,000-event limit enforced in Playing by MAX_MIDI_EVENT_COUNT
and validate the end-to-end operational limits for large midiData payloads,
including request-body and proxy size limits, PostgreSQL TOAST behavior, eager
loading in list queries, and `@Version` update/WAL impact. Preserve the current
validation while documenting or adjusting configuration and query/storage design
as needed to prevent oversized JSONB rows in production.
src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java (1)

16-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

MIDI 제약 값이 DTO·엔티티·검증 메시지 세 곳에 흩어져 있습니다. MAX_MIDI_EVENT_COUNTMidiEventConstants로 잘 뽑아냈지만, 개수 상한이 메시지 문자열에 다시 하드코딩되고 피치/강도 범위(0, 127)는 두 계층에 각각 리터럴로 남아 있어 한쪽만 바뀌면 계층 간 계약이 조용히 어긋납니다. 상수 클래스 한 곳으로 모으는 것이 근본 해법입니다.

  • src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java#L16-L18: @Size 메시지의 100,000{max} 보간으로 바꾸고, @Min/@Max0·127MidiEventConstantsstatic final int 상수로 교체하세요(MidiEventSaveRequestTest가 메시지를 문자 단위로 단정하므로 함께 갱신 필요).
  • src/main/java/com/mr/domain/playing/entity/MidiEventData.java#L57-L82: validatePitch/validateVelocity0·127 리터럴을 같은 상수로 교체하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java` around
lines 16 - 18, Centralize all MIDI constraint values in MidiEventConstants: in
src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java lines
16-18, use {max} in the `@Size` message and replace `@Min/`@Max literals 0 and 127
with the corresponding constants, then update MidiEventSaveRequestTest’s exact
message assertions; in
src/main/java/com/mr/domain/playing/entity/MidiEventData.java lines 57-82,
replace the 0 and 127 literals in validatePitch and validateVelocity with those
same constants.
src/test/java/com/mr/domain/playing/controller/PlayingControllerTest.java (1)

270-310: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

10만 건 페이로드를 매번 직렬화하는 컨트롤러 테스트는 CI 시간을 잡아먹습니다.

MAX_MIDI_EVENT_COUNT + 1개의 DTO 생성 + Jackson 직렬화 + MockMvc 왕복이라 수백 MB급 문자열이 오갈 수 있습니다. @Size 어노테이션이 걸려 있다는 사실은 이미 MidiEventSaveRequestTest처럼 Validator를 직접 호출하는 단위 테스트에서 훨씬 저렴하게 검증할 수 있고, HTTP 계층 테스트에서는 대표 케이스 하나만 남기는 편이 실용적입니다.

경계값을 HTTP 계층에서 꼭 확인하려면 JSON 문자열을 스트림으로 만들어 넘기는 방식이 그나마 메모리에 유리합니다. 판단은 팀 CI 예산에 맡기겠습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/mr/domain/playing/controller/PlayingControllerTest.java`
around lines 270 - 310, Replace the large-payload HTTP test in
saveMidiEvents_eventCountExceeded with a lightweight representative controller
validation case, removing the MAX_MIDI_EVENT_COUNT + 1 DTO construction and
Jackson serialization; keep the exact `@Size` boundary assertion in
MidiEventSaveRequestTest via direct Validator testing, or use streamed JSON only
if the HTTP boundary must remain covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/mr/domain/playing/entity/Playing.java`:
- Around line 197-206: Update Playing.completeWithMidiData() and
normalizeMidiData() to track discarded MIDI events and detect when
timestamp-based filtering exceeds the configured MIDI_TIMESTAMP_TOLERANCE_MS
threshold relative to the request. Reject or warn and require retransmission
before setting PlayingStatus.COMPLETED; retain EMPTY_MIDI_EVENTS for the
all-discarded case, and ensure the discard count is explicitly exposed if
MidiEventSaveResponse is used to report save results.

In `@src/main/java/com/mr/domain/playing/service/PlayingService.java`:
- Around line 73-85: 원자성이 보장되지 않는 validateMidiSaveRequestInterval(userId)의 조회 후
completeWithMidiData(midiEvents) 완료 흐름을 수정하세요. 동일 userId의 동시 요청이 하나만 1분 제한을
통과하도록 최근 완료 이력 조회에 PESSIMISTIC_WRITE 잠금을 적용하거나 DB 기본값/애너테이션 기반 원자적 rate-limit
삽입을 사용하고, 기존 MIDI 이벤트 변환 및 완료 동작은 유지하세요.

In `@src/test/java/com/mr/domain/playing/controller/PlayingControllerTest.java`:
- Around line 72-76: Update the CustomUserDetails construction in
PlayingControllerTest to use the existing USER_ID constant instead of the
hardcoded 1L, keeping the authenticated principal ID consistent with the
verification at line 141.

In `@src/test/java/com/mr/domain/playing/dto/req/MidiEventSaveRequestTest.java`:
- Around line 60-69: Update the request DTO’s event list declaration, likely the
field or accessor in MidiEventSaveRequest, to apply a non-null container-element
constraint alongside `@Valid` (for example, annotate the generic MidiEventRequest
type argument). Preserve cascade validation for non-null elements and ensure a
list containing null produces a constraint violation as asserted by
MidiEventSaveRequestTest.

In `@src/test/java/com/mr/domain/playing/service/PlayingServiceTest.java`:
- Around line 591-605: Update startPlaying_nullBackingTrackId to construct and
pass a PlayingStartRequest with a null backing-track ID, so it exercises the
DTO’s `@NotNull` validation path; keep the existing exception and
repository-never-save assertions, and cover a null request separately if that
path is required.

---

Nitpick comments:
In `@src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java`:
- Around line 16-18: Centralize all MIDI constraint values in
MidiEventConstants: in
src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java lines
16-18, use {max} in the `@Size` message and replace `@Min/`@Max literals 0 and 127
with the corresponding constants, then update MidiEventSaveRequestTest’s exact
message assertions; in
src/main/java/com/mr/domain/playing/entity/MidiEventData.java lines 57-82,
replace the 0 and 127 literals in validatePitch and validateVelocity with those
same constants.

In `@src/main/java/com/mr/domain/playing/entity/Playing.java`:
- Around line 238-239: Remove the unreachable hasInvalidEventOrder validation
branches in the relevant Playing methods, including the logic around the shown
stream check and the additionally referenced section. Rely on MidiEventData’s
validated value-object invariants for sequence and timestampMs; if defensive
checks must remain for bypassed construction paths, retain them only with a
concise comment documenting that rationale.
- Around line 230-232: Review the 100,000-event limit enforced in Playing by
MAX_MIDI_EVENT_COUNT and validate the end-to-end operational limits for large
midiData payloads, including request-body and proxy size limits, PostgreSQL
TOAST behavior, eager loading in list queries, and `@Version` update/WAL impact.
Preserve the current validation while documenting or adjusting configuration and
query/storage design as needed to prevent oversized JSONB rows in production.

In `@src/test/java/com/mr/domain/playing/controller/PlayingControllerTest.java`:
- Around line 270-310: Replace the large-payload HTTP test in
saveMidiEvents_eventCountExceeded with a lightweight representative controller
validation case, removing the MAX_MIDI_EVENT_COUNT + 1 DTO construction and
Jackson serialization; keep the exact `@Size` boundary assertion in
MidiEventSaveRequestTest via direct Validator testing, or use streamed JSON only
if the HTTP boundary must remain covered.

In `@src/test/java/com/mr/domain/playing/entity/PlayingTest.java`:
- Around line 29-44: Remove the redundant startPlaying test method near the top
of PlayingTest, keeping the more comprehensive startPlaying_success test that
also validates the startedAt time range. Do not alter the remaining test
coverage or behavior.

In `@src/test/java/com/mr/domain/playing/service/PlayingServiceTest.java`:
- Around line 607-648: Remove the unused mockUser() and mockBackingTrack()
helper methods from PlayingServiceTest, or replace the setUp() stubbing used by
startPlaying_success with these helpers while retaining only the stubbings
actually required by the test. Ensure no unused Mockito stubs remain under
MockitoExtension strict stubbing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e79f33b-6760-4f83-ba29-68a306833b71

📥 Commits

Reviewing files that changed from the base of the PR and between 63d6998 and 98234e9.

📒 Files selected for processing (16)
  • src/main/java/com/mr/domain/playing/constant/MidiEventConstants.java
  • src/main/java/com/mr/domain/playing/controller/PlayingController.java
  • src/main/java/com/mr/domain/playing/dto/req/MidiEventSaveRequest.java
  • src/main/java/com/mr/domain/playing/dto/req/PlayingStartRequest.java
  • src/main/java/com/mr/domain/playing/dto/res/MidiEventSaveResponse.java
  • src/main/java/com/mr/domain/playing/dto/res/PlayingStartResponse.java
  • src/main/java/com/mr/domain/playing/entity/MidiEventData.java
  • src/main/java/com/mr/domain/playing/entity/Playing.java
  • src/main/java/com/mr/domain/playing/exception/MidiEventErrorStatus.java
  • src/main/java/com/mr/domain/playing/exception/PlayingErrorStatus.java
  • src/main/java/com/mr/domain/playing/repository/PlayingRepository.java
  • src/main/java/com/mr/domain/playing/service/PlayingService.java
  • src/test/java/com/mr/domain/playing/controller/PlayingControllerTest.java
  • src/test/java/com/mr/domain/playing/dto/req/MidiEventSaveRequestTest.java
  • src/test/java/com/mr/domain/playing/entity/PlayingTest.java
  • src/test/java/com/mr/domain/playing/service/PlayingServiceTest.java

Comment thread src/main/java/com/mr/domain/playing/entity/Playing.java
Comment on lines +73 to +85
validateMidiSaveRequestInterval(userId);

List<MidiEventData> midiEvents = request.events()
.stream()
.map(event -> MidiEventData.of(
event.sequence(),
event.type(),
event.pitch(),
event.velocity(),
event.timestampMs()
)).toList();

playing.completeWithMidiData(midiEvents);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 4 \
  'validateMidiSaveRequestInterval|completeWithMidiData|existsByUser_UserIdAndStatusAndEndedAtAfterAndDeletedAtIsNull' \
  src/main/java/com/mr/domain/playing/service/PlayingService.java

rg -n -C 4 '`@Version`|completeWithMidiData|endedAt|status' \
  src/main/java/com/mr/domain/playing/entity/Playing.java

rg -n -C 4 '`@Lock`|PESSIMISTIC_WRITE|LockModeType' \
  src/main/java/com/mr/domain

Repository: Musereview/BE

Length of output: 7798


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== PlayingService outline =="
ast-grep outline src/main/java/com/mr/domain/playing/service/PlayingService.java || true

echo "== PlayingService relevant sections =="
sed -n '1,180p' src/main/java/com/mr/domain/playing/service/PlayingService.java

echo "== PlayingRepository methods =="
rg -n -C 6 'interface .*Repository|existsByUser_UserIdAndStatusAndEndedAtAfterAndDeletedAtIsNull|save|Lock|PESSIMISTIC|PLAYING_NOT_FOUND|deleteByIdAndDeletedAtIsNull|findByIdAndDeletedAtIsNull' src/main/java/com/mr/domain/playing

echo "== Playing entity relevant sections =="
sed -n '40,230p' src/main/java/com/mr/domain/playing/entity/Playing.java

echo "== Controller save method if present =="
rg -n -C 8 'save|MidiEventSave|playing_id|userId' src/main/java/com/mr/domain/playing | sed -n '1,220p'

Repository: Musereview/BE

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Java/gradle/disk availability =="
command -v java || true
command -v javac || true
command -v gradle || true
command -v mvn || true
javac -version 2>&1 || true

echo "== Minimal Java version-concurrency probe =="
java_home="$(dirname "$(dirname "$(command -v java 2>/dev/null || true)")")"
if [ -n "$java_home" ] && command -v javac >/dev/null 2>&1; then
  tmpdir="$(mktemp -d)"
  cat > "$tmpdir/Probe.java" <<'EOF'
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public class Probe {
  public static void main(String[] args) throws Exception {
    CyclicBarrier c = new CyclicBarrier(2);
    AtomicInteger firstReads = new AtomicInteger();
    AtomicInteger firstCommitted = new AtomicInteger();

    class State {
      boolean recentlyCompleted = false;
      int playedCount = 0;
    }
    var state = new State();

    Runnable request = () -> {
      try {
        boolean beforeCommit1 = state.recentlyCompleted;
        c.await();
        boolean beforeCommit2 = state.recentlyCompleted;
        c.await();
        state.recentlyCompleted = true;
        state.playedCount++;
        c.await();
        if (beforeCommit1 && beforeCommit2) firstCommitted.incrementAndGet();
        else firstReads.incrementAndGet();
      } catch (Exception ignored) {}
    };
    Thread[] ts = new Thread[2];
    for (int i = 0; i < ts.length; i++) ts[i] = new Thread(request);
    for (Thread t : ts) t.start();
    for (Thread t : ts) t.join();
    System.out.println("java.runtime.version=" + System.getProperty("java.runtime.version"));
    System.out.println("current.count=" + state.playedCount);
    System.out.println("firstReads.false=true=" + firstReads.get());
    System.out.println("firstReads.true=true=" + firstCommitted.get());
  }
}
EOF
  javac "$tmpdir/Probe.java"
  java -cp "$tmpdir" Probe
else
  echo "javac/java unavailable or javac command missing"
fi

Repository: Musereview/BE

Length of output: 333


🌐 Web query:

Java ConcurrentModificationException ArrayList parallelStream forEach modification during iteration

💡 Result:

Modifying an ArrayList while iterating over it using parallelStream.forEach is an unsafe, non-compliant operation that violates the core principle of stream non-interference [1][2]. It is strongly discouraged and can lead to several unpredictable outcomes [2][3][4]. The primary risks and behaviors include: 1. ConcurrentModificationException: While the stream framework and the underlying ArrayList spliterator attempt to detect structural modifications (adding, removing, or resizing) in a fail-fast manner, this is done on a "best-effort" basis [5][6][7]. Depending on the timing and the specific implementation of the split, you might encounter a ConcurrentModificationException, but you cannot rely on it to catch all violations [5][6]. 2. Data Loss and Corruption: ArrayList is not thread-safe [6][4]. When a parallel stream modifies the list concurrently, multiple threads may attempt to perform structural modifications simultaneously [8][4]. This can cause race conditions where data is lost (e.g., two threads overwriting the same index) or the internal structure of the ArrayList becomes corrupted, potentially leading to other errors such as ArrayIndexOutOfBoundsException [3][8][4]. 3. Undefined Behavior: Even if an exception is not thrown, the results of the operation are non-deterministic [2][4]. You may experience incorrect data, inconsistent output, or silent failures [2][3][9]. To avoid these issues, follow these best practices: - Do not use stateful lambdas that modify the source collection [3][9][4]. - Use proper Stream collectors (e.g.,.collect(Collectors.toList)) to generate new collections rather than mutating existing ones [3][4]. - If you must accumulate results into a collection, use thread-safe alternatives or, preferably, the built-in reduction operations provided by the Stream API [3][9][8]. As noted in the official documentation, stream pipelines should be non-interfering, meaning the data source should not be modified during the execution of the stream pipeline [1][10]. Relying on fail-fast exceptions like ConcurrentModificationException for program correctness is considered an anti-pattern and a bug [5][6].

Citations:


MIDI 저장 제한을 “조회 + 완료” 단위로 원자화하세요.

validateMidiSaveRequestInterval(userId)exists...로 조회한 뒤 completeWithMidiData()로 상태를 바꿀 때까지, 동일 사용자의 동시 요청은 둘 다 최근 완료 이력이 없다고 읽을 수 있습니다. 같은 userId에 대해 PESSIMISTIC_WRITE 조회하거나 DB 기본값/애너테이션 단위의 원자적 rate-limit 삽입을 사용해 1분 제한을 보장하면 좋습니다. 관련 개념: https://docs.spring.io/spring-framework/reference/data-access/transactional/declarative-locking.html

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/playing/service/PlayingService.java` around lines
73 - 85, 원자성이 보장되지 않는 validateMidiSaveRequestInterval(userId)의 조회 후
completeWithMidiData(midiEvents) 완료 흐름을 수정하세요. 동일 userId의 동시 요청이 하나만 1분 제한을
통과하도록 최근 완료 이력 조회에 PESSIMISTIC_WRITE 잠금을 적용하거나 DB 기본값/애너테이션 기반 원자적 rate-limit
삽입을 사용하고, 기존 MIDI 이벤트 변환 및 완료 동작은 유지하세요.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

현재 요구사항이 단순히 연속 클릭이나 중복 요청을 줄이기 위한 보조 검증을 위한 기능인데, 동일 playingId 중복 요청은 @Version 낙관적 락으로 방어할 수 있지 않나요?

Comment thread src/test/java/com/mr/domain/playing/service/PlayingServiceTest.java
Comment thread src/main/java/com/mr/domain/playing/service/PlayingService.java Outdated

@p1001q p1001q left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

리뷰 요청하신 READY→IN_PROGRESS 흐름:
지금 구현(startPlaying())은 createBackingTrack()(READY 생성) 직후 바로 start()(IN_PROGRESS 전환)를 같은 트랜잭션에서 호출해서, 클라이언트 입장에선 READY 상태가 실제로 존재하는 순간이 없어요(응답도 항상 IN_PROGRESS로 옴).

"대시보드 설정 후 바로 시작 안 할 수도 있다"는 가정을 살리려면 생성(READY)과 시작(IN_PROGRESS 전환)을 별도 API로 나눠야 할 텐데, 지금 PR 스코프엔 그 별도 엔드포인트가 없어서 이 설계가 실제로 활용되고 있진 않습니다.

지금 당장 문제는 아니고(READY 검증 로직 자체는 있으니 나중에 별도 "연주 재개/시작" API가 생기면 바로 쓸 수 있음), 지금 스코프에서는 그냥 createBackingTrack() 결과를 바로 IN_PROGRESS로 만들어도 결과는 동일하다는 정도로 이해하시면 될 것 같아요

굳이 지금 구조를 바꿀 필요는 없어 보입니다!

Comment thread src/main/java/com/mr/domain/playing/service/PlayingService.java
Comment thread src/main/java/com/mr/domain/playing/service/PlayingService.java Outdated

@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.

Caution

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

⚠️ Outside diff range comments (1)
src/main/java/com/mr/domain/playing/service/PlayingService.java (1)

45-46: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

테스트 stub을 서비스 호출과 일치시키세요.

Line [45]에서 서비스는 findByIdAndDeletedAtIsNull(...)을 호출합니다. 하지만 src/test/java/com/mr/domain/playing/service/PlayingServiceTest.javastartPlaying_success()findById(...)만 stub합니다. 따라서 성공 테스트가 BACKING_TRACK_NOT_FOUND 예외로 종료됩니다.

테스트 stub을 다음처럼 변경하세요.

수정 예시
- given(backingTrackRepository.findById(backingTrackId))
+ given(backingTrackRepository.findByIdAndDeletedAtIsNull(backingTrackId))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/mr/domain/playing/service/PlayingService.java` around lines
45 - 46, Update the success-path stubbing in
PlayingServiceTest.startPlaying_success() to mock
backingTrackRepository.findByIdAndDeletedAtIsNull(...) with the requested
backing-track ID, matching the call used by PlayingService; remove the
mismatched findById stub so the test reaches the normal success flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/main/java/com/mr/domain/playing/service/PlayingService.java`:
- Around line 45-46: Update the success-path stubbing in
PlayingServiceTest.startPlaying_success() to mock
backingTrackRepository.findByIdAndDeletedAtIsNull(...) with the requested
backing-track ID, matching the call used by PlayingService; remove the
mismatched findById stub so the test reaches the normal success flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c987670f-d50c-46f6-bccf-b6fdc205cc87

📥 Commits

Reviewing files that changed from the base of the PR and between 4f54f46 and f1dc6d4.

📒 Files selected for processing (4)
  • src/main/java/com/mr/domain/playing/exception/MidiEventErrorStatus.java
  • src/main/java/com/mr/domain/playing/repository/PlayingRepository.java
  • src/main/java/com/mr/domain/playing/service/PlayingService.java
  • src/test/java/com/mr/domain/playing/service/PlayingServiceTest.java
💤 Files with no reviewable changes (3)
  • src/main/java/com/mr/domain/playing/repository/PlayingRepository.java
  • src/main/java/com/mr/domain/playing/exception/MidiEventErrorStatus.java
  • src/test/java/com/mr/domain/playing/service/PlayingServiceTest.java

@rkdehdrbs7885-oss
rkdehdrbs7885-oss self-requested a review July 31, 2026 15:20

@p1001q p1001q left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

반영 확인 했습니다! 수고하셨어요!!

@rkdehdrbs7885-oss rkdehdrbs7885-oss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

수고하셨습니다!

Comment thread src/main/java/com/mr/domain/playing/service/PlayingService.java
@on1yoneprivate
on1yoneprivate merged commit 16de757 into develop Jul 31, 2026
2 checks passed
@kimyw1018
kimyw1018 deleted the feat/#73-start-playing-session branch August 4, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Feature - 백킹트랙 기반 연주 세션 시작 API 구현

3 participants