LS이수종
← 프로젝트 목록

멋쟁이사자처럼 중앙 해커톤 · 팀 「늑대와 함께 춤을」

이어두다

생전에 “가족사진은 아내가 보관하고, 민수에게 프로젝트를 넘긴 뒤 지수가 클라우드를 정리해 줬으면 한다”처럼 말로 남긴 뜻을, 담당자와 순서가 있는 인계 계획으로 바꿉니다. 앞 단계가 끝나기 전에는 다음 행동을 공개하지 않습니다.

기간2026.07 – 2026.08
백엔드 2 · 프론트 2 · PM 1 · 디자인 1
내 역할백엔드 — 사후 인계 파이프라인
스택Java 21 · Spring Boot 4.1 · PostgreSQL
이 프로젝트는

멋쟁이사자처럼 중앙 해커톤 출품작입니다. 짧은 기간 안에 팀에서 가장 복잡한 도메인을 맡았습니다. 사람이 세상을 떠난 뒤에야 실행되는 서비스라 되돌릴 방법이 없고, 순서 하나가 어긋나면 가족사진이 인계되기 전에 클라우드가 지워지는 식의 사고가 납니다.

그래서 “무엇을 만들까”보다 “무엇이 절대 일어나면 안 되는가”를 먼저 정하고 코드를 짰습니다. 이 프로젝트에서 상태 전이를 서버가 강제한다는 것이 무슨 뜻인지 처음으로 제대로 이해했습니다.

담당 범위

시스템 아키텍처

Next.js Vercel HTTPS Spring Boot 4.1 · Java 21 Security · JWT + SecurityToken ×4 domain/ 12개 (plan · case · evidence …) HandoverStage 상태기계 (순서 강제) Scheduler EmailOutbox 15s · 증빙삭제 경보 Resilience4j 재시도 · 회로차단 springdoc-openapi · Swagger UI PostgreSQL UUID 기본키 AWS S3 증빙 파일 · 다운로드 토큰 SMTP 단계 안내 · OTP 메일 OpenAI 자연어 → 계획 구조화 외부 의존 · 실패 격리 대상
인계 절차의 “순서”는 HandoverStage 상태기계가 강제하고, 메일 발송처럼 실패할 수 있는 작업은 아웃박스 테이블에 적재한 뒤 스케줄러가 따로 처리합니다.

API 설계 구조

사건(release-case)을 최상위 자원으로 두고 그 아래에 증빙·이의제기·단계·대기상태를 중첩했습니다. 로그인한 작성자가 쓰는 경로와, 이메일 링크로 들어오는 익명 접근자가 쓰는 경로를 분리한 것이 핵심입니다.

경로접근 주체역할
/api/plans/{planId}/…작성자 (JWT)계획·생활영역·항목·담당자·확인자 관리
/api/release-cases/{caseId}/evidence익명 토큰증빙 제출 — 토큰은 경로가 아닌 멀티파트 본문으로 전달
/api/release-cases/{caseId}/disputes익명 토큰이의 제기, 접수 시 자동 발송 중지
/api/release-cases/{caseId}/waiting링크 토큰로그인 없이 대기 상태만 공개 조회
/api/posthumous-access/{token}링크 토큰 + OTP사후 인계 링크 진입 → OTP 인증
/api/partner/reviews/{reviewId}외부 파트너할당된 증빙만 열람 — 파트너 간 접근 격리
/api/admin/…관리자감사 로그, 증빙 삭제 관리

권한 규칙은 SecurityConfig에서 익명 토큰(permitAll) → 작성자 본인(authenticated) → 관리자 전용 순으로 매칭되도록 정렬했습니다. 관리자 규칙이 먼저 잡히면 익명 접근자가 403을 받기 때문입니다.

트러블슈팅

TS 1같은 OTP 코드가 무한 재발송되던 문제 — 세 층위의 원인
문제

운영 중 “OTP 인증 메일이 몇 초마다 계속 오고, 심지어 같은 코드가 반복된다”는 제보. 60초 쿨다운이 있는데도 발송이 멈추지 않았습니다.

원인
  1. 쿨다운 경쟁 조건sendOtp()가 행 잠금 없이 otpSentAt을 읽어, 동시 요청이 모두 갱신 전 값을 보고 쿨다운을 통과.
  2. 완료된 상태로의 재전이 — OTP를 큐에 넣을 때 이미 SENT인 단계를 함께 넘겨, 발송 성공 후 stage.send()가 예외를 던짐.
  3. 배치 예외 격리 누락 (근본 원인)dispatchPending()이 배치 전체를 하나의 @Transactional로 묶어, 위 예외 한 건이 같은 배치에서 이미 성공한 다른 행의 markSent()까지 롤백. 행이 영원히 PENDING으로 남아 주기마다 같은 본문을 재발송.

주석에는 “행 단위로 예외를 격리한다”고 적혀 있었지만 실제 코드에는 try/catch가 없었습니다. 같은 패턴의 EvidenceDeletionScheduler에는 격리가 구현돼 있어, 두 스케줄러를 비교해 차이를 확정했습니다.

해결
  • findByTokenHashForUpdate()(PESSIMISTIC_WRITE)로 쿨다운 확인 구간만 직렬화 — 잠금이 불필요한 조회는 기존 그대로 유지
  • OTP는 단계 전이 대상이 아니므로 큐 적재 시 단계 참조를 넘기지 않도록 수정
  • dispatchOne()을 예외 격리 래퍼와 실제 로직으로 분리해, 실패 건은 로그·감사기록만 남기고 나머지는 커밋되도록 변경
결과

같은 토큰으로 여러 요청이 동시에 들어와도 메일은 한 번만 발송되고, 실패한 한 건이 같은 배치의 정상 건을 되돌리지 않게 됐습니다.

TS 2역할 수락 링크가 영구 접근키로 재사용되던 취약점
문제

생전에 “담당자 역할을 수락해 달라”고 보낸 초대 링크의 토큰 하나가, 이후 사망 신고·증빙 제출·이의 제기 화면까지 전부 열 수 있었습니다. 만료도 폐기도 없는 사실상의 영구 키였습니다.

해결
  • SecurityToken을 신설해 ACCEPT_ROLE / REPORT_DEATH / UPLOAD_EVIDENCE / RAISE_OBJECTION 네 목적을 분리하고, 각각 대상·사건에 바인딩
  • 기존 inviteToken 필드를 제거하고, 각 단계가 완료되는 시점에 다음 목적의 토큰만 새로 발급 + 안내 메일 발송
  • 재발급·연락처 변경·사건 종료 시 관련 토큰 일괄 폐기
  • 토큰 소비를 조건부 UPDATE로 처리해 동시 요청에서도 단 한 번만 사용되도록 보장
결과

링크 하나가 열 수 있는 범위가 그 링크의 목적으로 좁혀졌고, 사용이 끝난 토큰은 즉시 무효가 됩니다.

TS 3서비스를 써 본 계정은 항상 탈퇴에 실패하던 FK 위반
문제

담당자 초대·확인자 지정·사후 인계 발송 중 하나라도 해 본 계정은 탈퇴 시 DataIntegrityViolationException으로 실패했습니다. 신규 계정으로만 확인해 놓친 결함입니다.

원인

추측 대신 pg_constraint를 조회해 운영 스키마의 FK 제약을 전수 확인했습니다. 전부 ON DELETE 지정이 없는(NO ACTION) FK였고, 자식 행을 지우지 않고 부모부터 삭제하는 지점이 5곳 있었습니다.

자식 테이블부모 테이블
email_outboxemail_logs
posthumous_access_tokenshandover_stages
package_action_completionshandover_stages
security_tokensrole_assignees 외 3
evidence_download_tokensevidences
해결
  • 각 자식 테이블에 부모 기준 일괄 삭제 메서드를 추가하고 삭제 순서를 재정렬
  • role_assignees.backup_for_id가 자기 자신을 가리키는 자기참조 FK여서, 대체 담당자를 주 담당자보다 먼저 지우도록 정렬 추가
TS 4식별자가 순번으로 노출되던 문제
문제

기본키가 Long/IDENTITY라 사후 인계 링크나 증빙 다운로드 주소에 순번이 그대로 드러났습니다. 값 하나를 알면 인접한 다른 사건도 추측할 수 있는 구조였습니다.

해결
  • 전체 엔티티의 @IdUUID로 전환하고 Repository·Controller·DTO·JWT·저장소 키 생성까지 연쇄 영향 범위를 전부 반영
  • 익명 접근자용 토큰은 URL 경로 대신 요청 본문으로 옮겨, 링크가 공유돼도 첨부 귀속 정보가 함께 새지 않도록 조정
결과

식별자 추측으로 남의 사건에 접근할 여지를 없앴습니다.

서비스 화면

말로 남긴 뜻 → 구조화 → 사용자 승인
말로 남긴 뜻 → 구조화 → 사용자 승인
순서 충돌 감지 — 보존·인계 전 삭제 차단
순서 충돌 감지 — 보존·인계 전 삭제 차단
확인자 신고 → 증빙 검토 → 대기 → 역할별 공개
확인자 신고 → 증빙 검토 → 대기 → 역할별 공개

화면은 프론트엔드 팀원이 구현했습니다. 저는 위 흐름의 서버 측 상태 전이·토큰 발급·메일 발송·증빙 접근 제어를 담당했습니다.

저장소

Backend 저장소 ↗Frontend 저장소 ↗
다음엄마약 →