
이번 프로젝트를 진행하면서 게시판, 댓글, 회원 등 핵심 데이터의 삭제 방식을 두고 고민에 빠졌다.
DELETE FROM table WHERE id = 1 이 SQL 한 줄이면 시원하게 데이터를 삭제할 수 있지만, 그렇게 하면 나중에 법적인 문제가 생겼을 때나 사용자 이슈가 있을 때, 복구를 해야하는 상황이 생길 수 있지 않을까? 하는 생각이 문득 들어서 다른 게시판들은 어떻게 하고 있나 찾아보았다.
그랬더니 역시나, 많은 게시판들에서도 논리 삭제를 사용하고 있다는 걸 알 수 있었다. 그럼 논리 삭제가 뭔지 이번에 포스팅하겠다.
간단히 말해, 데이터를 실제로 지우지 않고, 지워진 척 표시만 해두는 것이다.
DELETE 쿼리를 실행하여 DB에서 row 자체를 영구적으로 제거한다. (복구 불가)UPDATE 쿼리를 실행하여 deleted_at(삭제일시) 컬럼에 시간을 기록하는 거나, is_deleted = true로 상태를 변경한다.사용자가 실수로 게시글을 삭제했을 때, 물리 삭제를 했다면 DB 관리자인 나조차도 복구해 줄 방법이 없다. 하지만 논리 삭제를 했다면, deleted_at을 다시 NULL로 바꾸는 것만으로 데이터를 되살릴 수 있다.
만약 많은 기능이 들어가 있는 게시글이 있다고 치자. 댓글, 좋아요, 알림 기록, 통계 데이터 등등..
이 게시글을 논리 삭제를 해버린다면, 연관된 데이터 처리에 대한 고민도 더 깊게 해야할 것이다. 물론 CASCADE DELETE로 연관된 데이터도 다 지워버릴 순 있지만, 다른 사용자 댓글까지도 전부 지워진다.
하지만 논리 삭제를 하면 참조 관계를 유지하면서도 조회에서만 제외되니, 오히려 편할 수 있다.
악성 유저가 비방, 명예훼손, 혹은 사기성 글을 올렸다. 피해자가 고소를 진행하려는데, 가해자가 글을 삭제하고 탈퇴해 버렸다. 만약 물리 삭제로 구현이 되었을 경우에, 데이터가 없으니 수사기관에 제출할 증거가 없어진다. 플랫폼 책임론이 불거질 수도 있다.
하지만 논리 삭제로 구현을 하면, 화면에는 보이지 않지만 DB에는 원본이 그대로 있다. 경찰 수사 협조 공문이 오면 해당 데이터를 즉시 증거 자료로 제출할 수 있다.
악성 봇이나 스패머들은 흔적을 지우기 위해 생성과 삭제를 반복한다. 이들의 패턴을 잡으려면 '삭제된 기록'이 더 중요하다. 물리 삭제로 구현했을 경우, 탈퇴했으니 회원 정보가 사라져서, 누가/어떤 IP가 그랬는지 통계를 낼 수 없다.
논리 삭제로 구현했을 경우, 삭제된 계정들의 로그를 분석해서 "동일한 IP 대역"임을 밝히고 해당 IP를 밴하여 공격을 방어할 수 있다.
비즈니스 관점에서 "데이터가 언제 삭제되었는가"는 매우 중요한 인사이트다.
'대부분 업로드 직후 5분 이내에 삭제됨' → UI/UX가 불편해서 다시 올리는 거구나
'특정 카테고리의 데이터가 유독 많이 삭제됨' → 카테고리 분류가 헷갈리는구나
이외에도 deleted_at 타임 스탬프를 분석해서 특정 이벤트 종료 직후나, 결제 페이지 오류 발생 시점과 탈퇴 시점이 일치함을 확인하고 이를 통해 서비스의 문제점을 개선할 수 있다.
물론 논리 삭제가 만능은 아니다.
모든 SELECT 쿼리마다 WHERE deleted_at IS NULL 조건을 붙여야 한다. 실수로 이 조건을 빼먹으면 삭제된 유령 데이터가 버젓이 화면에 노출되는 사고가 터진다.
회원가입 시 email을 유니크(Unique)로 설정했다고 치자.
어떤 사용자가 탈퇴(논리 삭제)를 했다. -> 데이터는 남았다. -> 이 사용자가 같은 이메일로 재가입을 시도한다.
이럴 경우 DB입장에서는 '삭제된 데이터도 데이터'이므로, "이미 존재하는 이메일입니다"라며 가입을 막아버린다. (이걸 해결하려면 인덱스에 조건을 걸거나, 별도의 탈퇴 테이블을 만들어야 한다.)
지워도 지워지지 않으니 데이터는 계속 쌓이기만 한다. 사용자가 많아지면 DB 용량 비용이 늘어난다.
단점이 명확하지만, 나는 이번 프로젝트의 핵심 도메인 (User, Post, Comment 등)에 논리 삭제를 적용했다. 이유는 다음과 같다.
이게 단점 1번인 "쿼리 작성의 귀찮음"을 해결해 주기 때문이다.
@Entity
@Table(name = "\"Post\"")
// delete() 메소드 호출 시, 실제로 DELETE 쿼리 대신 이 UPDATE 쿼리가 나간다.
@SQLDelete(sql = "UPDATE \"Post\" SET \"DELETED_AT\" = CURRENT_TIMESTAMP WHERE \"POST_ID\" = ?")
// findAll(), findById() 등 조회 시, 자동으로 이 조건이 붙는다.
@Where(clause = "\"DELETED_AT\" IS NULL")
public class Post extends BaseTimeEntity { ... }
이렇게 엔티티에 어노테이션만 붙여두면, 개발자가 실수할 여지 없이 알아서 삭제된 데이터를 걸러준다.
이번 프로젝트는 커뮤니티 성격이 강하다. 관리자가 실수로 공지사항을 지우거나, 유저가 중요한 게시글을 실수로 지웠을 때 '복구 가능하다'는 심리적 안전장치가 스토리지 비용보다 훨씬 가치 있다고 판단했다.
다만, 개인정보 보호법(파기 의무)과 유니크 인덱스 문제를 고려하여 전략을 섞었다.
스케줄러를 통해 일정 기간 후 중요 정보는 파기하거나 별도 테이블로 분리하는(물리 삭제) 하이브리드 방식을 고려하고 있다."무조건 논리 삭제가 좋다"는 아니다. 로그 데이터처럼 양이 방대하고 복구가 필요 없는 데이터는 물리 삭제가 맞다.
하지만 "데이터는 21세기의 원유다"라는 말이 있다.
단순히 용량을 아끼기 위해 데이터를 날려버리기보다는, 혹시 모를 사고에 대비하고 미래의 분석 자원으로 활용하기 위해 논리 삭제를 기본 정책으로 가져가는 것이 현대 웹 서비스에 더 적합한 방식이라고 생각한다.