이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 이력, 감사 로그, 소프트 삭제는 각각 어떤 문제를 해결하는가?
  • deleted_at만 추가하면 왜 충분하지 않은가?
  • 운영과 법적 요구사항은 삭제 설계에 어떤 영향을 주는가?

개요

삭제와 이력은 초기에 가볍게 넘기기 쉽지만, 운영 서비스에서는 매우 중요하다. 사용자가 데이터를 삭제했을 때 실제로 지울지, 복구할 수 있게 숨길지, 감사 기록을 남길지, 개인정보를 익명화할지 결정해야 한다.

소프트 삭제는 deleted_at 컬럼 하나가 아니라 조회, 제약, 인덱스, 보관, 복구 정책 전체다.

원리

이력, 감사, 소프트 삭제의 원리는 “삭제하지 않고 숨긴다”가 아니라 데이터 생명주기를 정책으로 표현하는 것이다. 어떤 데이터는 즉시 물리 삭제해도 되고, 어떤 데이터는 법적 보관이 필요하며, 어떤 데이터는 개인정보만 익명화하고 거래 사실은 남겨야 한다.

백엔드 개발자는 deleted_at 컬럼을 추가하기 전에 조회 조건, Unique 제약, 인덱스 크기, 복구 UX, 보관 기간, 개인정보 삭제 요청, 운영 정정 절차를 함께 결정해야 한다. 이 결정은 DB 설계와 API 응답, 관리자 기능, 인프라 백업 정책까지 연결된다.

물리 삭제

물리 삭제는 row를 실제로 지운다.

DELETE FROM comments
WHERE id = :commentId;

장점은 저장 공간과 쿼리가 단순하다는 것이다. 단점은 복구와 감사가 어렵다. 결제, 포인트, 주문 같은 데이터는 함부로 물리 삭제하면 안 된다.

소프트 삭제

소프트 삭제는 삭제 시각을 기록하고 일반 조회에서 제외한다.

ALTER TABLE posts ADD COLUMN deleted_at timestamp;
 
UPDATE posts
SET deleted_at = now()
WHERE id = :postId;

조회에는 조건이 필요하다.

SELECT id, title
FROM posts
WHERE deleted_at IS NULL
ORDER BY created_at DESC;

이 조건이 모든 조회에 빠짐없이 들어가야 한다. 인덱스도 고려해야 한다.

CREATE INDEX idx_posts_active_created
ON posts (created_at DESC)
WHERE deleted_at IS NULL;

부분 인덱스는 PostgreSQL 예시다. MySQL에서는 다른 설계를 검토해야 한다.

Unique 제약과 소프트 삭제

소프트 삭제는 Unique 제약과 충돌할 수 있다.

CREATE UNIQUE INDEX uq_members_email ON members (email);

회원 탈퇴를 소프트 삭제한 뒤 같은 이메일로 재가입을 허용하려면 어떻게 할까? PostgreSQL에서는 partial unique index를 사용할 수 있다.

CREATE UNIQUE INDEX uq_members_active_email
ON members (email)
WHERE deleted_at IS NULL;

DB별 기능 차이가 있으므로 설계가 필요하다.

MySQL은 PostgreSQL의 partial unique index와 같은 방식이 바로 되지 않을 수 있다. generated column을 활용하거나, 활성 row만 별도 테이블로 분리하거나, 재가입 정책을 다르게 정하는 식의 대안이 필요하다. DBMS 기능 차이를 모르면 소프트 삭제 정책이 migration 중에 막힌다.

감사 로그

감사 로그는 누가, 언제, 무엇을 바꾸었는지 기록한다.

CREATE TABLE audit_logs (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    actor_id bigint,
    action varchar(50) NOT NULL,
    target_type varchar(50) NOT NULL,
    target_id bigint NOT NULL,
    payload jsonb,
    created_at timestamp NOT NULL DEFAULT now()
);

감사 로그는 운영 사고 분석, 보안, 법적 요구사항에 중요하다. 하지만 개인정보와 보관 기간도 함께 고려해야 한다.

감사 로그에는 “무엇을 남기지 않을지”도 설계해야 한다. 비밀번호, 토큰, 주민번호, 결제 원문 같은 민감정보를 payload에 그대로 넣으면 감사 로그가 새로운 보안 사고 지점이 된다. 필요한 경우 hash, masking, 최소 필드, 별도 암호화, 보관 기간을 둔다.

실전 팁

  • 삭제 정책은 도메인별로 다르게 정한다.
  • 소프트 삭제는 모든 조회 조건과 Unique 제약에 영향을 준다.
  • 감사 로그는 append-only 성격으로 관리한다.
  • 개인정보는 삭제, 익명화, 보관 법규를 확인한다.
  • 복구 가능성과 완전 삭제 요구는 충돌할 수 있다.
  • 소프트 삭제 테이블의 모든 핵심 조회에는 deleted_at IS NULL 조건과 인덱스가 같이 설계되어야 한다.
  • 재가입/재사용 정책은 Unique 제약과 함께 정한다.
  • 감사 로그는 append-only 원칙, 접근 권한, 민감정보 마스킹, 보관 기간을 함께 정한다.

위험 신호!

  • 모든 테이블에 무조건 deleted_at을 붙인다.
  • 소프트 삭제된 데이터가 Unique 제약을 계속 막는다.
  • 조회에서 deleted_at IS NULL 조건이 누락된다.
  • 감사 로그에 민감정보를 그대로 저장한다.
  • 삭제와 익명화의 차이를 구분하지 않는다.
  • 소프트 삭제 후에도 active 목록 count와 unique 제약이 삭제 row 때문에 느려지거나 막힌다.
  • PostgreSQL partial index 예시를 MySQL에서도 그대로 된다고 가정한다.
  • 백업에는 개인정보가 남아 있는데 애플리케이션 row만 삭제하면 완전 삭제라고 생각한다.

확인 질문

  • 소프트 삭제가 단순 deleted_at 컬럼 추가로 끝나지 않는 이유는 무엇인가?
    • 모든 조회 조건, 인덱스, Unique 제약, 복구, 보관, 개인정보 정책에 영향을 주기 때문이다.
  • 감사 로그가 필요한 이유는 무엇인가?
    • 운영 사고, 보안, 데이터 변경 추적, 법적 요구사항을 위해 누가 무엇을 바꿨는지 남겨야 하기 때문이다.
  • 물리 삭제와 소프트 삭제 선택 기준은 무엇인가?
    • 복구 필요성, 법적 보관 의무, 개인정보 삭제 요구, 데이터 중요도, 쿼리 복잡도를 기준으로 선택한다.
  • 소프트 삭제와 개인정보 삭제 요청이 충돌할 수 있는 이유는 무엇인가?
    • 서비스 복구와 감사 목적상 row를 남기고 싶어도 개인정보는 삭제 또는 익명화해야 할 수 있으며, 백업과 감사 로그까지 보관 범위를 따져야 하기 때문이다.

참고 문서