콘텐츠로 이동

소프트 딜리트 / Cascade

모듈: application/service/common/soft_delete/ (추후 core/로 이동 예정). 레코드를 물리 삭제하지 않고 deleted_at에 삭제 시각 기록 + 선언적 Cascade로 자식까지 안전 처리.

1. 왜

연쇄 삭제(StudyRoom → TeachingNote → Homework …)를 서비스마다 직접 하면 ①N+1 ②중복 코드 ③삭제 순서 오류. 이를 오케스트레이터 + 정책으로 해소.

2. 구성

SoftDeleteOrchestrator      실행 엔진(Bean). 트랜잭션 경계 소유, 재귀 Cascade
   └ uses SoftDeletePolicy<T>   도메인별 삭제 정책 인터페이스
        ├ delete(List<T>)        자기 자신 벌크 삭제
        └ getCascadeRules()      자식 종속성 선언
             └ CascadeRule<P,C>  byIds(child, idExtractor, repoMethod) — 벌크 강제
  • Orchestrator가 SoftDeletePolicy 구현체 전체를 자동 수집(Map<Class<?>, Policy>)
  • Bottom-Up 원칙: 자식이 항상 부모보다 먼저 삭제 (FK/무결성 보장)
  • delete 안에서 자식을 직접 삭제하지 말 것 — 재귀는 Orchestrator 담당

3. 신규 도메인 추가 (3 step)

@Component
public class ConnectionDeletePolicy implements SoftDeletePolicy<Connection> {
    @Override public void delete(List<Connection> targets) {
        connectionRepository.deleteAllByIds(targets.stream().map(Connection::getId).toList());
    }
    @Override public Class<Connection> getTargetClass() { return Connection.class; }
    // getCascadeRules() 미오버라이드 = emptyList (리프)
}
// Repository — @SQLDelete는 1+N 유발하므로 직접 @Query 벌크 UPDATE
@Modifying
@Query("UPDATE ConnectionEntity c SET c.deletedAt = NOW() WHERE c.id IN :ids AND c.deletedAt IS NULL")
void deleteAllByIds(@Param("ids") List<Long> ids);

@Component만 붙이면 Orchestrator가 자동 인식.

4. 개선점

문제 기존 이 모듈
N+1 루프 단건 삭제(N쿼리) ID 기반 벌크 UPDATE 1회
순서 보장 서비스마다 직접 재귀가 Bottom-Up 자동
중복 코드 분산 Policy에 선언적 집중
신규 도메인 기존 서비스 수정 Policy 1개 추가
가시성 추적 어려움 앱 시작 시 Cascade 트리 로그

5. 알려진 Side Effect

deleteAll 타입 추론은 targets.get(0).getClass() 기반 → 리스트 원소가 동일 구체 타입이어야 함. Member 단일 테이블 상속 구조에서 문제 가능(검토 예정) → teams/engineering/domains/member.

6. 관련