운영 데이터를 지워야 하는데 관리 화면에 삭제 기능이 없어서 직접 지우는 수밖에 없었다. 지우기 전에 복구가 가능한지 확인하려고 Member 엔티티 파일을 열었다.
Table of contents
Open Table of contents
증상 — 들여오기와 적용의 차이
파일 상단에 import org.hibernate.annotations.SQLDelete; 가 있어서 지워도 deleted_at 표시만 되는 줄 알았다. 그런데 클래스 위에 @SQLDelete(sql = "UPDATE members SET deleted_at = NOW() WHERE id = ?") 선언이 없었다.
파일 상단의 import 는 이름을 들여오는 것이다. 클래스 위의 애너테이션은 그것을 적용하는 것이다. Hibernate 5.4 공식 문서대로 @SQLDelete 는 붙인 엔티티에 대해서만 DELETE 문을 바꾼다. 같은 이름이 위치에 따라 다른 일을 하므로 들여왔다는 사실만으로는 켜졌다고 볼 수 없다.
검증 — 검색을 두 갈래로 나눈 이유
이것을 확인하려면 언급이 있는지와 클래스 선언이 있는지를 따로 봐야 했다. grep -rn SQLDelete 로 이름만 검색하면 상단의 import 가 걸린다. 그러면 소프트 삭제 엔티티로 잘못 판정한다.
grep -rn '^@SQLDelete' 로 줄 첫머리 애너테이션 형태로 다시 검색하니 그 줄이 없었다. 이 엔티티의 삭제는 표시가 아니라 실제 삭제였다.
원인 — 이력은 남고 복구는 안 되는 조합
같은 엔티티에 Envers 의 @Audited 는 제대로 선언돼 있었다. 변경과 삭제를 members_AUD 이력 테이블에 자동으로 남기는 것이었다.
그래서 이 테이블은 자동 이력은 있는데 복구는 불가능한 조합이 된다. 무엇이 지워졌는지는 이력으로 알 수 있지만 데이터 자체는 되돌릴 수 없다.
주의 — 직접 질의는 이력도 건너뛴다
여기에 하나가 더 있었는데 그 이력 기록이 Hibernate 이벤트로 동작한다는 점이었다. JdbcTemplate 직접 질의로 지우면 엔티티를 거치지 않으므로 이력도 남지 않는다.
내가 하려던 것이 바로 그 방식이었다. 엔티티를 거친 삭제는 흔적이라도 남지만 직접 질의는 아무 흔적 없이 사라지게 만든다.
조치 — 우회한 만큼 앞에서 채우는 순서
두 가지 방법이 있었다. 하나는 EntityManager.remove() 를 부르는 일회성 코드를 짜서 배포하는 것이다. 다른 하나는 대상 행을 mysqldump --where 로 통째로 백업하고 Envers 가 남겼을 형식대로 members_AUD 에 이력을 손으로 넣은 뒤에 지우는 것이다. 뒤쪽을 골랐다. 이유는 지울 대상이 몇 건이라 배포를 태우는 비용이 더 커서다. 대신 _AUD 행의 REVTYPE 과 REV 를 손으로 맞춰야 하는 것을 감수했다. 순서를 이렇게 잡은 이유는 지운 뒤에는 이력에 넣을 내용을 다시 알아내야 해서다.
일반화하면 운영 데이터를 직접 조작할 때 자동 훅의 목록을 먼저 만들어야 한다. 이력과 캐시 갱신과 이벤트 발행 중 필요한 것을 앞에서 채운다. 그러고 나서 조작한다.
정리
- 파일 상단의
import는@SQLDelete적용 선언이 아니다 - 같은 이름이 위치에 따라 다른 일을 한다
- 언급 여부와 클래스 선언을 따로 검색해야 판정이 된다
@Audited이력은 있는데 복구는 불가능한 조합이 있다- 두 기능이 켜졌는지를 각각 확인한다
JdbcTemplate직접 질의는 Hibernate 이벤트를 안 타므로 이력도 안 남는다- 우회하기로 했으면 안 도는 것 목록을 만들고 앞에서 채운다
- 삭제 전 전체 행 백업이 언제나 먼저다