소프트 삭제의 어려움
(atlas9.dev)- 데이터 복구와 규제 준수를 위해
archived_at열 기반의 소프트 삭제가 자주 사용되지만, 시간이 지나면 복잡성과 비효율이 커짐 - 이 방식은 쿼리, 인덱스, 마이그레이션, 복원 로직을 복잡하게 만들고, 대부분의 보관 데이터가 다시 읽히지 않아 데이터베이스에 불필요한 부하를 초래함
- 대안으로 애플리케이션 이벤트 기반 보관, 트리거 기반 보관, WAL(Change Data Capture) 기반 보관이 제시됨
- 각 방식은 운영 복잡도, 인프라 요구, 복원 용이성에서 차이를 보이며, 특히 WAL 기반은 Kafka 등 외부 시스템과의 통합이 필요함
- 새로운 프로젝트라면 트리거 기반 접근법이 단순성과 유지보수성 측면에서 가장 균형 잡힌 선택임
소프트 삭제의 문제점
- 일반적으로
deleted불리언이나archived_at타임스탬프 열을 사용해 데이터를 논리적으로 삭제함- 고객이 데이터를 실수로 삭제했을 때 복구 가능
- 규제나 감사 목적상 보관이 필요한 경우도 있음
- 그러나
archived_at열은 쿼리, 운영, 애플리케이션 코드 전반에 복잡성을 유발- 대부분의 보관 데이터는 다시 읽히지 않음
- API 동작 문제나 자동화 도구(Terraform 등)로 인해 수백만 개의 불필요한 행이 누적될 수 있음
- 보관 데이터 정리 작업이 설정되지 않으면 데이터베이스 백업 및 복원 시 성능 저하 발생
- 쿼리와 인덱스에서 보관 데이터를 필터링해야 하며, 데이터 누출 위험 존재
- 마이그레이션 시 오래된 데이터 처리나 기본값 수정이 어려움
- 복원 로직이 복잡해지고, 외부 시스템 호출이 필요한 경우 버그 발생 가능
- 결과적으로
archived_at방식은 단순해 보이지만 장기적으로 유지보수 비용이 높음
애플리케이션 레벨 보관
- 삭제 시 이벤트를 발행하고, 이를 SQS로 전송해 다른 서비스가 S3에 보관
- 장점
- 주요 데이터베이스와 애플리케이션 코드 단순화
- 외부 리소스 정리를 비동기 처리하여 성능과 안정성 향상
- JSON 형태로 직렬화해 애플리케이션 친화적 구조로 보관 가능
- 단점
- 애플리케이션 코드 버그로 인해 보관 데이터 손실 가능
- 메시지 큐 등 운영 인프라 복잡성 증가
- S3의 보관 데이터는 검색 및 복원 도구 필요
트리거 기반 보관
- 삭제 전 트리거가 행을 별도의 archive 테이블에 JSON 형태로 복사
- 예시 테이블:
archive(id, table_name, record_id, data, archived_at, caused_by_table, caused_by_id)
- 예시 테이블:
- 외래 키 삭제(cascade) 시 삭제 원인 추적을 위해 세션 변수(
archive.cause_table,archive.cause_id) 사용- 어떤 상위 레코드가 하위 데이터를 삭제했는지 조회 가능
- 장점
-
라이브 테이블이 깨끗하게 유지,
archived_at열 불필요 - 보관 테이블 정리(
WHERE archived_at < NOW() - INTERVAL '90 days')가 간단 - 쿼리와 인덱스 효율 유지, 마이그레이션 단순화
- 백업 크기 감소
-
라이브 테이블이 깨끗하게 유지,
- 보관 테이블은 별도 테이블스페이스나 시간 파티셔닝으로 관리 가능
WAL(Change Data Capture) 기반 보관
- PostgreSQL의 WAL 로그를 읽어 삭제 이벤트를 외부 시스템으로 스트리밍
- 대표 도구: Debezium (Kafka와 연동)
- 경로 예시:
PostgreSQL → Debezium → Kafka → Consumer → Archive Storage
- 경량 대안
- pgstream: WAL을 웹훅이나 메시지 큐로 직접 전송
- wal2json: WAL을 JSON으로 출력
- pg_recvlogical: PostgreSQL 내장 논리 복제 도구
- 운영 복잡성
- Kafka 기반은 모니터링·장애 대응·튜닝 필요
- 소비자(consumer)가 지연되면 WAL 파일 누적 → 디스크 공간 부족 위험
- PostgreSQL 13+의
max_slot_wal_keep_size설정으로 제한 가능 - 복제 슬롯 지연 모니터링 및 알림 필수
- 장점
- 애플리케이션 코드 수정 없이 모든 변경 캡처 가능
- 다양한 목적지(S3, 데이터 웨어하우스, 검색 인덱스) 로 스트리밍 가능
- 기본 데이터베이스에 추가 부하 없음
- 단점
- 운영 복잡도와 인프라 비용 높음
- 소비자 지연 시 데이터 손실 또는 재동기화 필요
- 스키마 변경 시 소스-소비자 간 조율 필요
삭제를 처리하지 않는 복제본 아이디어
- DELETE 쿼리를 무시하는 PostgreSQL 복제본을 유지하는 아이디어 제시
- 삭제되지 않은 모든 데이터를 누적 보관 가능
- 보관 데이터 직접 쿼리 가능
- 잠재적 문제
- 삭제 정보 구분 불가 가능성
- 마이그레이션 적용 시 충돌 위험
- 저장 공간 및 운영 비용 증가
결론
- 새로운 프로젝트에서는 트리거 기반 보관 방식이 가장 실용적 선택
- 설정이 간단하고, 라이브 테이블을 깨끗하게 유지
- 별도 인프라 없이도 보관 데이터 조회 및 관리 용이
- 복잡한 인프라가 이미 존재하거나 다중 목적지 스트리밍이 필요한 경우에는 WAL 기반 접근이 적합
Hacker News 의견들
-
내가 일하는 은행 도메인에서는 오히려 soft delete가 유리하다고 느꼈음
deleted_at컬럼이 있으면 쿼리 작성이 명확하고, 분석용 쿼리나 관리자 페이지에서도 동일한 데이터셋을 다룰 수 있음
삭제는 대부분의 경우 드물고, soft delete된 행이 성능 문제를 일으킨 적도 거의 없었음
또한 관계가 그대로 유지되므로 복구(undo) 도 간단함
나는 더 나아가 행을 완전히 불변(immutable) 하게 만들어 업데이트 시 새 행을 추가하는 방식을 선호함
로그를 남기려면 DB 트리거를 이용해 INSERT/UPDATE/DELETE 시 복제 테이블에 기록을 남기는 접근이 좋다고 생각함- 네 말이 맞지만, 삭제가 드문 경우에만 해당된다고 봄
내가 본 테이블 중 50~70%가 soft delete된 경우에는 성능 저하가 확실히 있었음
결국 soft delete는 상황에 따라 다르고, 사전 분석이 필요함 - Postgres에서는 soft delete되지 않은 데이터만 인덱싱하도록 설정할 수도 있음
대부분의 경우 필요 없지만, RAM 절약에는 도움이 될 수 있음 - 은행에서 soft delete는 감사 추적성(auditability) 부족을 임시로 가리는 해결책에 불과함
진짜 해법은 Event Sourcing으로, 모든 변경을 이벤트로 기록해야 함
성능은 떨어지지만, 스냅샷과 동기화(sync) 로 보완 가능함 - DB를 불변 구조로 운영하려면 Datomic 같은 시스템을 고려할 만함
시간여행(time travel) 기능으로 과거 상태를 완전하게 조회할 수 있음 - 예전 보험사에서 근무할 때도 각 테이블을 append-only 로그로 운영했음
최신 상태는 가장 큰 타임스탬프의 행에 있고, 과거 상태는 필터로 조회 가능했음
이 방식은 강력한 이력 관리를 가능하게 함
- 네 말이 맞지만, 삭제가 드문 경우에만 해당된다고 봄
-
soft delete의 가장 큰 함정은 쿼리 복잡도임
처음엔WHERE deleted_at IS NULL만 추가하면 된다고 생각하지만, 몇 달 지나면 필터 누락으로 유령 데이터가 보고서에 등장함
View로 해결할 수 있지만, 결국 병렬 접근 패턴을 유지해야 하고, 삭제된 데이터 조회 시엔 추상화를 우회해야 함
Event sourcing이 더 깔끔하지만 운영 부담이 크기 때문에, 대부분은 하이브리드 접근을 택함- View는 충분히 강력한 도구임
문제는 많은 SWE와 BI 엔지니어가 SQL과 스키마 설계에 익숙하지 않다는 점임
soft delete보다 더 흔한 문제는 Type 2 Slowly Changing Dimension 처리임
대부분 불필요하게 audit table을 만들어 비효율적인 UPDATE/INSERT를 반복함
사실 DB는 정말 아름다운 시스템인데, 그만큼 존중받지 못함이 아쉬움
- View는 충분히 강력한 도구임
-
soft delete가 DB 내장 기능으로 제공되면 좋겠다고 생각함
테이블 단위로 활성화하고, 삭제 처리 전략을 선택할 수 있다면 이상적일 것 같음- 실제로 Iceberg, Delta Lake, BigQuery 같은 시스템은 time travel 기능을 제공함
하지만 많은 팀이 커스텀 요구사항 때문에 결국 SCD(Slowly Changing Dimension) 방식으로 구현함
- 실제로 Iceberg, Delta Lake, BigQuery 같은 시스템은 time travel 기능을 제공함
-
내 경험상 트리거 기반 접근이 가장 안정적이었음
아카이브 테이블은 append-only로 유지하고, 복구는 애플리케이션 레이어에서 처리해야 함
업데이트는 soft delete로 간주하고, 트리거가 이전 상태를 캡처하도록 함
트리거는 반드시 BEFORE 시점에 실행되어야 하며, 로직은 단순해야 함
파티션은 월 단위가 일반적이고, 쓰기 부하가 많으면 일 단위로 나누는 게 좋음 -
나는 DB가 stateful → stateless로 진화하길 바람
모든 변경을 append-only 이벤트로 기록하고, 필요한 데이터는 view로 표현하는 구조를 선호함
DB가 자동으로 materialized index를 관리해주면 이상적임
일부 최신 DB는 이런 기능을 제공하지만, 아직은 OLTP 중심의 발전이 부족함- 이건 결국 Event Sourcing 개념임
Martin Fowler의 설명을 참고할 만함
- 이건 결국 Event Sourcing 개념임
-
예전에 다닌 회사에서는 모든 시스템에 soft delete를 적용했었음
교수님도 “비즈니스 세계에서는 데이터가 절대 삭제되지 않는다”고 하셨던 게 기억남- 완전 삭제는 미래의 데이터 분석 능력을 스스로 제한하는 행위임
저장 공간은 싸니까, 데이터를 절대 지우지 말아야 함 - 하지만 수정에 대해서는 교수님이 아무 말도 안 하셨던 게 흥미로움
- 완전 삭제는 미래의 데이터 분석 능력을 스스로 제한하는 행위임
-
데이터베이스는 사실(fact) 을 저장하는 곳임
레코드 생성은 새로운 사실, 삭제는 또 다른 사실임
하지만 행을 물리적으로 지우면 사실이 사라짐
대부분의 경우 이런 삭제는 바람직하지 않음- 하지만 데이터가 유출 위험을 가진 자산이라면, 오히려 대량 삭제가 필요할 수도 있음
유지 비용과 보안 리스크를 고려해야 함 - DB가 불변이 아니라면, 수정 자체가 이미 사라진 사실을 만드는 셈임
데이터를 영구 보존하는 결정은 신중해야 함 - 개인적으로는 데이터 저장소가 조회와 삽입 두 가지 연산만 지원해야 한다고 생각함
이를 위해 데이터의 수명 주기를 이해하는 것이 중요함
- 하지만 데이터가 유출 위험을 가진 자산이라면, 오히려 대량 삭제가 필요할 수도 있음
-
Firezone에서는 처음에 soft delete를 감사 로그용으로 썼지만, 마이그레이션 문제로 포기했음
대신 Postgres CDC(Change Data Capture) 를 사용해 별도의 쓰기 최적화 테이블로 이벤트를 내보내는 방식으로 전환함
soft delete는 사용자 복구 기능에는 유용하지만, 감사나 규정 준수 용도로는 부적절하다고 생각함- 단순한 프로젝트에서는 DB 변경 대신 API 호출 자체를 감사하는 게 더 효율적임
-
soft delete 필드를 가진 테이블 위에 View를 만들어, 삭제된 행을 숨기는 방식이 깔끔함
이렇게 하면 애플리케이션은 삭제 여부를 신경 쓸 필요가 없음- Postgres의 RLS(Row Level Security) 를 이용하면 soft delete된 행을 자동으로 숨길 수 있음
애플리케이션은 여전히 동일한 테이블에 읽기/쓰기/삭제를 수행함
- Postgres의 RLS(Row Level Security) 를 이용하면 soft delete된 행을 자동으로 숨길 수 있음
-
스키마 드리프트(schema drift) 는 어떻게 처리하냐는 질문이 있음
삭제 당시의 스키마로 직렬화된 데이터를 나중에 복원하려면, 스키마 변경이 문제됨- 내 경험상 아카이브된 객체는 거의 접근되지 않음
삭제 후 며칠 내 복원하는 경우가 대부분이라 스키마 변경 영향이 적음
오래된 아카이브를 새 모델로 마이그레이션하는 건 복잡하고 오류 가능성이 높은 작업이었음
결국 시스템의 사용 방식에 따라 접근 전략이 달라짐
- 내 경험상 아카이브된 객체는 거의 접근되지 않음