본문으로 건너뛰기
JSL107's Tech Note
돌아가기

삭제 없이 게시물을 안전하게 숨기기

9분 분량이 글 고치기
목차 · 8

게시물을 삭제하지 않고 숨기려면 화면에서 보이지 않게 하는 것만으로는 부족해요. 이번 작업에서는 기존 데이터를 보존하면서도 노출, 집계, 제출 권한에 이르기까지 숨김 상태를 일관되게 해석하도록 각 경계를 보강했어요.

문제: 게시물을 지우면 제출 내역도 함께 사라졌다

학기 중에 제출양식을 교체하면서 오래된 게시물을 어떻게 처리할지가 문제였어요.

옛 게시물을 그대로 두면 보호자와 학생 화면에 이전 양식과 새 양식이 함께 노출돼 사용자가 잘못된 양식에 제출할 수 있어요. 반대로 게시물을 삭제하면 soft delete로 처리하더라도 교직원이 기존 제출 명단을 다시 확인하기 어려워져요. 복구 API는 있었지만 실제 클라이언트에서는 사용하지 않았어요.

기존 기능만으로 해결하기도 어려웠어요. 양식의 제출 마감 시각은 제출 서버에서 검사하지 않았고, 게시판 단위로 적용하는 게시판 범위 숨김 기준으로는 특정 게시물 하나만 숨길 수 없었어요.

필요한 동작은 삭제가 아니었어요. 데이터는 보존하되 사용자 유형에 따라 노출 여부만 달라야 했어요.

사용하지 않던 숨김 상태 컬럼을 재활용했다

먼저 게시물 데이터 테이블에 이미 있던 숨김 여부 컬럼을 다시 사용했어요. 새 컬럼을 추가하지 않았기 때문에 샤드 전체에 ALTER TABLE을 수행하지 않고 코드 배포만으로 기능을 적용할 수 있었어요.

노출 정책은 역할에 따라 나눴어요. 보호자와 학생에게는 숨김 게시물이 목록과 상세 화면에 보이지 않도록 했어요. 교직원에게는 이전처럼 모든 게시물을 반환하면서 각 게시물의 숨김 여부 값만 추가로 전달했어요. 덕분에 교직원용 목록 필터나 여러 계층으로 전달할 파라미터를 따로 만들 필요가 없었어요.

이 구조에서는 게시물과 기존 제출 내역을 삭제하지 않아요. 교직원은 과거 제출 명단을 계속 확인할 수 있고, 일반 사용자에게만 오래된 양식이 보이지 않아요.

노출만 막는다고 작업이 끝나는 것은 아니었어요. 목록, 집계, 제출 권한처럼 서로 다른 경로에서도 숨김 상태를 일관되게 해석해야 했어요.

목록과 집계의 데이터 정합성을 맞췄다

첫 구현을 마친 뒤 보호자 목록에서는 숨겨진 게시물이 사라졌지만 응답의 전체 건수에는 여전히 포함되는 문제가 발견됐어요.

목록 조회에는 게시물-대상 그룹 연결 테이블을 기반으로 게시물 목록을 집계하는 로직을 사용했어요. 반면 숨김 여부 값은 게시물 데이터 테이블에 있었어요. 기존 count 쿼리는 조인하지 않았기 때문에 숨김 여부를 알 수 없었어요.

count 쿼리 전체를 다시 작성하면 제출게시판뿐 아니라 다른 게시판의 집계 경로에도 영향을 줄 수 있었어요. 후속 변경에서는 기존 쿼리를 유지하면서 계산된 전체 개수에서 숨김 게시물 수만 빼는 방식을 택했어요. 수정 범위를 제출게시판으로 한정해 backward compatibility 위험을 줄였어요.

화면에 표시하는 요약 건수는 별도의 요약 건수 조회 API에서 계산했고, 이 경로에는 앞선 변경으로 이미 숨김 조건이 적용돼 있었어요. 목록의 전체 건수는 당시 웹과 앱에 타입만 선언돼 있을 뿐 실제로 사용하지 않았어요. 그래도 API 응답 계약을 실제 목록 내용과 일치시키기 위해 함께 정리했어요.

커서 기반 페이지에서는 쓰지 않는 전체 건수를 계산하지 않도록 불필요한 COUNT도 제거했어요. 사용자에게 보이는 기능뿐 아니라 응답의 의미와 조회 비용까지 바로잡은 변경이었어요.

조회 상한 때문에 일부 사용자에게 게시물이 남을 수 있었다

숨김 상태를 변경할 때 게시물이 발행된 대상 그룹 목록을 조회하는 경로에는 조회 개수 상한이 고정돼 있었어요.

게시물이 조회 상한보다 많은 대상 그룹에 발행됐다면 상한을 넘은 그룹은 처리 대상에서 빠질 수 있었어요. 서버의 숨김 여부 값은 바뀌었지만 일부 앱 화면에는 게시물이 계속 노출될 가능성이 있었어요.

후속 변경에서는 이 조회 상한을 보완했어요. 숨김 기능을 단순한 컬럼 변경으로만 봤다면 발견하기 어려운 문제였어요. 실제 노출 상태는 게시물 데이터뿐 아니라 게시 대상 그룹과 캐시·조회 경로가 일관되게 동작하는지에도 달려 있었어요.

숨김과 제출 authorization은 별개의 문제였다

게시물을 목록과 상세 화면에서 숨겨도 제출 자체까지 막히지는 않았어요.

제출 서버의 응답 저장 경로에서는 로그인 여부, 양식 식별자, 토큰 claim, 권한을 검사했지만 게시물 데이터나 게시물-대상 그룹 연결 데이터는 조회하지 않았어요. 사용자가 옛 링크를 보관했거나 기존 제출 화면에 다시 접근하면 숨김 게시물의 양식에도 제출할 수 있었어요.

이를 막으려고 제출 토큰을 발급하는 서버 지점 두 곳에 숨김 상태 검사를 추가했어요. 일반 사용자가 숨김 게시물의 토큰을 요청하면 발급을 거부하고, 교직원은 예외적으로 통과시켰어요. 같은 API를 기존 응답을 열람할 때도 사용했기 때문이에요.

핵심은 화면 노출과 authorization을 구분하는 것이었어요. UI에서 링크를 없애면 접근 경로는 줄어들지만 권한까지 회수되지는 않아요. 실제 제출은 서버의 토큰 발급 경계에서 통제해야 했어요.

이 변경만으로 완전히 차단되지는 않았어요. 이미 제출 화면을 열어 유효한 토큰을 받은 브라우저에서는 토큰을 다시 발급받지 않고도 제출할 수 있었어요. 이 경우까지 막으려면 저장 시점에 권한을 검사하거나 기존 권한을 회수해야 하지만 작성 중인 응답이 사라질 수 있다는 정책상 비용이 있었어요. 이 범위는 후속 기획과 검증이 필요한 한계로 남겼어요.

형제 게시물이 토큰 검사를 우회시켰다

토큰 발급 게이트를 추가하자 과거 데이터가 새로운 경계 조건으로 떠올랐어요.

게이트는 양식 식별자로 게시물을 역조회한 뒤 첫 번째 행의 숨김 여부 값만 확인했어요. 같은 기관에 하나의 양식을 참조하는 게시물이 여러 개 남아 있다면 숨김 처리되지 않은 형제 게시물이 먼저 조회될 수 있었어요. 정렬 조건도 없어서 어떤 행이 첫 번째가 될지 보장할 수 없었어요.

쓰기 시점의 검증 로직은 동일한 양식을 참조하는 새 게시물을 생성하지 못하도록 제한했지만 이미 존재하는 데이터에는 소급 적용되지 않았어요.

첫 번째 행만 검사하던 판정 방식을 전체 결과를 검사하는 방식으로 바꿨어요. 연결된 게시물 가운데 하나라도 숨김 상태라면 토큰 발급을 차단하도록 했어요.

의도적으로 보수적인 방식을 택했어요. 잘못 차단하면 사용자가 즉시 문제를 알아챌 수 있지만, 잘못 허용하면 숨긴 양식에 제출이 계속 들어와도 발견하기 어려워요. authorization에서는 조용히 열리는 실패보다 명시적으로 닫히는 실패가 안전했어요.

숨김 게시물도 상단 고정은 해제할 수 있어야 했다

또 다른 경계 조건은 상단 고정 API에서 발생했어요.

상단 고정과 고정 해제는 같은 엔드포인트를 사용했어요. 요청에 고정 만료 시각이 있으면 고정하고, 없으면 해제하는 구조였어요. 그런데 초기 숨김 상태 가드는 요청 방향을 구분하지 않고 숨김 게시물에 대한 모든 호출을 403으로 차단했어요.

그 결과 상단에 고정된 게시물을 숨김 처리하면 교직원 화면에는 계속 보이면서도 고정을 해제할 수 없었어요.

후속 변경에서는 숨김 게시물을 새로 고정하는 동작은 제한하되 고정 만료 시각 === null인 고정 해제 요청은 허용했어요. 같은 API라도 상태를 강화하는 요청과 완화하는 요청을 구분해야 한다는 사례였어요.

결과와 남은 검증

이번 작업은 새 삭제 정책을 만들거나 대규모 데이터 마이그레이션을 하는 대신 기존 숨김 여부 컬럼을 재활용하는 데서 출발했어요. 보호자·학생과 교직원의 게시물 노출을 분리하고, 목록 전체 건수와 요약 건수의 데이터 정합성도 맞췄어요. 발행 대상 조회의 고정된 개수 상한과 동일 양식을 참조하는 형제 게시물의 우회를 보완했으며, 숨김 게시물의 제출 토큰 발급은 차단하되 상단 고정 해제는 허용했어요. 커서 페이지에서 사용하지 않는 전체 건수 집계도 제거했어요.

스키마를 바꾸지 않아 배포 범위를 줄였고, 기존 교직원 조회 동작과 제출 데이터도 보존했어요. 화면에서 보이지 않는 상태와 서버에서 제출할 수 없는 상태는 서로 다른 문제라는 점도 확인해 authorization 경계를 별도로 강화했어요.

현재 근거만으로는 운영 안정성을 모두 검증했다고 보기 어려워요. 이미 토큰을 받은 열린 제출 화면에서는 여전히 제출할 수 있어요. 이를 막을지는 작성 중인 응답과 기존 세션을 어떻게 처리할지 결정한 뒤 설계해야 해요.

로그인부터 목록 조회, 상세 접근, 토큰 발급, 실제 제출까지 이어지는 클라이언트·서버 연속 회귀 테스트 결과도 필요해요. 배포 및 rollback 기록도 남아 있지 않아 운영 단계에서 복구할 수 있는지 추가로 확인해야 해요.

삭제하지 않고 숨기는 기능은 boolean 컬럼 하나로 끝나지 않았어요. 노출 정책, 집계 정합성, authorization, 과거 데이터, 성능 최적화가 같은 상태를 일관되게 해석해야 비로소 안전한 기능이 됐어요.


이 글 고치기
이 글 공유하기:

이전 글
크로스사이트 인증 쿠키 장애를 진단하고 안전하게 수정한 과정
다음 글
LLM 응답 형식을 JSON Schema로 강제한 이유