Skip to content

fix: 어드민이 보낸 알림이 발송되지 않고, 게시글 조치가 두 결과를 냈다 - #145

Merged
RosieOh merged 1 commit into
mainfrom
fix/admin-notification-dispatch
Oct 22, 2026
Merged

RosieOh merged 1 commit into
mainfrom
fix/admin-notification-dispatch

Conversation

@RosieOh

@RosieOh RosieOh commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Refs #132 (6단계)

"컨트롤러는 리포지토리를 직접 쓰지 않는다" 위반 61건이 전부 어드민 컨트롤러에 있었습니다. 규칙을 맞추려고 코드를 옮기기 전에 그 안에서 실제로 무엇이 깨지는지부터 봤고, 둘 나왔습니다.

1. 어드민이 보낸 알림은 발송되지 않았다

경로 저장 푸시·메일 발송
NotificationCreationService.createAndSend (예약·접종 리마인더) O O (동기)
FacilityVacancyNotifier, PolicyDeadlineNotifier O O (dispatchAsync)
NotificationService.createNotification (사용자 API) O O (dispatchAsync)
POST /api/admin/notifications ("알림 발송") O X

컨트롤러가 notificationRepository.save() 를 부르고 끝냈습니다. 관리자가 보낸 알림은 앱 알림함에만 쌓이고 푸시·메일이 나가지 않습니다. 설명은 "알림 발송" 이고 응답도 201 이라, 보낸 사람은 나갔다고 믿습니다.

테스트가 그대로 말해 줬습니다:

Wanted but not invoked: notificationDispatcher bean.dispatchAsync(<any Notification>);
Actually, there were zero interactions with this mock.

요청 처리 중에는 SMTP·FCM 왕복이 응답을 막으므로 createAndSendAsync 를 추가해 그쪽을 씁니다. 동기 발송은 발송 결과를 집계해 돌려줘야 하는 배치만 씁니다.

2. 같은 조치가 두 결과를 냈다 — 숨김 vs 하드 삭제

경로 결과
신고 수락 (ModerationService.resolve → hideTarget) 숨김. isActive=false, status=HIDDEN. 행이 남아 댓글·좋아요가 가리키는 대상이 유지되고 되돌릴 수 있다
DELETE /api/admin/community/posts/{id} 하드 삭제. FK 가 전부 ON DELETE CASCADE 라 댓글·좋아요·북마크까지 함께 사라지고 되돌릴 수 없다

같은 의도("이 글은 부적절하다")인데 어느 화면을 썼는지에 따라 결과가 갈립니다. 게다가 이 저장소는 사용자 탈퇴에서는 반대 원칙을 적어 두었습니다 — "하드 삭제하면 게시글·건강기록 등 참조 데이터가 함께 사라지므로 soft delete 로 처리한다" (AdminUserController). 게시글에도 같은 원칙을 적용해 숨김으로 바꿨고, 구현은 ModerationService.hidePost 한 곳입니다.

고치는 중에 또 하나 — 읽기 전용 트랜잭션이 UPDATE 를 삼켰다

숨김으로 바꿨는데 테스트가 isActive: expected false but was true 로 실패했습니다.

AdminCommunityService 가 클래스 레벨 @Transactional(readOnly = true) 이고 hide() 에 표시가 없어서, 읽기 전용 트랜잭션이 먼저 열리고 ModerationService.hidePost 의 @Transactional(기본 전파 REQUIRED)이 그 트랜잭션에 참여했습니다. 읽기 전용에서는 Hibernate 가 flush 를 하지 않으므로 UPDATE 가 조용히 사라집니다 — 응답은 204 인데 숨겨지지 않습니다. 서비스로 옮기면서 새로 만들 수 있는 함정이라 주석으로 남겼습니다.

옮긴 것

컨트롤러 새 서비스
AdminNotificationController AdminNotificationService (발송은 NotificationCreationService 경유)
AdminCommunityController AdminCommunityService (조치는 ModerationService 경유)
AdminHealthController AdminHealthRecordService
AdminHospitalController AdminHospitalService

도메인 경계는 거의 줄지 않았습니다 (242 → 240)

솔직하게 적어 둡니다. 어드민이 health·notification·community 리포지토리를 쓰는 것은 그대로이고, 위반이 컨트롤러에서 서비스로 옮겨 간 것입니다. 어드민은 본래 모든 도메인의 데이터를 관리하므로 이 숫자가 줄려면 각 도메인이 어드민 조회 입구를 내줘야 합니다(ChildDirectory 처럼). 전체 목록 조회를 내주는 입구가 네 개 더 생기는 셈이라, 그 값이 비용보다 큰지는 따로 판단할 일입니다.

숫자

  • ArchUnit baseline 357 → 331. 컨트롤러→리포지토리 61 → 37, 도메인 경계 242 → 240
  • 전체 테스트 643개, 실패 0
  • OpenAPI 경로 수는 그대로(226). DELETE /api/admin/community/posts/{id} 의 설명이 "게시글 삭제" → "게시글 숨김"

동작이 바뀌는 지점

어드민 게시글 삭제가 하드 삭제 → 숨김이 됩니다. 어드민 화면이 "삭제" 라고 표시한다면 "숨김" 으로 바꾸는 쪽이 정확합니다.

"컨트롤러는 리포지토리를 직접 쓰지 않는다" 위반 61건이 전부 어드민 컨트롤러에 있었다.
규칙을 맞추려고 코드를 옮기기 전에 그 안에서 실제로 무엇이 깨지는지부터 봤고, 둘 나왔다.

1) 어드민이 보낸 알림은 발송되지 않았다

알림을 만드는 경로가 넷이고 셋은 저장 후 발송까지 한다.

  NotificationCreationService.createAndSend (리마인더)  저장 + 동기 발송
  FacilityVacancyNotifier / PolicyDeadlineNotifier      저장 + dispatchAsync
  NotificationService.createNotification (사용자 API)   저장 + dispatchAsync
  POST /api/admin/notifications ("알림 발송")           저장만

컨트롤러가 notificationRepository.save() 를 부르고 끝냈다. 관리자가 보낸 알림은 앱
알림함에만 쌓이고 푸시·메일이 나가지 않는다. 설명은 "알림 발송" 이고 응답도 201 이라
보낸 사람은 나갔다고 믿는다. 테스트가 그대로 말해 줬다 —
"Wanted but not invoked: dispatchAsync(...)  Actually, there were zero interactions".

요청 처리 중에는 SMTP·FCM 왕복이 응답을 막으므로 createAndSendAsync 를 추가해 그쪽을
쓴다. 동기 발송은 발송 결과를 집계해 돌려줘야 하는 배치만 쓴다.

2) 같은 조치가 두 결과를 냈다 — 숨김 vs 하드 삭제

  신고 수락(ModerationService.resolve → hideTarget)  숨김. isActive=false, HIDDEN
  DELETE /api/admin/community/posts/{id}             하드 삭제

FK 가 전부 ON DELETE CASCADE 라 하드 삭제는 댓글·좋아요·북마크까지 함께 지우고 되돌릴
수 없다. 같은 의도인데 어느 화면을 썼는지에 따라 결과가 갈린다. 게다가 이 저장소는
사용자 탈퇴에서 반대 원칙을 적어 두었다 — "하드 삭제하면 게시글·건강기록 등 참조
데이터가 함께 사라지므로 soft delete 로 처리한다"(AdminUserController). 게시글에도 같은
원칙을 적용해 숨김으로 바꿨고, 구현은 ModerationService.hidePost 한 곳이다.

고치는 중에 또 하나: 읽기 전용 트랜잭션이 UPDATE 를 삼켰다

숨김으로 바꿨는데 테스트가 isActive: expected false but was true 로 실패했다.
AdminCommunityService 가 클래스 레벨 @transactional(readOnly = true) 이고 hide() 에
표시가 없어서, 읽기 전용 트랜잭션이 먼저 열리고 hidePost 의 @transactional(전파
REQUIRED)이 그 트랜잭션에 참여했다. 읽기 전용에서는 Hibernate 가 flush 를 하지 않으므로
UPDATE 가 조용히 사라진다 — 응답은 204 인데 숨겨지지 않는다. 서비스로 옮기면서 새로
만들 수 있는 함정이라 주석으로 남겼다.

옮긴 것
- AdminNotificationController → AdminNotificationService
- AdminCommunityController    → AdminCommunityService
- AdminHealthController       → AdminHealthRecordService
- AdminHospitalController     → AdminHospitalService

도메인 경계는 거의 줄지 않았다(242 → 240). 어드민이 다른 도메인 리포지토리를 쓰는 것은
그대로이고 위반이 컨트롤러에서 서비스로 옮겨 간 것이다. 어드민은 본래 모든 도메인의
데이터를 관리하므로, 줄려면 각 도메인이 어드민 조회 입구를 내줘야 한다.

- ArchUnit baseline 357 → 331 (컨트롤러→리포지토리 61 → 37)
- 전체 테스트 643개, 실패 0
- OpenAPI 경로 수는 그대로(226). 게시글 삭제 설명이 "숨김" 으로 바뀌었다

Refs #132
@RosieOh
RosieOh merged commit bd0a575 into main Oct 22, 2026
7 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