Skip to content

feat: 휴지통 템플릿 영구 삭제 · 비우기 API - #78

Open
oTAETAEo wants to merge 5 commits into
developfrom
feat/70-trash-purge
Open

oTAETAEo wants to merge 5 commits into
developfrom
feat/70-trash-purge

Conversation

@oTAETAEo

@oTAETAEo oTAETAEo commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

관련 이슈

기준 브랜치

#69(16번 복구) 위에 쌓아 시작했고, #69가 병합되어 기준이 develop으로 바뀌었습니다.
develop(#63 갤러리 저장 포함)을 merge로 합쳤고, 충돌은 없었습니다. 합친 상태로 ./gradlew clean build 통과 (테스트 981건, 실패 0).

작업 내용

명세 17번 DELETE /api/v1/trash/templates/{templateId}(개별 영구 삭제)와 18번 DELETE /api/v1/trash/templates(휴지통 비우기)를 구현했습니다.

커밋별로 보기

  1. 삭제 쿼리: TemplateRepository.deleteTrashedByIdAndUserId, deleteTrashedByUserId (@Modifying JPQL delete, 지운 행 수 반환)
  2. 서비스: TrashCommandService.purge, empty, TrashPurgeCommand, TrashPurgeResult, TrashEmptyResult
  3. API: TrashController에 추가, TRASH200_3·TRASH200_4, TrashPurgeResponse, TrashEmptyResponse
  4. 수정: 영구 삭제·비우기도 회원 행을 먼저 잠가 복구와의 교착 방지 + 동시성 테스트

동작

API 조건 결과
17번 id·user_id·deleted_at is not null로 삭제 1행이면 TRASH200_3 + templateId, 0행이면 TRASH404_1 (없는 ID, 다른 회원, 활성 템플릿, 이미 지운 템플릿)
18번 user_id·deleted_at is not null로 삭제 TRASH200_4 + deletedCount. 비어 있어도 성공(0)
  • 엔티티를 읽지 않고 삭제 쿼리로 지웁니다 (feat: 휴지통 목록 API (#54) #55 리뷰 합의). 엔티티를 읽으면 본문을 복호화하므로, 복호화에 실패하는 행도 지울 수 있어야 하기 때문입니다. 통합 테스트로 확인했습니다.
  • 행을 지우는 쿼리라 updated_at은 넣지 않습니다.
  • @Modifying(flushAutomatically = true, clearAutomatically = true): 재발급 토큰 레포지토리와 같습니다.
    • clear: 없으면 지운 행이 1차 캐시에 남아 같은 트랜잭션의 findById가 그 행을 돌려줍니다 (플래그를 빼고 실패하는 것을 확인함).
    • flush: 캐시를 비울 때 아직 내보내지 않은 변경이 사라지지 않게 합니다.
    • 호출 뒤에는 미리 읽어 둔 엔티티를 고쳐도 DB에 반영되지 않습니다.
  • templates를 참조하는 외래키는 없습니다 (ERD v0.8, 마이그레이션 확인).
  • 00:10 정리 배치가 같은 모양을 쓸 수 있게 삭제 쿼리를 작게 두었습니다. 배치는 이 PR 범위가 아닙니다.

이슈에 없던 결정: 영구 삭제·비우기도 회원을 먼저 잠근다

  • 비우기가 회원을 잠그지 않으면, 진행 중인 복구와 교착이 나서 비우기가 MySQLTransactionRollbackException(500)이 됩니다. 동시성 테스트로 재현했습니다.
    • 비우기는 idx_templates_user_deleted → 기본 키 순서로 행을 잠급니다.
    • 복구는 기본 키를 잠근 뒤, 저장할 때 같은 인덱스를 고칩니다.
  • 복구와 같이 lockUser를 먼저 불러 같은 회원의 휴지통 쓰기를 줄 세웠습니다. 개별 영구 삭제도 비우기와 같은 순서 문제가 있을 수 있어 함께 맞췄습니다.
  • 그래서 #69에서 "정리 배치·영구 삭제는 회원을 잠그지 않는다"고 적은 주석 두 곳(TemplateRepository, TrashCommandService.restore)과 테스트 주석 하나를 "정리 배치는"으로 고쳤습니다.
  • 회원을 먼저 잠그므로 없는 회원 번호로 받은 토큰이면 TRASH404_1 대신 USER404_1입니다. 복구와 같고, 명세 1장의 개발용 토큰 설명과도 맞습니다.

명세와 다른 점

없습니다. 응답 필드와 문구는 명세 17·18번 그대로입니다.

테스트

  • TrashPurgeControllerTest (테스트컨테이너 MySQL)
    • 17번: 성공 응답·필드·no-store, 그 행만 삭제. 없는 ID·다른 회원의 활성·휴지통 템플릿·내 활성 템플릿·두 번째 삭제 TRASH404_1(아무것도 지우지 않음), 숫자가 아닌 templateId COMMON400_1, 토큰 없음 COMMON401_1
    • 18번: 내 휴지통만 삭제하고 deletedCount 일치, 활성 템플릿·다른 회원 휴지통 유지, 비어 있으면 0, 토큰 없음 COMMON401_1
    • 복호화가 실패하는 행도 17·18번으로 삭제
    • 삭제 후 15번 휴지통 목록의 totalCount·항목, 지운 단축어로 새 템플릿 생성
  • TrashPurgeRepositoryTest: 조건별 지운 행 수, 깨진 본문 행, 1차 캐시 비우기, 쌓인 변경 내보내기
  • TrashCommandServicePurgeTest (모키토): 지운 행 수에 따른 결과·TRASH404_1, 엔티티 조회 없음, 회원 잠금 → 삭제 순서
  • TrashPurgeConcurrencyTest (실제 커밋)
    • 같은 템플릿 영구 삭제 2건 → 1건 성공, 1건 TRASH404_1
    • 복구와 영구 삭제를 동시에 → 하나만 성공, 나머지 TRASH404_1
    • 영구 삭제가 먼저 잡으면 복구는 기다렸다가 TRASH404_1 / 복구가 먼저 잡으면 영구 삭제가 TRASH404_1
    • 복구 중 비우기 → 500 없이 둘 다 끝나고 복구한 템플릿만 남음
  • ./gradlew clean build 통과 (develop 합친 뒤 테스트 981건, 실패 0)

변경량

develop 대비 +910 / −4줄 (프로덕션 +111 / −3, 테스트 +799 / −1). 400줄 목표를 넘는 것은 이슈 완료 조건의 통합·동시성 테스트 때문입니다. 프로덕션 코드는 약 110줄입니다.

정리 배치(00:10)에서 볼 것

  • 배치가 회원을 잠그지 않고 deleted_at 범위로 지우면 복구와 같은 교착이 날 수 있습니다. 행 단위로 끊어 지울지, 교착 시 재시도할지 정해야 합니다.

🤖 Generated with Claude Code

엔티티를 읽지 않고 본인 소유 + 휴지통 조건으로 지우는 삭제 쿼리 두 개를 추가한다.
본문 복호화에 실패하는 행도 지울 수 있어야 하기 때문이다.
지운 행이 1차 캐시에 남지 않게 실행 뒤 캐시를 비우고, 그 전에 쌓인 변경을 내보낸다.
TrashCommandService에 purge·empty를 추가한다. 영구 삭제는 지운 행이 0이면 TRASH404_1,
비우기는 지운 개수를 돌려주고 비어 있어도 성공이다.
DELETE /api/v1/trash/templates/{templateId} (TRASH200_3)와
DELETE /api/v1/trash/templates (TRASH200_4)를 추가한다.
비우기는 (user_id, deleted_at) 인덱스 → 기본 키 순서로 행을 잠그고, 복구는 기본 키를 잠근 뒤
저장할 때 같은 인덱스를 고친다. 둘이 겹치면 교착이 나서 비우기가 500이 되는 것을 동시성 테스트로 재현했다.
복구와 같이 회원 행을 먼저 잠가 같은 회원의 휴지통 쓰기를 줄 세운다. 개별 영구 삭제도 같은 순서로 맞춘다.
영구 삭제가 회원을 잠그지 않는다고 적은 복구 쪽 주석은 정리 배치만 남기도록 고친다.
@oTAETAEo oTAETAEo added feature 기능 개발 template 내 템플릿·휴지통 labels Nov 6, 2026
@oTAETAEo oTAETAEo self-assigned this Nov 6, 2026
@oTAETAEo oTAETAEo changed the title [feat] 휴지통 템플릿 영구 삭제 · 비우기 API feat: 휴지통 템플릿 영구 삭제 · 비우기 API Nov 7, 2026
Base automatically changed from feat/67-trash-restore to develop November 7, 2026 04:14
@oTAETAEo
oTAETAEo marked this pull request as ready for review November 7, 2026 06:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 기능 개발 template 내 템플릿·휴지통

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 휴지통 템플릿 영구 삭제 · 비우기 API

1 participant