소프트 딜리트 / 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.