사용자 삭제 내역 보존
컨텍스트
2026년 6월 13일 이전에는 사용자, 학생, 직원 행을 삭제하면 ON DELETE CASCADE 외래 키를 통해 작업 기록까지 지워질 수 있었습니다. 신원 정리용으로는 괜찮아 보였지만, 행위자가 떠난 뒤에도 테넌트가 보존해야 하는 출결, 수업, 시험, 상담, 공지사항, SMS 기록에는 허용될 수 없는 동작이었습니다.
기본 출시는 27e5603b에서 이루어졌으며 후속 커밋으로 사용자 대상 동작이 조정되었습니다.
2a960e47: 삭제된 액터를 null 누출 대신 명시적인 자리 표시자로 렌더링하도록 학습된 모델을 읽습니다.71fcf736,e6b1cf7e및c4d9d4fe: 팀은 활성 수업 등록이 삭제를 차단해야 하는지 또는 중도 탈락으로 전환되어야 하는지 여부를 반복했습니다.
관련 정상 상태 참고 자료는 학생 서비스 및 수업 서비스 페이지에 있습니다.
무엇이 깨졌거나 변경되었는지
스키마와 애플리케이션 계층이 모두 이동해야 했습니다.
V66__preserve_user_history_on_delete.sql는 과거 외래 키를 계단식 의미 체계에서ON DELETE SET NULL로 변경했습니다.- 이전 외래 키가
NULL가 된 경우 제품이 "삭제된 행위자"와 "할당되지 않음"을 구별할 수 있도록exam_results.student_deleted및classes.teacher_deleted가 마이그레이션에 추가되었습니다. StudentCommandService.deleteStudent(...)는 단순한 인증 사용자 삭제가 중단되었습니다. 이제 비활성 학생 전제 조건을 시행하고ClassService를 통해 활성 수업 등록을 정리하며 향후 상담 작업이 진행 중일 때 삭제를 거부합니다.
마이그레이션의 핵심 계약은 명시적입니다.
-- lumie-backend/app/src/main/resources/db/migration/public/V66__preserve_user_history_on_delete.sql
alter table if exists sms_messages
add constraint sms_messages_sender_id_fkey
foreign key (sender_id) references staff(id) on delete set null;
진단 및 결정 사다리
- 팀은 먼저 컨트롤러 코드가 아닌 스키마에서 데이터 손실 근본 원인을 수정했습니다. 이것이 바로
V66__preserve_user_history_on_delete.sql가 여러 테이블을 터치하고 삭제 마커와 트리거를 추가하는 이유입니다. - 첫 번째 애플리케이션 계층 반응은 보수적이었습니다. 현재 수업이나 향후 상담 예약이 여전히 존재하는 경우 삭제를 차단합니다.
- 수업 중퇴 자체가 유효한 역사이기 때문에 학 생 생활주기 현실에 비해 이는 너무 엄격한 것으로 판명되었습니다.
71fcf736는 삭제 시 자동 활성 등록 삭제를 도입했습니다. e6b1cf7e는 첫 번째 구현에서 클래스 상태 처리가 회귀된 후 블록을 잠시 복원했습니다.c4d9d4fe는 현재 단일 삭제 흐름에서 살아남은 동작인 드롭아웃 경로를 다시 도입했습니다.
현재 단일 삭제 경로는 최종 결정을 보여줍니다.
// lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentCommandService.java
if (student.isActive()) {
throw new BusinessException(StudentErrorCode.CANNOT_DELETE_ACTIVE_STUDENT);
}
classService.dropActiveEnrollmentsForStudent(tenantSlug, id);
AuthService.DeleteUserResult deleteResult = authService.deleteUser(userId);
해결
스키마 보존과 애플리케이션 정리를 결합한 내구성 있는 수정 사항:
- FK 대상이 null을 허용하고
ON DELETE SET NULL를 사용하기 때문에assignment_submissions,attendance_records,class_enrollments,counseling_*,exam_results및sms_messages의 기록 행은 ID 삭제 후에도 유지됩니다. V66__preserve_user_history_on_delete.sql의 삭제 전 트리거는exam_results.student_deleted및classes.teacher_deleted를 표시하여 소유자 행이 사라진 후에도 UI가 기록 의미를 유지할 수 있도록 합니다.- 단일 학생 삭제는 이제 수업 내역을 지우는 대신 활성 등록을 삭제하는 반면, 예정된 상담 예약은 여전히 삭제 경로를 차단합니다.
예방
- 삭제 경로가 모듈 경계를 넘으면 지속성 계약을 먼저 수정하세요. 애플리케이션 검사만으로는 데이터베이스 수준 계단식 작업을 중지할 수 없습니다.
- 외래 키를 변경하기 전에
NULL의 의미를 유지하세요.student_deleted및teacher_deleted는NULL가 이미 해당 테이블에서 다른 것을 의미했기 때문에 존재합니다. - 비즈니스에 여전히 기록이 필요한 경우 삭제를 영원히 차단하는 대신 활성 수명 주기 상태를
ENROLLED -> DROPPED와 같은 기록 터미널 상태로 변환하는 것이 좋습니다. - 관찰해야 할 현재 드리프트:
deleteStudent(...)및batchDelete(...)는 검사된StudentCommandService.java에서 동 일한 보호 장치를 공유하지 않습니다. 단일 삭제는 활성 등록을 삭제하고 향후 작업을 확인합니다. 일괄 삭제는 여전히 인증 삭제를 호출하기 전에 활성 학생만 거부합니다. 향후 변경에서는 해당 차이가 의도적인지 여부를 결정하고 이를 문서화하거나 제거해야 합니다.