유저로 부터 컨텐츠 삭제 요청이 들어 와도,
실제 table에서 record를 delete하는것이 아닌,
그냥 상태만 수정하는 soft delete를 수행하는 경우가 많다.
이는 아래와 같은 이유들 때문이다.
그렇기에 유저에게 삭제기능을 제공해야하는 콘텐츠는 상태 관리를 위한 column을 별도로 두며,
주로 삭제된 날짜를 상태판별에 사용한다.

위 table의 경우 deleted at이 삭제된 날짜를 관리하며,
null여부에 따라 삭제 여부를 판별할 수 있다.
TypeORM 에서는 아래와 같은 요소들로 자체적으로 soft delete 기능을 제공한다.
@DeleteDataColumnsoftRemove()softDelete()withDeleted@DeleteDataColumnTypeORM에서 엔티티의 프로퍼티에게 소프트 삭제 시각을 저장하는 전용 컬럼
softRemove() 또는 softDelete 가 호출되면, 이 컬럼에 현재 시간(new Date())가 자동으로 기록된다.
import { DeleteDateColumn } from 'typeorm';
@Entity()
export class Review {
@PrimaryGeneratedColumn()
id: number;
@Column()
content: string;
@DeleteDateColumn({ name: 'deleted_at' })
deletedAt?: Date;
}
옵션
| 속성 | 의미 |
|---|---|
name | DB에 실제 저장될 컬럼 이름 (deleted_at) |
nullable | 기본값은 true, 생략 가능 |
type | 기본은 timestamp (PostgreSQL, MySQL 등 DB에 따라 적절히 자동 설정됨) |
softRemove() & softDelete()DeleteDateColumn 와 연계되어 논리 삭제를 제공하는 함수
제일 큰 차이는 사용 방식에 있으며, 내부 동작 또한 약간 다르다.
softRemove()
엔티티를 통해 삭제한다
const review = await this.reviewRepo.findOne({ where: { id: 3 } });
await this.reviewRepo.softRemove(review);
내부적으로 아래 sql을 수행
UPDATE reviews SET deleted_at = CURRENT_TIMESTAMP WHERE id = 3;
엔티티를 미리 로드해야 하고, Entity Lifecycle hook(
@BeforeRemove,@AfterRemove)이 작동한다.
softDelete()
조건을 바탕으로 삭제한다
await this.reviewRepo.softDelete(3);
// 또는
await this.reviewRepo.softDelete({ user_id: 1 });
그리고 내부적으로 아래 sql을 수행한다.
UPDATE reviews SET deleted_at = CURRENT_TIMESTAMP WHERE id = 3;
엔티티를 로드하지 않으므로 빠르고, lifecycle hook이 작동하지 않는다.
결론
SQL 수준에서 보면 둘다 동일한 UPDATE이다.
성능저하를 감안하여 Entity Lifecycle hook과 연계할지(softRemove)
빠른 성능을 최우선 할지(softDelete)
용도에 따라 적절히 선택하여 사용한다.
TypeORM에서 흔히 쓰이는 조회 함수이다.
await this.reviewRepo.findOne({
where: { id: 1 }
});
이외에도 findOne, findOneBy, findBy 등의 함수들이 있으며,
모두 기본적으로 soft delete를 지원한다.
즉, @DeleteDataColumn이 설정된 column에 null 이외의 값이 저장되어 있다면,
soft delete 상태인것으로 간주하여 검색에서 자동 제외한다.
withDeleted 옵션find 계열의 함수로 soft deleted 상태의 데이터를 포함하여 검색하고 싶다면
withDeleted: true 옵션을 사용하면 된다.
restoresoft delete 상태의 엔티티를 다시 복구해준다.
await repo.restore(id); // 가능
await repo.restore({ user_id: 1 }); // 가능 (TypeORM v0.3 이후)
TypeORM 사용시 QueryBuilder 는 soft delete를 자동 적용하지 않음에 주의해야한다.
await repo.createQueryBuilder('review').getMany(); // ❗ deletedAt 무시됨
해결책
soft delete는 애플리케이션 레벨에서의 관리 기법 이므로, DB에서 직접 설정할 필요는 없다.
다만, 운영 환겡여선 DB 차원에서 아래와 같은 작업들을 고려하는것을 추천한다.
CREATE INDEX idx_reviews_deleted_at ON reviews(deleted_at);
대부분의 쿼리에서 WHERE deleted_at IS NULL 조건을 사용하므로,
soft delete 상태의 레코드를 빠르게 넘길 수 있음
오래된 soft delete 데이터를 영구 삭제하기 위한 작업은 아래와 같이 진행한다.
DELETE FROM reviews
WHERE deleted_at IS NOT NULL
AND deleted_at < NOW() - INTERVAL '30 days';
soft deleted 상태의 데이터를 절대 수정하지 못하게 막는 trigger를 적용하면
보안, 무결성 관점에서 도움이 된다.
CREATE OR REPLACE FUNCTION prevent_update_on_soft_deleted()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.deleted_at IS NOT NULL THEN
RAISE EXCEPTION 'Deleted records cannot be updated';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER block_update_after_soft_delete
BEFORE UPDATE ON reviews
FOR EACH ROW
EXECUTE FUNCTION prevent_update_on_soft_deleted();
위 트리거가 적용되면 soft delete 상태의 record는 UPDATE 할 수 없게된다.
이는 금융, 의료, 법률등의 특정 도메인에서는 매우 유용하게 사용된다.