결제 알림 전체삭제
목차
notification 영역에 알림 전체삭제 기능을 붙였다. 단순히 DELETE 쿼리 하나 추가하는 작업이 아니었고, 필터 조건 기반 삭제 로직을 쿼리 레벨부터 설계해서 화면까지 연결하는 작업이었다. 변경된 파일은 SQL 매퍼 2개, 내부 클래스 1개, yml_prod.yml 1개, 뷰/스타일 1개.
작은 범위처럼 보이지만 금융/결제 도메인에서 알림은 단순 UI 요소가 아니다. 읽음 상태, 삭제 상태, 뱃지 카운트가 모두 연동되어 있어서 하나라도 어긋나면 화면마다 숫자가 달라지는 현상이 생긴다. 그래서 기존 화면이나 API에서 제공하지 않던 동작이 필요해졌을 때, UI만 손대지 않고 쿼리 레벨부터 다시 설계하는 방향을 선택했다.
구현 흐름
작업 전에 알림 기능의 현재 상태를 정리해봤다. 기존에 있던 것:
- 알림 생성 트리거 포인트 (일부)
- 읽음 처리 (단건)
- 뱃지 카운트 쿼리 - 중요 알림만 카운트
여기에 새로 추가한 것:
- 알림 타입별 라벨 정의
- 전체삭제 처리 (타입/상태 필터 적용)
- 읽음 일괄 처리
필터 기반 삭제는 단순 전체 삭제와 다르다. 타입이나 읽음 상태 조건을 붙여서 부분 삭제가 가능해야 했기 때문에, 매퍼 레벨에서 동적 조건을 처리할 수 있는 구조가 필요했다. MyBatis 환경이라면 이런 패턴으로 처리하게 된다:
<delete id="deleteNotificationsByFilter" parameterType="NotificationDeleteRequest">
DELETE FROM notification
WHERE user_id = #{userId}
<if test="notificationType != null">
AND notification_type = #{notificationType}
</if>
<if test="isRead != null">
AND is_read = #{isRead}
</if>
<if test="targetIds != null and !targetIds.isEmpty()">
AND id IN
<foreach item="id" collection="targetIds" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</delete>
<if> 조건 분기를 써서 파라미터가 없으면 전체, 있으면 필터 적용으로 동작하게 했다. 하나의 쿼리로 전체삭제/조건삭제를 모두 커버하는 구조. 매퍼를 2개 건드린 건 삭제 후 카운트 재집계 쿼리도 같이 손봐야 했기 때문이다.
내부 클래스 변경은 요청 DTO에 필터 필드를 추가한 것이다. yml_prod.yml은 새로 생긴 알림 타입 코드를 환경별 설정으로 분리했다. 하드코딩 대신 설정으로 빼두면 타입이 늘어날 때마다 코드를 건드리지 않아도 되고, 환경별로 다른 값을 주기도 쉬워진다.
설계에서 신경 쓴 것들
금융/결제 도메인이라 데이터 정합성 체크가 기본이다. 이번 작업에서 의식적으로 따진 항목들:
| 항목 | 결정 | 이유 |
|---|---|---|
| 실시간 갱신 방식 | 액션 후 수동 갱신 | 폴링 오버헤드 대비 UX 트레이드오프 |
| 삭제 정책 | 하드 딜리트 | 결제 알림이지만 감사 로그가 별도로 관리됨 |
| 뱃지 카운트 범위 | 중요 알림만 | 전체 미읽 카운트는 노이즈가 많아 신뢰도가 낮음 |
| NULL 처리 | 매퍼 조건 분기로 방어 | 파라미터 누락 시 의도치 않은 전체 삭제 방지 |
폴링 vs 이벤트 방식은 결제 알림의 실시간성이 크리티컬하지 않고, 트래픽 비용을 고려했을 때 사용자 액션 후 갱신으로 충분하다고 판단했다. 이게 맞는 결정인지는 실사용 패턴 보고 다시 검토할 여지는 있다.
예외 케이스는 세 가지를 챙겼다. 알림이 없을 때 삭제 시도(빈 데이터), 다른 사용자의 알림 ID로 요청(권한 없는 접근), 필터 파라미터가 부분적으로만 들어올 때(NULL 처리). 권한 체크는 서비스 레이어에서 userId 기준으로 먼저 걸러지고, 쿼리에도 user_id = #{userId} 조건을 달아 이중 방어했다. 한 곳에서만 막으면 어느 경로로 들어오느냐에 따라 뚫리는 경우가 생기기 때문에.
쿼리 실행 계획은 변경 전에 확인해뒀다. 결제 도메인 테이블은 데이터가 붙기 시작하면 인덱스 하나 차이로 성능이 크게 갈린다. notification_type, user_id, is_read 조합에 인덱스가 제대로 걸려 있는지 사전에 체크하는 습관이 나중에 "왜 갑자기 느리지?" 하는 상황을 막아준다.
작업 후기
사내 서비스 기능 하나가 버튼 하나 추가로 끝나지 않는다는 걸 이번에도 다시 체감했다. SQL 집계, 상태 머신, 예외 처리, 화면 렌더링, 권한 체크가 전부 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나온다.
금융/결제 도메인은 숫자 하나가 틀리면 신뢰가 흔들린다. "대충 맞는 것 같은데"로 넘어가면 나중에 반드시 다시 돌아온다. 그래서 구현 후 직접 화면에서 동작을 확인하고, 기존 데이터가 깨지지 않았는지, 관련 화면의 숫자가 일치하는지 cross-check하는 걸 기본으로 유지하고 있다.
개발 흐름도 정리해두자면:
- 변경 전 현재 동작 스크린샷이나 수치를 메모해둠
- 수정 후 같은 케이스로 재확인
- 관련 화면이 있으면 숫자 cross-check
- 커밋 메시지는 "무엇을"보다 "왜"를 담으려고 노력함
작은 커밋을 자주 하면 문제가 생겼을 때 어느 변경에서 깨졌는지 찾기 훨씬 쉽다. 이번 작업도 매퍼 변경, DTO 변경, 뷰 변경을 각각 독립 커밋으로 쪼갰다. 롤백이 필요할 때 전체를 되돌리지 않고 특정 변경만 revert할 수 있다는 게 실무에서 꽤 중요하다. 한 커밋에 다 때려넣으면 "이 중에서 뷰만 되돌리고 싶은데" 하는 상황이 반드시 온다.
쿼리 레벨 설계를 먼저 하고 위로 올라가는 방향이 이번엔 맞았다. 화면 먼저 만들고 쿼리를 끼워 맞추다 보면 나중에 쿼리 구조 때문에 화면 로직을 다시 뜯는 역방향 수정이 생기는데, 반대로 가니까 그게 없었다. 총괄 포지션에서 설계 결정을 직접 내릴 수 있다는 게 이런 상황에서 속도를 만들어준다.
댓글 0
첫 댓글 달아줘.