Skip to content

feat(migration): experiment_exposure/experiment_shadow_decision 테이블 추… - #676

Merged
dlxodus02 merged 1 commit into
mainfrom
develop
Jul 24, 2026
Merged

feat(migration): experiment_exposure/experiment_shadow_decision 테이블 추…#676
dlxodus02 merged 1 commit into
mainfrom
develop

Conversation

@jongjunn

@jongjunn jongjunn commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

…가 (#673)

  • EnrollmentStartedEvent 발행 → ScheduleGenerationListener가 커밋 후 Python /generate-for-member 호출
  • ScheduleGenerationPort/AiAdapter, schedulerAiExecutor 전용 풀, schedule.ai.* 설정
  • fix(enroll): schedulerAiExecutor 포화 시 CallerRunsPolicy→비차단 폐기+경고 로그 (CodeRabbit)

풀 포화 시 즉시 스케줄 생성이 수강 요청 스레드에서 동기 실행(최대 60초)되어
수강 API를 지연/차단하는 문제 해소. 폐기 건은 주간 배치(weekly_reflow)가 백업.

PATCH /api/posts/{id} 로 이미지를 재첨부하면 응답은 200이지만 신규/기존 이미지가 모두 사라지는 버그 수정.

원인: PostCommandService.update() 가 신규 파일 업로드 후
deleteByPostId(postId) 로 post_id 전체 row 를 삭제. 신규 PostFile 은 기존 파일과 동일한 post_id 라 auto-flush 로 반영된 신규 row 까지 함께 삭제됨.

수정:

  • 신규 업로드 전에 기존 파일 스냅샷(oldFiles) 확보
  • 삭제 단계에서 deleteByPostId 대신 deleteByIdIn(oldFiles id) 로 기존 파일만 지정 삭제, 신규 row 는 보존
  • PostFileRepository 3계층에 deleteByIdIn(List) 추가 (파생 쿼리)
  • 회귀 테스트 2건 추가: 재첨부 시 신규 보존·기존만 삭제 / 미첨부 시 기존만 제거
  • test(community): 게시글 수정 중 업로드 실패 시 기존 파일 보존 회귀 테스트 추가

CodeRabbit 리뷰 반영 — 신규 업로드 도중 예외가 발생하면 기존 파일이
S3·DB 어디서도 삭제되지 않고, 이미 업로드된 신규 파일만 롤백 정리되는지
검증. 삭제 단계 순서가 다시 바뀌어 실패 요청에서 기존 파일을 잃는 회귀 방지.

  • fix(community): 게시글 수정 동시성 락 + 첨부 S3 삭제 커밋 후 분리

CodeRabbit 리뷰 반영 (2건).

#1 동시 PATCH 파일 개수 race (비관적 락):

  • 같은 게시글에 PATCH가 동시에 들어오면 양쪽 다 oldFiles가 비어 2장씩 저장돼 2장 초과가 잔존할 수 있었음.
  • update()의 게시글 조회를 findByIdForUpdate(PESSIMISTIC_WRITE)로 변경해 같은 게시글 수정 요청을 직렬화. 뒤 요청은 앞 요청 커밋 후 최신 상태를 읽음.
  • PostRepository 포트·어댑터·Spring Data(@lock+@query, study 도메인과 동일 패턴)에 findByIdForUpdate 추가.

#2 S3 삭제 트랜잭션 정합성 (AFTER_COMMIT 분리):

  • 트랜잭션 내 S3 삭제는 이후 커밋 실패 시 S3/DB가 어긋남.
  • 기존 파일 S3 삭제를 PostFilesDeletedEvent 발행 → PostFileCleanupListener가 AFTER_COMMIT에 처리하도록 분리(코드베이스 기존 이벤트 리스너 패턴과 일치). DB row 삭제는 트랜잭션 내 유지, S3는 커밋 후에만 삭제. 롤백 시 S3 미삭제. update()·delete() 두 경로 모두 적용.

테스트:

  • 기존 회귀 테스트를 이벤트 발행 검증으로 갱신(+ findByIdForUpdate 스텁).
  • PostFileCleanupListenerTest 추가: 전체 URL 삭제 / 일부 실패 시 예외 미전파·계속.
  • fix(community): 비관적 락 조회를 파생 쿼리로 전환 (semgrep QUERY_002 해소)

신규 @query가 semgrep QUERY_002(신규 @query 추가 금지) 게이트에 차단됨. findByIdForUpdate의 @query+@lock을 파생 쿼리 메서드 findWithLockById + @lock 조합으로 교체 — @query 없이 동일하게 SELECT ... FOR UPDATE를 건다. ("WithLock"은 find와 By 사이의 무시되는 서술어라 파생 파싱 대상은 Id 뿐.) 도메인 포트(findByIdForUpdate) 시그니처는 그대로 유지, 어댑터 위임만 변경.


종료 시 daily_study_stats·랭킹에 '마지막 heartbeat 이후 증가분(tail)'만 반영되어 세션당 ≈1초만 집계되던 버그 수정. endedSession의 전체 누적을
집계에 반영하도록 변경하고 tail delta 계산 로직을 제거한다.

  • EndStudyTimerSessionService: deltaStudySeconds(tail) → 전체 누적(accumulatedStudySeconds)
  • calculateDeltaStudySeconds 제거
  • 회귀 테스트 추가(실측 버그 session 68 재현), 기존 테스트를 전체누적 기준으로 정정

  • feat(migration): experiment_exposure/experiment_shadow_decision 테이블 추가

Python-Server 스케줄러의 A/B 실험 관측 로그가 이 테이블 없이도 fail-soft로 500은 안 났지만(에러 1146/1054 스킵), 로그가 전혀 안 쌓여 실험 분석이
불가능했다. 정식 집계를 위해 Flyway 마이그레이션으로 생성.

  • fix: 마이그레이션 IF NOT EXISTS 제거 (CodeRabbit 리뷰 반영)

최근 마이그레이션 컨벤션(V3.4.x, V3.5.12/14/15)에 맞춰 신규 테이블
생성 시 IF NOT EXISTS 제거 — 스키마 드리프트를 조용히 가리지 않도록.


📌 연관 이슈

해결하거나 관련된 이슈 번호를 적어주세요.

  • Closes #

📝 작업 내용

구현한 주요 기능 및 작업 내역을 요약해 주세요.


🖥️ 프론트엔드 연동 가이드 (API 명세)

프론트엔드 팀원들이 API를 바로 연동하고 테스트할 수 있도록 핵심 정보를 작성해 주세요.

1. 주요 엔드포인트

  • [GET/POST/PUT/DELETE] /api/... : (API 설명)

2. 요청 파라미터 (Request)

파라미터명 위치 (Query/Body/Path) 필수 여부 설명

3. 정상 응답 예시 (200 OK)

응답 JSON 보기 (클릭)
{
  // 정상 응답 JSON 복사 붙여넣기
}

4. ⚠️ 프론트엔드 참고 및 주의사항

  • (예: type=COURSE일 때 courseId 누락 시 HTTP 400 발생)
  • (예: isRead 필드는 아직 구현 전이라 항상 false를 반환합니다)

🚨 주요 에러 코드 및 예외 (Exceptions)

프론트엔드에서 에러 핸들링을 할 수 있도록 주요 예외 상황을 명시해 주세요.

  • 에러 코드 (예: N004) : (발생 조건 및 설명)
  • 에러 코드 (예: C003) : (발생 조건 및 설명)

💡 백엔드 리뷰 포인트 (Backend Review)

백엔드 팀원들이 코드 리뷰 시 집중해서 봐주었으면 하는 부분을 적어주세요.

  • 아키텍처 및 도메인: (예: Port 분리를 통한 단일 책임 원칙 준수 등)
  • 우려되는 부분이나 고민: ---

✅ 체크리스트

  • 코드 컨벤션을 준수했습니다.
  • 불필요한 주석 및 콘솔 로그를 제거했습니다.
  • API 기능이 정상 동작하는지 테스트했습니다.
  • 예외(잘못된 값) 상황에 대한 검증 및 테스트를 통과했습니다.

Summary by CodeRabbit

  • 새 기능
    • A/B 실험 관측을 위한 데이터 저장 기능을 추가했습니다.
    • 회원별 실험 노출 이력과 변형 정보를 기록할 수 있습니다.
    • Shadow mode 결정, 예상 일정 변경 여부, 상세 정보를 저장할 수 있습니다.

#673)

* main으로 병합 (#670)

* feat(enroll): 수강 시작 시 AI 스케줄러 즉시 생성 배선 (AFTER_COMMIT @async)

- EnrollmentStartedEvent 발행 → ScheduleGenerationListener가 커밋 후 Python /generate-for-member 호출
- ScheduleGenerationPort/AiAdapter, schedulerAiExecutor 전용 풀, schedule.ai.* 설정

* fix(enroll): schedulerAiExecutor 포화 시 CallerRunsPolicy→비차단 폐기+경고 로그 (CodeRabbit)

풀 포화 시 즉시 스케줄 생성이 수강 요청 스레드에서 동기 실행(최대 60초)되어
수강 API를 지연/차단하는 문제 해소. 폐기 건은 주간 배치(weekly_reflow)가 백업.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(community): 게시글 수정 시 신규 첨부 유지·기존 파일만 지정 삭제 (#666)

* fix(community): 게시글 수정 시 신규 첨부 유지·기존 파일만 지정 삭제

PATCH /api/posts/{id} 로 이미지를 재첨부하면 응답은 200이지만 신규/기존
이미지가 모두 사라지는 버그 수정.

원인: PostCommandService.update() 가 신규 파일 업로드 후
deleteByPostId(postId) 로 post_id 전체 row 를 삭제. 신규 PostFile 은 기존
파일과 동일한 post_id 라 auto-flush 로 반영된 신규 row 까지 함께 삭제됨.

수정:
- 신규 업로드 전에 기존 파일 스냅샷(oldFiles) 확보
- 삭제 단계에서 deleteByPostId 대신 deleteByIdIn(oldFiles id) 로 기존 파일만
  지정 삭제, 신규 row 는 보존
- PostFileRepository 3계층에 deleteByIdIn(List<Long>) 추가 (파생 쿼리)
- 회귀 테스트 2건 추가: 재첨부 시 신규 보존·기존만 삭제 / 미첨부 시 기존만 제거

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(community): 게시글 수정 중 업로드 실패 시 기존 파일 보존 회귀 테스트 추가

CodeRabbit 리뷰 반영 — 신규 업로드 도중 예외가 발생하면 기존 파일이
S3·DB 어디서도 삭제되지 않고, 이미 업로드된 신규 파일만 롤백 정리되는지
검증. 삭제 단계 순서가 다시 바뀌어 실패 요청에서 기존 파일을 잃는 회귀 방지.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(community): 게시글 수정 동시성 락 + 첨부 S3 삭제 커밋 후 분리

CodeRabbit 리뷰 반영 (2건).

#1 동시 PATCH 파일 개수 race (비관적 락):
- 같은 게시글에 PATCH가 동시에 들어오면 양쪽 다 oldFiles가 비어 2장씩
  저장돼 2장 초과가 잔존할 수 있었음.
- update()의 게시글 조회를 findByIdForUpdate(PESSIMISTIC_WRITE)로 변경해
  같은 게시글 수정 요청을 직렬화. 뒤 요청은 앞 요청 커밋 후 최신 상태를 읽음.
- PostRepository 포트·어댑터·Spring Data(@lock+@query, study 도메인과 동일 패턴)에
  findByIdForUpdate 추가.

#2 S3 삭제 트랜잭션 정합성 (AFTER_COMMIT 분리):
- 트랜잭션 내 S3 삭제는 이후 커밋 실패 시 S3/DB가 어긋남.
- 기존 파일 S3 삭제를 PostFilesDeletedEvent 발행 → PostFileCleanupListener가
  AFTER_COMMIT에 처리하도록 분리(코드베이스 기존 이벤트 리스너 패턴과 일치).
  DB row 삭제는 트랜잭션 내 유지, S3는 커밋 후에만 삭제. 롤백 시 S3 미삭제.
  update()·delete() 두 경로 모두 적용.

테스트:
- 기존 회귀 테스트를 이벤트 발행 검증으로 갱신(+ findByIdForUpdate 스텁).
- PostFileCleanupListenerTest 추가: 전체 URL 삭제 / 일부 실패 시 예외 미전파·계속.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(community): 비관적 락 조회를 파생 쿼리로 전환 (semgrep QUERY_002 해소)

신규 @query가 semgrep QUERY_002(신규 @query 추가 금지) 게이트에 차단됨.
findByIdForUpdate의 @query+@lock을 파생 쿼리 메서드 findWithLockById +
@lock 조합으로 교체 — @query 없이 동일하게 SELECT ... FOR UPDATE를 건다.
("WithLock"은 find와 By 사이의 무시되는 서술어라 파생 파싱 대상은 Id 뿐.)
도메인 포트(findByIdForUpdate) 시그니처는 그대로 유지, 어댑터 위임만 변경.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(study_timer): 순공시간 집계에 세션 전체 누적 반영 (tail delta 버그) (#669)

종료 시 daily_study_stats·랭킹에 '마지막 heartbeat 이후 증가분(tail)'만
반영되어 세션당 ≈1초만 집계되던 버그 수정. endedSession의 전체 누적을
집계에 반영하도록 변경하고 tail delta 계산 로직을 제거한다.

- EndStudyTimerSessionService: deltaStudySeconds(tail) → 전체 누적(accumulatedStudySeconds)
- calculateDeltaStudySeconds 제거
- 회귀 테스트 추가(실측 버그 session 68 재현), 기존 테스트를 전체누적 기준으로 정정

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: jongjunn <jongjunny2001@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat(migration): experiment_exposure/experiment_shadow_decision 테이블 추가

Python-Server 스케줄러의 A/B 실험 관측 로그가 이 테이블 없이도 fail-soft로
500은 안 났지만(에러 1146/1054 스킵), 로그가 전혀 안 쌓여 실험 분석이
불가능했다. 정식 집계를 위해 Flyway 마이그레이션으로 생성.

* fix: 마이그레이션 IF NOT EXISTS 제거 (CodeRabbit 리뷰 반영)

최근 마이그레이션 컨벤션(V3.4.x, V3.5.12/14/15)에 맞춰 신규 테이블
생성 시 IF NOT EXISTS 제거 — 스키마 드리프트를 조용히 가리지 않도록.

---------

Co-authored-by: 유강현 <masan8259@gmail.com>
Co-authored-by: dlxodus02 <13579lty0907@gmail.com>
Co-authored-by: Yoonjongho1122 <k6k2dao112233@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2a73ef31-550c-48ae-b779-b5a61d560872

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

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.

@github-actions

Copy link
Copy Markdown

Gate 1 · 결정론 규칙

16 tests   16 ✅  1s ⏱️
 3 suites   0 💤
 3 files     0 ❌

Results for commit 38dfb02.

@github-actions

Copy link
Copy Markdown

Gate 2 · 결정론 판정

88 tests   82 ✅  0s ⏱️
29 suites   6 💤
29 files     0 ❌

Results for commit 38dfb02.

@github-actions

Copy link
Copy Markdown

Test Results

0 tests   0 ✅  0s ⏱️
0 suites  0 💤
0 files    0 ❌

Results for commit 38dfb02.

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

🤖 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/resources/db/migration/V3.6.1__create_experiment_tables.sql`:
- Line 27: Update the schedule_would_change column definition in the V3.6.1
experiment-table migration to enforce that only 0 or 1 values are accepted,
using a MySQL 8.0/InnoDB-compatible CHECK constraint while preserving its NOT
NULL DEFAULT 0 behavior.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 67fd06a5-859f-4c04-a352-4b711465c478

📥 Commits

Reviewing files that changed from the base of the PR and between a21efc8 and 38dfb02.

📒 Files selected for processing (1)
  • src/main/resources/db/migration/V3.6.1__create_experiment_tables.sql

variant VARCHAR(255) NOT NULL,
extension_delta INT NULL,
weekly_minutes_delta INT NULL,
schedule_would_change TINYINT(1) NOT NULL DEFAULT 0,

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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file excerpt =="
sed -n '1,80p' src/main/resources/db/migration/V3.6.1__create_experiment_tables.sql 2>/dev/null || true

echo
echo "== mysql dependency/version hints =="
rg -n "mysql|MariaDB|jdbc:mysql|flyway" -S --iglob '*.{gradle,gradle.kts,pom.xml,docker-compose*.yml,docker-compose*.yaml,sql,md}' . | head -200

echo
echo "== schedule_would_change usages =="
rg -n "schedule_would_change" -S . | head -200

Repository: Hard-Click/Hard-Click-BackEnd

Length of output: 3043


schedule_would_change에 0/1 CHECK 제약을 추가하세요.

TINYINT(1)은 불리언 타입과 같지 않아 2, -1 같은 값도 저장 가능합니다. shadow 결과 집계 컬럼이므로 잘못된 값 유입이 데이터 정합성을 훼손할 수 있고, 컬럼은 아직 생성 중인 migration 단계라 제약 추가 비용이 낮습니다. MySQL 8.0 이상/InnoDB 환경에서는 CHECK 제약으로 제어를 붙이세요.

권장 수정
     schedule_would_change    TINYINT(1)   NOT NULL DEFAULT 0,
+    CONSTRAINT chk_shadow_schedule_would_change
+        CHECK (schedule_would_change IN (0, 1)),
     detail                   JSON         NOT NULL,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
schedule_would_change TINYINT(1) NOT NULL DEFAULT 0,
schedule_would_change TINYINT(1) NOT NULL DEFAULT 0,
CONSTRAINT chk_shadow_schedule_would_change
CHECK (schedule_would_change IN (0, 1)),
detail JSON NOT NULL,
🤖 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/resources/db/migration/V3.6.1__create_experiment_tables.sql` at line
27, Update the schedule_would_change column definition in the V3.6.1
experiment-table migration to enforce that only 0 or 1 values are accepted,
using a MySQL 8.0/InnoDB-compatible CHECK constraint while preserving its NOT
NULL DEFAULT 0 behavior.

@dlxodus02
dlxodus02 merged commit cc64dd7 into main Jul 24, 2026
16 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.

2 participants