docs(gift): Phase 15 개인정보 정리 계약과 검증 결과를 기록한다
This commit is contained in:
@@ -4,12 +4,12 @@
|
||||
|
||||
| 문서 항목 | 내용 |
|
||||
|---|---|
|
||||
| 상태 | Phase 14 완료, 전체 테스트 사용자 요청으로 제외 |
|
||||
| 상태 | Phase 15 서버 구현·검증·리뷰 완료, 운영 반영 전 |
|
||||
| 작성일 | 2026-09-29 |
|
||||
| 요구사항 기준 | `docs/20260929_크리에이터_선물하기/prd.md` |
|
||||
| API 기준 | `docs/20260929_크리에이터_선물하기/prd.md`의 `8. API 계약` |
|
||||
| 현재 Phase | Phase 14 관리자 선물 취소 |
|
||||
| 현재 활성 Goal | 없음, `P14-T4` 시각 정밀도 회귀 수정 완료 |
|
||||
| 현재 Phase | Phase 15 전달 완료 배송 개인정보 정리 |
|
||||
| 현재 활성 Goal | 없음. `P15-GATE` 완료, 운영 반영은 별도 승인 |
|
||||
|
||||
## 목표
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
| 9 | 완료 | `3/3` | 완료 | 없음 |
|
||||
| 10 | 완료 | `3/3` | 완료 | 관리자 선물함 목록/상세 API 추가 |
|
||||
| 14 | 완료 | `4/4` | 완료 | 전체 테스트 제외, 운영 MySQL/배포/별도 프론트엔드 검증 범위 밖 |
|
||||
| 15 | 서버 구현·Gate 완료 | `4/4` Task, `P15-R1` 완료 | 없음 | 운영 schema/reader 배포 확인은 별도 |
|
||||
|
||||
- 동시에 하나의 미완료 goal만 운용한다.
|
||||
- 구현 중 요구사항이 바뀌면 이 문서의 체크박스와 범위를 먼저 갱신한 뒤 코드를 수정한다.
|
||||
@@ -1662,3 +1663,307 @@ P14-T1 문서 검증 → 사용자 구현 승인 → P14-T2 → P14-T3 → P14-G
|
||||
- 남은 항목: 외부 의존, 후속 범위 또는 없음
|
||||
- 문서: 갱신한 PRD/plan/review 링크
|
||||
```
|
||||
|
||||
## Phase 15: 전달 완료 배송 개인정보 정리
|
||||
|
||||
**Phase 결과:** 전달 완료 후 7일 이상 지난 선물의 배송 개인정보만 DB에서 지우고, 기존 사용자/관리자 응답 형태와 결제·동의·리뷰·업무 이력을 유지한다.
|
||||
|
||||
**2026-10-23 실행 승인:** 사용자가 권장한 서브에이전트 단계별 실행을 승인하고 worktree 없이 현재 `test` 브랜치에서 진행하도록 지정했다. `P15-T2`부터 순차 TDD·리뷰를 수행한다. 운영 SQL/배포와 커밋·푸시는 하지 않는다. 후속 ULW 검증 요청에 따라 단계별 focused 검증 후 Gate에서 전체 테스트도 1회 시도하며, 기존 전체 테스트 제외 규칙은 이번 Gate에 한해 대체한다. 무관한 실패는 수정 범위를 넓히지 않고 보고한다.
|
||||
|
||||
**후속 지시:** 사용자가 `git commit/push는 진행하지 않는다. 나머지는 계속 진행한다.`고 재확인했다. 모든 변경은 현재 브랜치의 미커밋 상태로 남기고 구현·검증·리뷰만 완료한다.
|
||||
|
||||
**선행조건:** Phase 14와 P15-T1 문서 검증 후 사용자 실행 승인으로 T2~T4, R1 및 Gate를 완료했다. 요구사항은 PRD `GIFT-018`~`GIFT-021`을 따른다.
|
||||
|
||||
**Phase 완료 조건:** `P15-T1`~`P15-T4`, 후속 `P15-R1` 및 `P15-GATE` 완료. 실제 test·lint·HTTP QA·리뷰 기록을 누적했다. SQL은 문서로만 작성했으며 운영 적용 완료를 주장하지 않는다.
|
||||
|
||||
### 목표와 제약
|
||||
|
||||
- 적격 조건은 `Gift.status=DELIVERED`, `GiftDelivery.deliveredAt != null`, `deliveredAt <= now.minusDays(7)`이다. 7일 경계 포함, 그 이전의 과거 누적 및 앞으로 만료되는 건을 모두 처리한다.
|
||||
- Kotlin `GiftDelivery`의 `senderName`, `senderPhoneNumber`, `senderZipCode`, `senderAddress`, `senderAddressDetail` 다섯 property는 모두 `var String?`가 된다. DB에서 현재 NOT NULL인 열은 앞의 네 개뿐이다. `sender_address_detail`은 이미 nullable이므로 ALTER하지 않는다.
|
||||
- 정리 시 `senderName`, `senderPhoneNumber`, `senderZipCode`, `senderAddress`, `senderAddressDetail`, `recipientName`, `recipientPhoneNumber`, `recipientZipCode`, `recipientAddress`, `recipientAddressDetail`, `courierCompanyName`, `trackingNumber`만 null 처리한다.
|
||||
- 적격 delivery를 `GiftRepository.findDeliveredGiftsWithDeliveryBeforeOrAt(cutoff: LocalDateTime): List<Gift>`로 조회한다. `GiftScheduler.eraseDeliveredGiftPersonalData(now: LocalDateTime): Int`가 `now.minusDays(7)`을 전달해 반환한 delivery 개인정보를 dirty checking으로 비운다. 이미 12개가 모두 null인 건은 건너뛰어 재실행 변경 수는 0으로 한다.
|
||||
- 상태/ID, Member nickname/profile, 결제, 동의 값/시각, 리뷰와 모든 업무 lifecycle/deadline/reminder 시각 및 `createdAt`은 보존한다. `updatedAt`은 `BaseEntity @PreUpdate`의 정상 동작을 따르며 이를 우회할 native SQL update는 사용하지 않는다.
|
||||
- 기존 필수 request 타입과 validation, public DTO schema, outer nullable object, visibility/derived flags/deadlines는 유지한다. 데이터가 없는 기존 non-null response 문자열은 `""`, 삭제된 전체 주소도 정확히 `""`이다. `SENT.senderInfo`는 객체와 빈 문자열을 유지하고 `RECEIVED.senderInfo`는 null이다. 모든 recipient address가 null이면 `recipientAddress=null`; user tracking과 admin courier/tracking nullable 값은 null이다. `recipientAddressRegistered`는 실제 필드 기준 false이며 `DELIVERED`는 재입력 prompt/action을 보이지 않는다.
|
||||
- 새 erased flag/table/retention config/batch knob/dependency/index/fallback은 추가하지 않는다. 푸시/환불 동작은 추가하지 않는다. 테스트 범위는 위 실행 승인 기록에 따라 focused 우선, 최종 전체 테스트 1회 시도로 한다.
|
||||
- 기존 scheduled entrypoint는 자동취소 `0 0 * * * *`뿐이다. 추가하는 독립 wrapper는 recipient expiry `0 10 * * * *`, erasure `0 20 * * * *`, reminders `0 40 * * * *`, 모두 `Asia/Seoul`이다. Wrapper는 기존 업무 메서드를 호출하며 각각 기존 방식의 독립 Redisson lock과 `@Transactional`을 사용한다. 작업 시간 차이를 보장하지 않으므로 작업 간 비중첩은 가정하지 않는다. 기존 `LocalDateTime` clock convention 그대로 wrapper에서 `LocalDateTime.now()`를 전달하며 Clock 설정/주입은 추가하지 않는다.
|
||||
- SQL은 30줄 미만 변경이므로 별도 migration 파일 대신 이미 `gift-schema.sql` 끝에 5줄 ALTER로 작성됐다. 기존 CREATE block은 NOT NULL 상태로 유지되며, schema 전체를 fresh install 순서로 적용하면 trailing ALTER 뒤 최종 네 필드는 nullable이다. 기존 DB는 ALTER 구문만 적용해야 한다.
|
||||
- 운영 rollout만 actual schema metadata 검토, DDL 별도 승인 및 적용, nullable schema/compatible reader의 전체 instance 준비를 요구한다. 이는 local code/H2 test 선행조건이 아니다. 첫 erase 전 모든 reader는 nullable 호환이어야 한다. 이후 non-null reader rollback은 unsafe하며 코드만으로 삭제 데이터를 복구할 수 없다. 자동 production SQL 실행/backup 변경은 없다.
|
||||
|
||||
### 리뷰 포커스
|
||||
|
||||
1. `deliveredAt` 경계, 오래된 이력, null 시각 및 다른 status 구별: `GiftSchedulerTest` 실제 DB predicate 검증.
|
||||
2. 반복/부분 실행과 보호 데이터 보존: flush/clear/reload 후 `GiftSchedulerTest` 확인.
|
||||
3. 삭제 이후 역할별 nullable/non-null API mapping과 정확한 주소 문자열: query 및 controller 테스트.
|
||||
4. scheduler 분·lock·transaction wrapper 분리, 같은 클래스 delegation: scheduler annotation/reflection/delegation 테스트.
|
||||
5. nullable 컬럼과 non-null reader 간 rollout 순서/rollback 위험: `P15-T1`, `P15-GATE` deployment checklist 대조.
|
||||
|
||||
### Task 15.1 Phase15 문서 및 추적 기준
|
||||
|
||||
**Goal 실행 `P15-T1`:** PRD와 이 계획에 승인된 데이터 정리 계약, 테스트 기준, 배포 안전 게이트를 일치시킨다.
|
||||
|
||||
- **시작 조건:** Phase 14 완료, 사용자 Phase15 승인 범위 확인.
|
||||
- **완료 증거:** PRD `GIFT-018`~`GIFT-021`/Decision Log와 본 Phase의 계약·Tasks·dependency 일치. lead가 문서 최종 검증 명령을 실행하고 이 Task의 마지막 체크박스를 완료 처리한다.
|
||||
- **범위 밖:** source/test/client-summary 변경, SQL 실행, 설정 변경 및 구현 완료 표시. `gift-schema.sql`은 lead가 이미 추가한 SQL 문서 범위에 포함하며 이 문서 편집에서는 수정하지 않는다.
|
||||
- **TDD 예외 사유:** 문서 전용 작업이며 production 동작을 변경하지 않는다.
|
||||
- **대체 검증 방법:** PRD/plan traceability와 변경 경로를 diff로 대조하고 `./gradlew --no-daemon tasks --all` 및 `git diff --check`를 실행한다. 실제 결과는 하단 `P15-T1 최종 문서 검증`에 기록한다.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `docs/20260929_크리에이터_선물하기/prd.md`
|
||||
- Modify: `docs/20260929_크리에이터_선물하기/plan-task.md`
|
||||
- Include in scope: `docs/20260929_크리에이터_선물하기/gift-schema.sql`의 lead 추가 trailing ALTER section. SQL documentation은 준비됨; lead 검토 대상이며 이 Task에서는 SQL 파일을 수정/실행하지 않는다.
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: 승인된 계약 `GIFT-018`~`GIFT-021`, `DEC-017`~`DEC-021`.
|
||||
- Produces: `P15-T2`~`P15-GATE`의 구현 경계와 수용 조건.
|
||||
|
||||
- [x] **문서 초안:** PRD 요구사항/정책과 계획 task/deployment gate를 갱신한다. 문서 작성 완료만 표시하며 검증 완료를 의미하지 않는다.
|
||||
- [x] **최종 검증:** 문서 검증 명령을 실행하고 PRD/plan과 `gift-schema.sql` 하단 ALTER만 변경됐음을 확인했다. Phase14 이력 보존 및 승인 계약 추적을 대조하고 하단에 실제 검증 결과를 기록했다. 소스 구현이나 DB 반영 완료를 의미하지 않는다.
|
||||
|
||||
### Task 15.2 nullable persistence와 안전한 응답 mapping
|
||||
|
||||
**Goal 실행 `P15-T2`:** 네 sender column nullable schema와 배송 개인정보 없는 기존 응답 표현을 함께 지원한다.
|
||||
|
||||
- **시작 조건:** `P15-T1` lead 검증 완료 및 사용자 구현 승인. 실제 schema metadata/DDL 승인이나 운영 access는 local code/H2 test의 선행조건이 아니다.
|
||||
- **완료 증거:** entity mapping/DDL 검토 및 query/controller focused test에서 non-null 응답 문자열, nullable 필드, 기존 request validation/visibility/action 계약 통과.
|
||||
- **범위 밖:** HTTP schema 변경, 개인정보 보관 정책 추가, 사용자 입력 nullable화, 운영 SQL 실행.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftDelivery.kt`
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt`
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminQueryService.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftEntityMappingTest.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminQueryServiceTest.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`
|
||||
- Verify only: request/response DTO types; mapping은 기존 query service 경계에서 처리하고 DTO 변경을 전제하지 않는다.
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: 현재 response DTO와 mapping, 기존 필수 request validation.
|
||||
- Produces: `GiftDelivery.senderName`, `senderPhoneNumber`, `senderZipCode`, `senderAddress`, `senderAddressDetail` 모두 `var String?`. 현행 NOT NULL DB sender 열 4개만 기존 `gift-schema.sql` trailing ALTER에서 nullable화. Response schema는 변경 없이 empty-string/null mapping 유지.
|
||||
|
||||
- [x] **RED:** `GiftEntityMappingTest.shouldPersistNullForAllSenderFields`, 양방향 query 및 관리자 query, 실제 controller 응답 검증을 추가했다. 필수 sender 빈 문자열 거부는 기존 `GiftCommandServiceTest.shouldRejectInvalidGiftApplication`의 네 case를 재사용하고, `GiftControllerTest.shouldKeepRequiredSenderRequestValidation`으로 누락 네 case를 추가했다.
|
||||
- [x] **RED 확인:** entity focused 5개 중 1개 실패, 관련 6개 클래스 129개 중 신규 6개 실패를 확인했다. 저장 제약 3건과 query null 매핑 3건이며 기존 113개는 모두 통과했다. Kotlin reflection은 RED에서만 사용하고 최종 테스트에서는 제거했다.
|
||||
- [x] **GREEN:** 발신자 다섯 property를 `var String?`로 바꾸고 query service 두 곳에서 이름/전화번호 `.orEmpty()` 및 nullable 주소 포맷을 적용했다. DTO·요청 검증·업무 시각은 유지했다.
|
||||
- [x] **GREEN 확인:** 관련 focused 테스트 129개 실패·오류·skip 0, Kotlin 컴파일과 ktlint 통과. 12개 필드 null 저장을 flush/clear/reload로 확인했다. 기존 SQL ALTER와 엔티티의 네 nullable 컬럼을 대조했으며 SQL은 실행하지 않았다.
|
||||
- [x] **REFACTOR:** source diff 3개 파일의 최소 변경을 확인했고 독립 query/entity 32개 재실행과 lint를 통과했다. 단계 리뷰는 명세·품질 모두 승인됐다. 상세 결과는 아래 P15-T2 기록을 따른다.
|
||||
|
||||
#### P15-T2 구현·검증 기록 — 2026-10-23
|
||||
|
||||
- Baseline: `./gradlew --no-daemon test --tests '*GiftEntityMappingTest' --tests '*GiftQueryServiceTest' --tests '*GiftAdminQueryServiceTest' --tests 'kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest' --tests '*AdminGiftControllerTest' ktlintCheck` exit 0, 113개 통과(1분 44초).
|
||||
- RED: 동일 클래스 범위 신규 테스트 129개 중 6개 실패(1분 2초), null 저장 미지원 및 non-null 응답에 null 전달을 재현했다. HTTP 두 case는 저장 선행조건 실패였으며 HTTP 매핑 자체의 RED로 계산하지 않았다.
|
||||
- GREEN: baseline과 같은 focused 명령 exit 0(1분 18초), entity 5 + user query 19 + admin query 8 + command 50 + user controller 15 + admin controller 32 = 129개, 실패·오류·skip 0. 신규 16 invocation과 기존 입력 검증이 통과했다.
|
||||
- 독립 재검증: `./gradlew --no-daemon test --tests '*GiftEntityMappingTest' --tests '*GiftQueryServiceTest' --tests '*GiftAdminQueryServiceTest' ktlintCheck` exit 0(39초), XML에서 32개 실패·오류·skip 0 확인. LSP 도구가 없어 Kotlin 컴파일·ktlint로 대체했다.
|
||||
- 리뷰: 독립 P15-T2 명세·품질 리뷰 승인, 차단 사항 없음. 보존된 실행 로그 및 diff를 대조했다. 전체129 XML은 독립32 재실행으로 덮어써졌으므로 reviewer는 전체 개별 XML까지 재검증했다고 주장하지 않는다.
|
||||
- 한계/관찰: H2의 기존 chat schema 및 비관련 translation/charge scheduler `SKIP LOCKED` 경고, 중복 JSON/Netty/Security 경고, Gradle deprecation이 있었다. 새 실패는 없으며 무관한 코드는 수정하지 않았다. 이번 HTTP는 MockMvc 검증이며 삭제 후 real-socket 시나리오는 Gate에서 수행한다.
|
||||
- 자원: 전용 TMPDIR/JVM temp 경로를 사용했고 Gradle 종료 후 관련 Gradle/Redis 프로세스 잔류가 없음을 확인했다. 커밋·worktree·운영 DB 작업 없음.
|
||||
|
||||
### Task 15.3 적격 query와 결정적 개인정보 정리
|
||||
|
||||
**Goal 실행 `P15-T3`:** 주어진 `now`에서 정확히 7일 이상 지난 DELIVERED 선물의 12개 배송 개인정보를 idempotent하게 지운다.
|
||||
|
||||
- **시작 조건:** `P15-T2` 완료. local JPA test는 변경한 nullable entity를 H2에서 검증하며 production schema 확인/DDL 실행을 요구하지 않는다.
|
||||
- **완료 증거:** 실제 `GiftRepository` predicate와 scheduler/service의 embedded H2 JPA 테스트에서 경계/상태/null/보존/재실행 확인. 기존 `@DataJpaTest`가 지정 datasource를 대체함을 확인했으므로 MySQL-mode 또는 MySQL 실서버 검증으로 표시하지 않는다.
|
||||
- **범위 밖:** native SQL로 auditing 우회, erased metadata, push/refund, 상태 전이, 미전달 상태 데이터 정리.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftRepository.kt`
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `P15-T2`의 nullable `GiftDelivery`.
|
||||
- Produces: `GiftRepository.findDeliveredGiftsWithDeliveryBeforeOrAt(cutoff: LocalDateTime): List<Gift>`; `GiftScheduler.eraseDeliveredGiftPersonalData(now: LocalDateTime): Int`는 eligible delivery를 dirty checking으로 비우고 변경 건수를 반환한다.
|
||||
|
||||
- [x] **RED:** `shouldEraseDeliveredGiftWhenDeliveredAtEqualsSevenDayCutoff`, `shouldIgnoreDeliveredGiftOneSecondNewerThanSevenDayCutoff`, `shouldEraseOlderHistoricalDeliveredGifts`, `shouldIgnoreNullDeliveredAtAndNonDeliveredGifts`를 실제 repository/JPA fixture로 작성했다.
|
||||
- [x] **RED:** `shouldEraseRemainingFieldsFromPartiallyClearedGift`로 12개 중 하나씩 남은 12개 행을 검증하고, 최초 all-null 및 재실행 변경 수 0 테스트를 추가했다.
|
||||
- [x] **RED:** `shouldPreserveGiftMemberPaymentConsentReviewAndBusinessTimestamps`에서 실제 Member·UseCan·GiftReview와 선물·배송 업무 필드를 flush/clear/reload로 비교했다. 실제 변경된 행의 updatedAt은 고정하지 않고, no-op의 추가 수정 없음만 검증했다.
|
||||
- [x] **RED 확인:** scheduler 기존 14개 통과 후 신규 8개가 미구현 query/service `NoSuchMethodException`으로 실패했다. 임시 reflection 호출은 최종 구현에서 정적 호출로 교체했다.
|
||||
- [x] **GREEN:** 기존 JPQL exists 패턴의 적격 조회와 `@Transactional eraseDeliveredGiftPersonalData(now)`를 추가했다. 정확히 12개 필드만 dirty checking으로 null 처리하며 완전 null은 skip한다.
|
||||
- [x] **GREEN 확인:** scheduler 22개 실패·오류·skip 0. 결제/동의/리뷰/Member/업무 시각 보존과 no push/refund를 확인했다.
|
||||
- [x] **REFACTOR:** 새 테스트 한 줄의 lint 길이 위반을 줄바꿈으로 수정한 뒤 같은 focused/lint 명령을 통과했다. 독립 실제 재실행과 단계 리뷰도 통과했다.
|
||||
|
||||
#### P15-T3 구현·검증 기록 — 2026-10-23
|
||||
|
||||
- Baseline: `./gradlew --no-daemon test --tests '*GiftSchedulerTest' ktlintCheck` exit 0(27초), 기존 14개 통과.
|
||||
- RED: `./gradlew --no-daemon test --tests '*GiftSchedulerTest'` exit 1(33초), 22개 중 신규 8개 메서드 부재 실패, 기존 14개 통과. fixture는 실제 DB 저장 후 실패했으며 오타/설정 실패가 아니다.
|
||||
- GREEN: 첫 실행 JPA22개 통과 후 새 테스트의 130자 초과 lint 실패를 수정했다. 최종 baseline 명령 exit 0(36초), 22개 실패·오류·skip 0.
|
||||
- 독립 검증: 첫 실행은 `UP-TO-DATE`였으므로 실제 재실행으로 계산하지 않았다. `./gradlew --no-daemon test --rerun --tests '*GiftSchedulerTest' ktlintCheck` exit 0(23초), XML 22개 실패·오류·skip 0, timestamp `2026-10-23T11:05:56` 확인.
|
||||
- 범위: repository query 16줄, 삭제 업무 메서드 42줄 및 실제 DB 테스트 8개를 추가했다. T2/SQL/기존 업무 로직은 유지했다. 2020년 이력, 다른 상태 6개, 부분 삭제 각 필드, 재실행 및 실제 UseCan 보존을 포함한다.
|
||||
- 리뷰: P15-T3 명세·품질 승인, 차단 사항 없음. reviewer가 본 최신 XML은 lead 재실행 산출물이며 implementer의 `11:00:42` 실행과 구분했다.
|
||||
- 검증 한계 정정: 계획과 설정 문자열에는 MySQL-mode가 있으나 `@DataJpaTest`는 기본 embedded H2로 교체한다. 실제 JDBC URL에는 MODE가 없었으므로 H2 JPA roundtrip으로만 보고한다. 기존 무관한 DDL/JSON/Gradle 경고와 LSP 부재는 유지하며 MySQL 검증·운영 반영은 하지 않았다.
|
||||
|
||||
### Task 15.4 독립 scheduler entrypoint와 분산락
|
||||
|
||||
**2026-10-23 리뷰 정정:** 신규 wrapper의 즉시 `finally unlock`은 메서드 반환 후 이루어지는 Spring commit보다 빨라 동일 reminder 재진입을 허용할 수 있다. 아래 최초 패턴 요구는 이 정정이 우선한다. 신규 세 작업은 lock 획득 성공 시 `finally`에서 기존 `RecommendationSnapshotRefreshService.unlockAfterTransaction`과 같은 transaction synchronization 패턴으로 해제를 등록하고 `afterCompletion`에서 소유권을 확인해 해제한다. 실제 transaction이 없는 직접 호출에서는 업무 종료 후 즉시 해제한다. 기존 :00 동작은 범위 밖으로 유지한다. commit/rollback 직전에는 재진입이 거부되고 완료 뒤에는 허용되는 실제 transaction regression을 추가한다.
|
||||
|
||||
**Goal 실행 `P15-T4`:** 기존 만료/알림 업무 메서드를 신규 시간별 scheduler wrapper에 등록하고 각각 독립 lock lifecycle을 적용한다.
|
||||
|
||||
- **시작 조건:** `P15-T3` 완료.
|
||||
- **완료 증거:** scheduler reflection/lock test와 기존 업무 메서드 delegation 회귀 통과.
|
||||
- **범위 밖:** worker 실행시간 가정, 작업 간 전역 비중첩 보장, 기존 expiry/reminder 업무 로직 변경, Clock config 및 HTTP endpoint.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt`
|
||||
- Test reference: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotSchedulerTest.kt`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `GiftScheduler.eraseDeliveredGiftPersonalData(now: LocalDateTime): Int`, 기존 `markExpiredRecipientAddressGiftsUndeliverable(now: LocalDateTime): Int`, `sendDeadlineReminderGifts(now: LocalDateTime): Int`.
|
||||
- Produces: `runExpiredRecipientAddressGiftExpiration()`, `runDeliveredGiftPersonalDataErasure()`, `runDeadlineReminderGifts()` parameterless `@Scheduled`/`@Transactional` wrapper. 각각 `0 10 * * * *`, `0 20 * * * *`, `0 40 * * * *`, `zone="Asia/Seoul"`. Lock key는 각각 `lock:gift-expired-recipient-address-undeliverable`, `lock:gift-delivered-personal-data-erasure`, `lock:gift-deadline-reminders`. 기존 `runExpiredTrackingRegistrationGiftCancellation()`의 :00 cron/key는 바꾸지 않는다.
|
||||
|
||||
- [x] **RED:** JUnit MethodSource로 세 진입점의 cron/zone/transaction, 실제 delegate 및 now, lock 성공/실패/예외/소유권 5종을 검증하는 15사례를 작성했다.
|
||||
- [x] **RED 확인:** 기존 22개는 통과, 신규 15개는 wrapper 부재로 실패했다. 리뷰 후 실제 beforeCommit 조기 해제 및 rollback/소유권 회귀의 추가 RED도 확인했다.
|
||||
- [x] **GREEN:** 세 parameterless wrapper와 독립 key를 등록했다. 리뷰 정정에 따라 획득 성공한 신규 lock은 finally에서 transaction synchronization을 등록하고 afterCompletion에 해제한다. 기존 :00과 업무 로직은 유지한다.
|
||||
- [x] **GREEN 확인:** 최초 37개 통과 후 실제 transaction 완료 경계 15사례를 보강해 최종 52개 실패·오류·skip 0을 확인했다. Kotlin 컴파일과 lint도 통과했다.
|
||||
- [x] **REFACTOR:** 신규 세 wrapper의 transaction 완료 해제를 작은 helper로 공통화했다. scoped 재리뷰는 차단 사항 해결 및 명세·품질 승인 판정이다. 작업 간 비중첩이나 Redis 장애 시 exactly-once를 주장하지 않는다.
|
||||
|
||||
#### P15-T4 구현·리뷰 수정 기록 — 2026-10-23
|
||||
|
||||
- 최초 TDD: `./gradlew --no-daemon test --rerun --tests '*GiftSchedulerTest' ktlintCheck` baseline22 통과(24초), 신규 wrapper RED37개 중15실패(35초), GREEN37개/lint 통과(45초). lead 실제 재실행37개/lint도 exit0(27초), XML `11:20:16` 확인.
|
||||
- 리뷰 차단: @Transactional 메서드의 finally가 proxy commit보다 먼저 unlock하여 다른 인스턴스의 recipient reminder 중복 실행 구간을 만들 수 있음을 확인했다. `PLAN-DEC-020`으로 신규 세 작업의 해제 계약을 정정하고 재현 테스트를 먼저 추가했다.
|
||||
- 수정 RED: 실제 JpaTransactionManager의 TestTransaction beforeCommit/rollback 경계에서 52개 중15실패(33초)를 확인했다. callback을 수동 호출해 통과시키지 않았다.
|
||||
- 수정 GREEN: `./gradlew --no-daemon test --tests '*GiftSchedulerTest' ktlintCheck` 첫 실행은120초 도구 timeout으로 결과 미확정. 잔여 JVM 없음 확인 후300초 제한으로 재실행하여 exit0(23초), 52개 실패·오류·skip0, XML `2026-10-23T11:35:27` 확인. compile/lint 동일 입력은 UP-TO-DATE이며 테스트는 실제 실행됐다.
|
||||
- 보장: 정상 transaction의 beforeCommit/afterCommit과 rollback 직전까지 모의 경쟁 lock 획득이 거부되고 완료 후 허용된다. 실패 획득은 callback 미등록, 완료 시 소유권 상실은 unlock 없음, non-transaction 직접 호출은 업무 후 해제를 검증했다. 실제 Redis 다중 인스턴스 검증은 아니다.
|
||||
- 재리뷰: 원 reviewer가 단일 blocker를 ADDRESSED, Spec PASS/Quality PASS로 판정했고 신규 회귀 없음. 실제 scheduler bean proxy/등록은 Gate에서 별도로 확인한다.
|
||||
- 자원/범위: commit 테스트는 fixture를 생성하지 않으며 종료 후 Gift/Delivery count0을 확인했다. 기존 fixture 테스트는 rollback 유지. 프로세스 잔류 없음, 기존00/T2/T3/SQL 보존, 커밋·푸시 없음.
|
||||
|
||||
### Phase 15 Gate
|
||||
|
||||
**최종 리뷰 차단 및 보강 범위:** 전체 테스트·빌드가 통과했으나 신규 :10/:40 작업이 HTTP 배송지 등록과 병행될 때 오래된 Gift/Delivery를 커밋해 주소·동의·상태를 덮어쓸 수 있다. 단순 scheduler 분산락만으로 HTTP 요청을 보호할 수 없으므로 아래 `P15-R1`을 먼저 완료한다. 이미 완료된 T2~T4 기록은 되돌리지 않는다.
|
||||
|
||||
**Goal 실행 `P15-GATE`:** nullable DB 배포 순서, 개인정보 제거, 기존 API와 업무 데이터 보존을 최종 확인한다.
|
||||
|
||||
- **시작 조건:** `P15-T1`~`P15-T4`의 구현 승인 및 완료 증거 충족.
|
||||
- **완료 증거:** gift focused regression, lint, 테스트 제외 build, 격리된 실제 HTTP QA, schema/배포 checklist 기록.
|
||||
- **범위 밖:** 운영 DB 변경/삭제 실행, 신규 production HTTP endpoint, 실제 push, backup/log/cache 삭제 주장, `updatedAt` 보존 강제. 전체 테스트는 최신 실행 승인에 따라 1회 시도하되 무관한 실패를 고치지 않는다.
|
||||
|
||||
```bash
|
||||
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftEntityMappingTest" --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.scheduler.GiftSchedulerTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest"
|
||||
./gradlew ktlintCheck
|
||||
./gradlew build -x test
|
||||
```
|
||||
|
||||
**Expected:** 명령 결과는 실행 후 기록하며 현재 통과를 주장하지 않는다. HTTP QA는 독립 fixture를 가진 격리 환경에서 erase business method를 실행한 후 real socket의 기존 SENT/RECEIVED/admin GET으로 재조회한다. 실제 정리 HTTP endpoint는 추가하지 않는다. Production DB를 대상으로 하지 않으며 실제 push가 발생하지 않는다.
|
||||
|
||||
수동/deployment 확인:
|
||||
|
||||
- [x] 격리 실제 HTTP로 전/후 `SENT`/`RECEIVED`/admin 응답의 shape, 정확한 빈 주소/null, nickname, DELIVERED no-action/CTA를 확인했다.
|
||||
- [x] SQL 문서와 엔티티를 대조해 네 sender column의 nullable 완화와 기존 길이/comment 보존 및 destructive DML 없음을 확인했다. 실제 운영 schema metadata/DDL 적용은 별도 rollout 조건이며 이번 Gate에서 수행하지 않았다.
|
||||
- [x] 모든 instance의 nullable reader 준비 후 첫 정리 실행, 구버전 non-null reader rollback 위험, 코드만으로 삭제 데이터 복구 불가를 배포 안내에 명시했다. 운영 준비 자체를 확인했다는 의미는 아니다.
|
||||
- [x] 실제 Spring에서 기존00과 신규10/20/40 등록을 확인하고 erasure의 no push/refund를 검증했다. PRD §8.15의 기존 expiry 팬 알림과 현재 코드의 불일치는 별도 관찰로 유지한다. R1은 새 예약 경로의 동시 저장 안전성만 보강하고 새 업무 정책/푸시는 추가하지 않았다.
|
||||
|
||||
### Phase 15 실행 순서와 의존성
|
||||
|
||||
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||
|---:|---|---|---|---|
|
||||
| 36 | `P15-T1` | 승인된 계약 | 아니요 | 문서 검증 완료 |
|
||||
| 37 | `P15-T2` | `P15-T1` 검증 + 별도 구현 승인 | 아니요 | 승인 전 구현 금지 |
|
||||
| 38 | `P15-T3` | `P15-T2` | 아니요 | nullable mapping 완료 후 진행 |
|
||||
| 39 | `P15-T4` | `P15-T3` | 아니요 | query/service 완료 후 연결 |
|
||||
| 40 | `P15-GATE` | `P15-T2`~`P15-T4` 완료 | 아니요 | Gate 실패 소유 Task 수정 |
|
||||
|
||||
```text
|
||||
P15-T1 문서 검증 → 별도 구현 승인 → P15-T2 → P15-T3 → P15-T4 → P15-GATE
|
||||
|
||||
별도 rollout gate: DDL review + 별도 실행 승인 → compatible nullable schema 적용 및 모든 instance reader 확인 → 첫 erasure 허용
|
||||
```
|
||||
|
||||
### Phase 15 Decision Log 추가
|
||||
|
||||
기존 결정 및 Phase14 기록은 변경하지 않고 다음 결정만 추가한다.
|
||||
|
||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-10-23 | `PLAN-DEC-013` | 확정 | 정리 조건은 DELIVERED 및 non-null `deliveredAt <= now.minusDays(7)`이며 과거 누적 대상도 포함한다. 정확한 12개 배송 개인정보만 null 처리하고 완전 null 재실행은 변경 0건이다 | 승인 계약, PRD `DEC-017` | `P15-T2`~`P15-T3`, `GIFT-018` |
|
||||
| 2026-10-23 | `PLAN-DEC-014` | 확정 | sender 필수 request 계약과 기존 public response schema를 유지하고 non-null 응답 문자열은 empty string으로 매핑한다. `updatedAt`은 기존 `BaseEntity @PreUpdate`로 관리한다 | 승인 계약, PRD `DEC-018`; auditing 우회 금지 | `P15-T2`, `P15-GATE` |
|
||||
| 2026-10-23 | `PLAN-DEC-015` | 확정 | 실제 schema 확인/승인된 네 sender column nullable DDL 및 모든 instance nullable reader 이후 첫 erase를 배포 gate로 둔다. 기존 SQL 하단 ALTER로 fresh install 최종 schema도 nullable을 지원한다. code rollback은 삭제 데이터 복구가 불가능하고 자동 운영 SQL/backup 변경은 하지 않는다 | 데이터 삭제와 nullable reader 전환의 기술적 배포 제약 | `P15-T1`~`P15-GATE` |
|
||||
| 2026-10-23 | `PLAN-DEC-016` | 확정 | scheduler는 :00/:10/:20/:40에 독립 lock/transaction entrypoint를 두고 기존 expiry/reminder 처리는 바꾸지 않는다. 작업 간 비중첩을 보장하지 않는다 | 승인된 scheduler 등록 계약 | `P15-T4`, `P15-GATE` |
|
||||
| 2026-10-23 | `PLAN-DEC-017` | 확정 | 변경 SQL은 30줄 미만이므로 기존 `gift-schema.sql` trailing ALTER에 기록한다. CREATE block은 고치지 않으며 기존 DB 적용 및 fresh install 순서, DB 실행 미수행을 구분한다 | latest SQL placement decision, 기존 5줄 ALTER | `P15-T1`, `P15-T2`, `P15-GATE`, PRD `DEC-019` |
|
||||
| 2026-10-23 | `PLAN-DEC-018` | 확정 | Kotlin sender property 다섯 개를 nullable로 하고 이미 nullable인 `sender_address_detail`을 제외한 네 DB 열만 ALTER한다 | PRD `DEC-020` | `P15-T2`, `GIFT-019` |
|
||||
| 2026-10-23 | `PLAN-DEC-019` | 확정 | 신규 schedule wrapper는 parameterless이고 각 wrapper가 기존 시간관례의 `LocalDateTime.now()`를 delegate에 전달한다. 정확한 독립 schedule와 lock key는 goal Interfaces에 명시한다 | 현재 `GiftScheduler.kt` 구현과 승인 cron | `P15-T4`, PRD `DEC-021` |
|
||||
| 2026-10-23 | `PLAN-DEC-020` | 정정 | 신규 세 wrapper의 분산락은 DB commit/rollback 후 afterCompletion에서 해제한다. transaction이 없는 직접 호출은 기존 finally 해제 의미를 유지한다. 기존 :00은 변경하지 않는다 | T4 리뷰: 커밋 전 unlock의 중복 reminder 재진입 위험; 기존 recommendation transaction synchronization 패턴 재사용 | `P15-T4`, `P15-GATE` |
|
||||
|
||||
### Phase 15 문서 초안 기록
|
||||
|
||||
아래는 최종 검증 전 초안 작성 시점의 기록이며, 최신 상태는 다음 `P15-T1 최종 문서 검증`을 따른다.
|
||||
|
||||
- 상태: 문서 계획 승인, 구현 대기. `P15-T1`의 lead 문서 최종 검증이 미완료다.
|
||||
- 무엇을: PRD 및 이 Phase 계획에 적격 기준, 개인정보 필드, response mapping, scheduler 분, migration 순서와 TDD 검증을 명시했다.
|
||||
- 왜: 구현 전 승인된 정확한 동작과 data-loss 배포 순서를 고정하기 위해서다.
|
||||
- 어떻게: PRD/plan만 편집했다. lead가 기존 `gift-schema.sql` 끝에 nullable ALTER 문서를 준비했으며, 이 수정 단계에서는 SQL을 실행하지 않았다. 계획에는 test/SQL 실행 결과를 기재하지 않았다.
|
||||
- 남은 항목: lead가 `./gradlew --no-daemon tasks --all`과 `git diff --check` 실행 후 `P15-T1` 검증 checkbox를 갱신한다. 이후에도 `P15-T2` 시작에는 구현 별도 승인이 필요하다.
|
||||
- 다음 행동: `P15-T1` 최종 문서 검증, 구현 승인 전 `P15-T2` 착수 금지.
|
||||
|
||||
### P15-T1 최종 문서 검증 — 2026-10-23
|
||||
|
||||
- 상태: 문서 작성 및 검증 완료. `P15-T2`~`P15-T4`, `P15-GATE`는 미착수다.
|
||||
- 무엇을: 기존 PRD에 `GIFT-018`~`GIFT-021`과 결정 기록을 추가하고, 이 계획에 nullable 저장/응답 호환, 7일 경과 삭제, 세 스케줄 등록 및 focused TDD 검증을 추가했다. 별도 계획 폴더나 SQL 파일은 만들지 않았다.
|
||||
- 왜: 승인된 삭제 대상 12개와 보존 데이터, 기존 데이터 소급 적용, 10/20/40분 실행 시각을 구현 전에 같은 기준으로 추적하기 위해서다.
|
||||
- 검증: `./gradlew --no-daemon tasks --all` exit 0, `BUILD SUCCESSFUL in 12s`. `test`, `ktlintCheck`, `build` 명령이 등록돼 있음을 확인했으며 테스트나 빌드 성공을 뜻하지 않는다.
|
||||
- 검증: `git diff --check` exit 0. `git diff --name-only`에서 이 디렉터리의 `prd.md`, `plan-task.md`, `gift-schema.sql` 세 파일만 변경됐음을 확인했다. Phase14의 완료 체크박스·결정·검증 기록은 보존했다.
|
||||
- 수동 대조: 발신자 Kotlin 필드 다섯 개와 DB ALTER 열 네 개, 삭제 대상 열 12개, 정확히 7일인 경계 포함, 기존 데이터 포함, 응답의 빈 문자열/nullable 구분, 신규 cron/zone/lock 계약, 요구사항과 Task 연결을 확인했다. 추가 ALTER 본문은 5줄이며 기존 타입·길이·COMMENT를 유지하고 데이터 삭제 구문은 없다.
|
||||
- 한계: 사용 가능한 LSP 도구가 없어 문서/SQL 대조와 diff 검사로 검증했다. 실제 MySQL 구문 실행, 운영 schema 확인, 애플리케이션 기동, 기능 테스트, ktlint 및 빌드는 수행하지 않았다. 문서/SQL 기록만 변경했으므로 focused/전체 테스트를 생략했다. 기존 Gradle deprecation 경고는 이번 범위 밖이다.
|
||||
- 남은 항목: 사용자 구현 승인 후 `P15-T2`의 첫 미완료 RED부터 실행한다. 운영 DDL 적용과 실제 데이터 삭제는 별도 배포 승인/조건을 충족해야 하며, 커밋·푸시는 하지 않았다.
|
||||
|
||||
### P15-R1: 신규 스케줄과 배송지 등록의 경합 보강
|
||||
|
||||
**최종 재리뷰 정정:** 수취인 주소 경합은 공유 Gift→Delivery 잠금과 최신 refresh로 해결됐다. 다만 자동취소 후보의 기존 batch `@Lock` 제거는 동일 발신자 복수 선물의 환불 순서를 바꿀 수 있어 복원한다. `findReceivedGiftsWithTrackingDeadlineBefore`에 기존 `PESSIMISTIC_WRITE`와 `order by g.id`를 적용하여 첫 환불 전 후보를 모두 잠근다. reviewer는 이 복원으로 새 A/B 회귀가 차단됨을 확인했고 공통 `CanPaymentService` Member 잠금 변경 요구를 범위 확대로 철회했다. 공통 결제 로직은 수정하지 않는다.
|
||||
|
||||
**Goal:** 새로 자동 실행되는 배송지 만료·기한 알림이 동시에 성공한 수취인 배송지 등록 또는 진행 상태 변경을 덮어쓰지 않는다.
|
||||
|
||||
- **시작 조건:** 최종 Oracle 리뷰 S3 차단 재현. 전체 테스트·lint·build는 수정 전 `./gradlew --no-daemon test --rerun ktlintCheck build` exit0(9분38초).
|
||||
- **Files:** `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftRepository.kt`, 필요 시 같은 디렉터리의 `GiftDeliveryRepository.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt`, `GiftAdminService.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt` 및 영향받는 기존 선물 테스트.
|
||||
- **완료 증거:** 실제 독립 transaction·동시 호출에서 reminder 읽기/주소 등록/commit 순서와 반대 순서, expiry/주소 등록 경합의 회귀 통과. 동일 Gift 행 잠금 또는 기존 구조에 맞는 최소 직렬화로 최신 상태·배송지를 확인하며 입력을 잃지 않는다. 최종 scoped 재리뷰 승인.
|
||||
- **범위 밖:** 신규 기한/푸시/환불 정책, 전역 isolation 변경, 의존성·테이블·버전 컬럼, 운영 DB 실행, 관련 없는 도메인 수정. 단순 DynamicUpdate만으로 상태 경합을 덮지 않는다.
|
||||
- [x] **RED:** 실제 독립 transaction/latch 11개 사례에서 성공한 주소 등록 값이 reminder/운영 변경의 오래된 entity 커밋으로 null이 되거나 상태가 되돌아가는 실패를 재현했다.
|
||||
- [x] **GREEN:** 참여 writer는 Gift→Delivery 잠금과 `refresh(..., PESSIMISTIC_WRITE)`를 사용하고, scheduler는 최신 상태·주소·기한·알림표시를 재검사한다. 자동취소 후보의 기존 batch lock도 ID 순서와 함께 복원했다. 공통 결제 로직과 기한 정책은 변경하지 않았다.
|
||||
- [x] **GREEN 확인:** 지정4클래스181개와 실제 HTTP 삭제 QA·lint 통과 후, 자동취소 잠금 복원 RED/GREEN과2클래스104개 회귀·lint도 통과했다. 최종 전체2396개 테스트·lint·build를 재실행해 통과했다.
|
||||
- [x] **REFACTOR/리뷰:** 기존 S3 차단 해소와 최신 Gift/Delivery 읽기를 reviewer가 승인했다. 최종 자동취소 batch lock3줄 복원 및 회귀도 별도 scoped evidence 리뷰 APPROVED. 최신 전체2396개/lint/build 통과로 Gate를 완료했다.
|
||||
|
||||
### P15-GATE 1차 검증 및 최종 리뷰 — 2026-10-23
|
||||
|
||||
- 실제 HTTP: 기존 AdminGiftControllerTest에 `shouldEraseDeliveredGiftAndPreserveRealJwtHttpContracts`를 추가했다. 실제 JWT localhost GET28회로 익명401/무관한 회원 거부, 발신자·수취인·관리자 전후 응답을 확인했다. 최초 삭제1/재실행0, persisted null12, Gift/Delivery 각1행과 목록 유지, 실제 결제/동의/리뷰/시각 보존 및 no refund/push를 확인했다.
|
||||
- 실제 Spring: injected scheduler의 TransactionInterceptor proxy와 ScheduledAnnotationBeanPostProcessor 등록4개(00/10/20/40)를 확인했다. 등록 증거 후 테스트 context의 해당4개 작업만 cancel하여 벽시계와 fixture 경합을 막았다. production 설정이나 작업 등록은 변경하지 않았다.
|
||||
- HTTP 검증 명령: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest.shouldEraseDeliveredGiftAndPreserveRealJwtHttpContracts' --no-daemon --console=plain` exit0(44초), 해당 클래스 전체33개 exit0(35초), `./gradlew ktlintCheck --no-daemon --console=plain` exit0(18초). 최초 lint 줄바꿈 위반은 수정 후 모두 재실행했다.
|
||||
- 전체 검증: `./gradlew --no-daemon test --rerun ktlintCheck build` exit0, `BUILD SUCCESSFUL in 9m 38s`. JUnit XML322개 집계에서 테스트2377개, 실패·오류·skip0을 확인했다. 이 결과는 `P15-R1` 수정 전 기준이며 수정 후 결과로 재사용하지 않는다.
|
||||
- 최종 Oracle 리뷰: S1(nullable/API), S2(삭제 범위/보존), S4(HTTP/DDL) PASS, S3는 배송지 입력과 신규 reminder/expiry의 stale entity update 경쟁으로 BLOCKED. callback 이후 락 해제는 승인됐으나 HTTP 요청과 공유되는 DB 잠금 보강이 필요하다. `P15-R1`로 실패 재현·최소 수정을 추가했고 기존 완료 Task를 되돌리지 않았다.
|
||||
- 환경: LSP 도구가 없어 Kotlin 컴파일·ktlint로 대체했다. 실제 MySQL/운영 DDL/운영 배포/실제 Redis 다중 인스턴스는 검증하지 않았다. QA JVM/Redis/포트 종료를 확인했고 전체 suite 후에도 java/redis-server 프로세스 잔류가 없었다. 커밋·푸시 없음.
|
||||
|
||||
### P15-R1 경합 수정 및 마지막 잠금 복원 검증 — 2026-10-23
|
||||
|
||||
- RED: `./gradlew --no-daemon test --tests '*AdminGiftControllerTest.shouldSerializeRecipientMutations'` exit1(44초), 독립 transaction 11개 실패. reminder가 먼저 읽고 주소가 커밋된 뒤 recipient5개 필드가 null로 되돌아가는 것을 실제 재조회로 확인했다.
|
||||
- 수정: Gift/Delivery의 공통 locking/current read와 refresh, scheduler 후보 정렬 및 잠금 뒤 최신 조건 재검사를 적용했다. 주소 등록·수령확인·관리자 상태 변경이 같은 잠금 순서를 사용하며 새 기한 거부·푸시·환불 정책은 추가하지 않았다.
|
||||
- focused: `./gradlew --no-daemon test --tests '*AdminGiftControllerTest' --tests '*GiftCommandServiceTest' --tests '*GiftAdminServiceTest' --tests '*GiftSchedulerTest' ktlintCheck` exit0(1분4초), 181개 실패·오류·skip0. 신규 양방향 경합18사례와 실제 JWT HTTP 삭제 QA가 포함됐다. 최초 lint10건은 포맷만 수정한 뒤 재실행했다.
|
||||
- 재리뷰: 기존 S3는 ADDRESSED. 자동취소 후보의 batch lock 제거가 만든 A/B 환불 순서 회귀 지적은 원래 `PESSIMISTIC_WRITE` 복원과 `order by g.id`로 해소하는 것으로 범위를 합의했다. reviewer는 공통 Member/결제 잠금 변경 요구를 범위 확대로 철회했다.
|
||||
- 마지막 RED: `./gradlew --no-daemon test --tests '*AdminGiftControllerTest.shouldLockExpiredTrackingBatchUntilTransactionCommits'` exit1(44초). 후보 조회 transaction이 열린 동안 두 번째 선물 취소가 먼저 완료돼 `Expected TimeoutException ... but nothing was thrown`으로 실패했다. 별도의 fixture 분류번호 중복 실패는 RED에서 제외했다.
|
||||
- 마지막 GREEN: `GiftRepository`의 Lock import/annotation/ID 정렬3줄만 복원했다. 같은 테스트 exit0(44초), `./gradlew --no-daemon test --tests '*AdminGiftControllerTest' --tests '*GiftSchedulerTest' ktlintCheck` exit0(1분1초), 104개 실패·오류·skip0. 실행 SQL `order by gift0_.id for update`, `P15_BATCH_LOCK orderedCandidates=2 contenderBlockedUntilCommit=true refundCan=80 refundCount=1` 확인.
|
||||
- 최종 전체 실행: `./gradlew --no-daemon test --rerun ktlintCheck build` exit0, `BUILD SUCCESSFUL in 9m 30s`. JUnit XML322개에서 테스트2396개, 실패0·오류0·skip0. 앞선 수정 전2377개/중간2395개 결과와 구분한 최신 코드 기준이다. `build/libs/sodalive-0.0.1-SNAPSHOT.jar` 산출물을 확인했다.
|
||||
- 최종 실표면 증거: 최신 `AdminGiftControllerTest` XML52개 통과, `P15_HTTP erased=true status=200 success=true senderStringsEmpty=true recipientAddressNull=true trackingNull=true requiredFlags=false nicknamePreserved=true timelinePreserved=true lists=1/1/1 auth=401/denied`, `P15_DB erased=1 repeated=0 persistedNulls=12 giftRows=1 deliveryRows=1 paymentConsentReviewHistoryPreserved=true` 확인.
|
||||
- 한계: 실제 MySQL REPEATABLE_READ/Redis 다중 인스턴스 실험은 아니다. LSP 도구 부재는 Kotlin compile/ktlint로 대체했다. 비관련 H2 SKIP LOCKED/DDL 경고와 Gradle deprecation은 기존 현상으로 남겼다. 최종 추가 잠금 복원의 evidence 확인 리뷰와 임시 자원 정리는 아래 최종 판정에서 기록한다.
|
||||
|
||||
### P15-GATE 최종 판정
|
||||
|
||||
- 판정: 서버 구현·검증 완료. 최종 전체 테스트2396개(322 suites), 실패·오류·skip0, lint/build exit0(9분30초). 실제 JWT HTTP와 DB roundtrip, 스케줄 등록/transaction, commit/rollback 락 해제, 주소 등록 경합18사례 및 batch lock 회귀를 포함한다.
|
||||
- 리뷰: 최초 전체 Oracle 리뷰의 S3 차단은 공유 Gift→Delivery 잠금/current-read로 ADDRESSED. 그 수정이 건드린 자동취소 batch lock은 원래 비관적 잠금+ID정렬로 복원했고 마지막 scoped 리뷰 APPROVED, 남은 blocker 없음. 원 세션 재개 실패 뒤 증거를 전달한 후속 reviewer가 실제 복원 diff와 테스트를 확인했다.
|
||||
- 구현상 결정: 업무 시각 보존과 기존 updatedAt auditing을 유지했다. 같은 Gift의 writer는 직렬화되므로 대기 시간이 생길 수 있지만 주소·동의·상태 유실을 방지한다. 공통 결제·회원 잔액 정책은 범위 밖으로 변경하지 않았다.
|
||||
- 운영 경계: 하단 ALTER는 미실행이다. 실제 MySQL schema와 nullable reader 전체 배포를 확인한 뒤 스케줄을 운영해야 한다. 삭제 후 구버전 reader 복귀는 안전하지 않다. 백업·로그·외부 저장소 삭제는 이번 기능이 아니다.
|
||||
- 전달 방식: 사용자 지시대로 현재 `test` 브랜치에서 worktree 없이 작업했고 모든 변경은 미커밋 상태다. git commit/push/stage를 수행하지 않았다.
|
||||
- 최종 문서 검증: `./gradlew --no-daemon tasks --all` exit0(5초), `git diff --check` exit0. HEAD는 시작 시점과 같은 `c837ff18f82e3260c692a7c9f1884dbefccba441`, index 변경 없음이다.
|
||||
- 자원 정리: 전체 테스트 및 문서 검증 종료 후 java/redis-server 프로세스와 작업 전용5개 디렉터리의 열린 파일이 없음을 확인했다. 생성했던 `gift-p15-t2.mAk3Mv`, `gift-p15-t3.McSvmT`, `gift-p15-t4.mIILMF`, `gift-p15-r1.XJMSPV`, `gift-p15-qa.EJ30P6`와 그 안의 runtime cache/임시 Tomcat/Redis/로그를 삭제하고 각 경로의 부재를 확인했다. 검증 요약은 이 계획에 보존하며 최신 JUnit/HTML/build 산출물과 공유 Gradle 캐시는 유지했다. 작업 notepad도 요약 반영 후 제거한다.
|
||||
|
||||
### Phase 15 커밋 전 재검증 — 2026-10-26
|
||||
|
||||
- 사용자 후속 승인: `/commit-with-validation`으로 Phase 15 변경 커밋을 요청했다. 위 미커밋 기록은 당시 상태로 보존하며 이번 요청은 커밋을 승인한다.
|
||||
- 검증 명령: `./gradlew --no-daemon test --rerun --tests '*GiftEntityMappingTest' --tests '*GiftSchedulerTest' --tests '*GiftQueryServiceTest' --tests '*GiftAdminQueryServiceTest' --tests '*GiftCommandServiceTest' --tests '*GiftAdminServiceTest' --tests '*GiftControllerTest' --tests '*AdminGiftControllerTest' ktlintCheck`.
|
||||
- 결과: exit 0, `BUILD SUCCESSFUL in 1m 18s`. 8개 클래스 229개 테스트 실패·오류·skip 0. 테스트는 재실행됐으며 컴파일과 lint는 동일 입력의 UP-TO-DATE 결과다. 실제 JWT HTTP 삭제 전후 계약, 개인정보 12개 null 저장·재실행 0건, 경합 및 자동취소 batch lock 회귀를 포함한다.
|
||||
- 범위: 기존 구현의 커밋 검증이므로 전체 테스트와 패키징 빌드는 반복하지 않았다. 2026-10-23 전체 2,396개 결과와 이번 focused 결과를 구분한다. 운영 MySQL DDL·배포·실제 Redis 다중 인스턴스 검증 및 push는 수행하지 않는다.
|
||||
|
||||
Reference in New Issue
Block a user