Skip to content

fix: 탈퇴한 계정에 알림이 계속 발송됐다 - #149

Merged
RosieOh merged 3 commits into
mainfrom
fix/no-notifications-to-withdrawn
Oct 23, 2026
Merged

RosieOh merged 3 commits into
mainfrom
fix/no-notifications-to-withdrawn

Conversation

@RosieOh

@RosieOh RosieOh commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

남은 도메인 경계 위반 237건 중 → user 가 92건으로 가장 큰 덩어리입니다. 각 도메인이 User 로 무엇을 하는지 세어 보다가 발송 경로에서 이걸 발견했습니다.

무슨 일이 일어나나

탈퇴는 물리 삭제가 아닙니다(User.withdraw() — #146에서 양쪽 경로를 통일했습니다). 행이 남고 isActive=false, deletedAt 이 찍히며 이메일은 deleted_<id>@deleted.local 로 익명화됩니다. 그런데 그 사용자가 남긴 대기 등록·알림 설정·푸시 토큰은 그대로 남습니다.

어린이집 대기를 걸어 둔 사람이 탈퇴한다
→ 대기 기록은 WAITING 으로 남아 있다
→ 그 시설에 자리가 난다
→ FacilityVacancyNotifier 가 entry.getUser() 로 알림을 만들고 발송한다
→ 탈퇴한 계정의 기기로 푸시가 가고, 익명화된 주소로 메일이 나간다

NotificationDispatcher 는 수신자가 null 인지만 봤습니다.

User recipient = notification.getUser();
if (recipient == null) { ... return false; }
// 탈퇴·비활성 여부는 보지 않는다

막는 자리는 한 곳

발송 경로는 넷이지만(리마인더 배치, 빈자리·마감 알림, 사용자 API, 어드민) 전부 이 디스패처를 거칩니다. 그래서 여기 하나에 조건을 넣었습니다 — 경로마다 검사를 넣으면 다음 경로가 생길 때 또 빠집니다. (#145 에서 어드민 발송이 디스패처를 아예 거치지 않던 것을 고친 덕에 이제 네 경로가 모두 여기로 모입니다.)

관리자가 비활성화한 계정도 같게 봅니다. 로그인할 수 없는 계정에 알림을 보낼 이유가 없습니다.

인앱 알림 레코드 자체는 막지 않습니다. 이미 저장된 뒤라 여기서 되돌릴 수 없고, 조회 API 가 인증을 요구하므로 탈퇴 계정은 어차피 볼 수 없습니다.

테스트

먼저 실패를 확인했습니다 — 탈퇴·비활성 계정 모두 Expecting value to be false but was true.

차단이 과하면 아무에게도 알림이 가지 않으므로 "정상 계정은 차단되지 않는다" 도 함께 고정했습니다.

기존 테스트 하나가 깨졌습니다

NotificationChannelAvailabilityTest 의 수신자 픽스처가 isActive 를 설정하지 않아 "비활성" 으로 읽혔습니다. 실제 저장된 사용자는 @PrePersist 가 true 로 채우므로 픽스처를 맞췄고, 왜 명시해야 하는지 주석으로 남겼습니다.

판단: isActive == null 을 "받을 수 없음" 으로 봅니다. 저장된 사용자에게는 null 이 생기지 않고(위 @PrePersist), 발송 경로에서는 모르는 상태를 보내지 않는 쪽이 안전합니다.

숫자

  • 전체 테스트 650개, 실패 0

리포지토리 시크릿이 하나도 설정돼 있지 않다(gh secret list → 0개). 그래서 main 머지마다
Deploy to Production 이 첫 스텝(Check required secrets)에서 죽었다. #143~#147 까지 다섯 번
연속 같은 모양이다.

  success  Test and Build
  success  Security Scan
  success  Build Docker Image
  failure  Deploy to Production     ← Check required secrets

컨테이너를 띄우기 전에 멈추므로 운영 인스턴스를 건드리지는 않는다. 문제는 그 반대다 —
아무것도 배포되지 않는데 매번 빨간불로 끝나서 그 빨간불이 정상처럼 보인다. 그 상태가
굳으면 진짜 실패도 눈에 띄지 않는다(#94 가 적은 그대로다).

시크릿 상태를 세 가지로 나눴다.

  전부 있음   → 배포한다
  하나도 없음 → 건너뛴다. 실패가 아니고, 잡 요약에 "운영 배포를 건너뜀" 과 설정 문서
                경로가 남는다. 배포가 안 됐다는 사실이 조용히 지나가지 않는다
  일부만 있음 → 실패한다. 호스트는 있는데 키가 없는 식이면 누군가 설정하다 멈춘 것이고
                그건 건너뛸 일이 아니다

스테이징도 같은 규칙으로 맞췄다.

문서(docs/operations/deployment-secrets.md)를 추가했다. 개인키를 채팅·이슈·PR 에 붙여넣지
않고 파일에서 바로 읽는 명령(gh secret set PRODUCTION_SSH_KEY < ~/.ssh/...), 배포 전용 키를
따로 만드는 이유, 서버에 미리 있어야 하는 것(docker 그룹, 스왑 2GB), 첫 배포에서 각 스텝이
실패할 때의 의미를 적었다.

아직 남은 것도 적었다 — 교체 순간의 짧은 순단, SSH 키 권한을 command= 로 좁히지 못한 상태,
애플리케이션 환경 변수(.env)가 이 문서 범위 밖인 점.

Refs #94
앞 커밋에서 Set up SSH 스텝에 조건을 두 번 넣었다. 같은 키가 두 번이면 GitHub 이 워크플로
파일 자체를 거부한다 — 실행 목록에 "This run likely failed because of a workflow file issue"
만 남고 잡이 하나도 생기지 않는다.

yaml.safe_load 는 통과했다. 파이썬 YAML 파서는 중복 키를 뒤 값으로 덮어쓰고 넘어가기
때문이다. 스텝 목록을 실제로 훑어 "조건이 걸려야 하는 스텝에 if 가 있는지" 를 확인하는
쪽으로 검증을 바꿨고, 그 과정에서 deploy-production 의 Set up SSH 에 조건이 빠져 있던 것도
함께 찾았다.

Refs #94
탈퇴는 물리 삭제가 아니다(User.withdraw()). 행이 남고 isActive=false, deletedAt 이 찍히며
이메일은 deleted_<id>@deleted.local 로 익명화된다. 그런데 그 사용자가 남긴 대기 등록·알림
설정·푸시 토큰은 그대로 남는다.

그래서 이런 일이 생긴다.

  어린이집 대기를 걸어 둔 사람이 탈퇴한다
  → 대기 기록은 WAITING 으로 남아 있다
  → 그 시설에 자리가 난다
  → FacilityVacancyNotifier 가 entry.getUser() 로 알림을 만들고 발송한다
  → 탈퇴한 계정의 기기로 푸시가 가고, 익명화된 주소로 메일이 나간다

NotificationDispatcher 는 수신자가 null 인지만 봤다. 발송 경로는 넷이지만(리마인더 배치,
빈자리·마감 알림, 사용자 API, 어드민) 전부 이 디스패처를 거치므로 막는 자리도 여기
하나다 — 경로마다 검사를 넣으면 다음 경로가 생길 때 또 빠진다.

관리자가 비활성화한 계정도 같게 본다. 로그인할 수 없는 계정에 알림을 보낼 이유가 없다.
인앱 알림 레코드 자체는 막지 않는다. 이미 저장된 뒤라 여기서 되돌릴 수 없고, 조회 API 가
인증을 요구하므로 탈퇴 계정은 어차피 볼 수 없다.

테스트로 먼저 확인했다 — 탈퇴·비활성 계정 모두 "Expecting value to be false but was true".
차단이 과하면 아무에게도 알림이 가지 않으므로 "정상 계정은 차단되지 않는다" 도 함께 고정했다.

기존 테스트 하나가 깨졌다. NotificationChannelAvailabilityTest 의 수신자 픽스처가 isActive
를 설정하지 않아 "비활성" 으로 읽혔다. 실제 저장된 사용자는 @PrePersist 가 true 로 채우므로
픽스처를 맞췄고, 왜 명시해야 하는지 주석으로 남겼다.

- 전체 테스트 650개, 실패 0
@RosieOh
RosieOh merged commit b0bf4ba into main Oct 23, 2026
8 of 13 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.

1 participant