주소가 남아 있어도 객체는 사라질 수 있다

메모리를 해제한다고 그 주소를 담은 모든 포인터가 자동으로 널이 되지는 않습니다. 같은 주소를 복사해 둔 포인터도 그대로 남습니다. 해제된 영역에 이전 데이터가 잠시 보이면 프로그램이 정상적으로 동작하는 것처럼 느껴질 수 있지만, 이후의 할당이 그 공간을 재사용하면 값이 달라집니다. use-after-free는 이런 수명이 끝난 객체를 다시 사용하는 문제입니다. 디버거에서 메모리를 읽을 수 있는지와 언어 규칙상 객체를 사용할 수 있는지는 다른 질문입니다. 값이 남아 있다는 이유로 유효한 객체라고 판단하지 말고 누가 언제 해제했는지 확인해야 합니다.

생성·전달·해제·사용을 시간순으로 놓기

문제를 좁힐 때는 객체가 생성된 위치, 다른 곳으로 전달된 위치, 해제된 위치, 나중에 사용된 위치를 순서대로 기록합니다. 그중 어느 하나만 보면 연결을 놓치기 쉽습니다. 특히 컨테이너에서 원소를 지우거나 객체를 교체하는 작업은 명시적인 free나 delete가 보이지 않아도 수명과 주소에 영향을 줄 수 있습니다. 콜백이 객체 주소를 저장해 뒀다가 나중에 실행하는 구조라면 등록 시점보다 실행 시점이 중요합니다. 소유한 쪽은 이미 종료되었는데 작업 큐에는 이전 주소가 남아 있지 않은지 호출 경로와 종료 순서를 함께 조사하세요.

다중 스레드에서는 멈춤 자체가 조건을 바꾼다

해제와 사용이 서로 다른 스레드에서 발생하면 중단점에 멈추는 것만으로도 실행 순서가 바뀌어 문제가 사라질 수 있습니다. 이때 재현이 안 된다는 이유로 가설을 버리기보다 로그나 메모리 검사 도구를 병행할 필요가 있습니다. DBG Desk의 현재 스택 화면은 정지한 스레드의 정보이며 모든 스레드의 소유 관계를 자동으로 보여주지 않습니다. 객체를 공유하는 경로, 잠금이나 참조 관리의 범위, 작업 취소와 종료가 만나는 시점을 별도로 점검하세요. 검사 도구가 보고한 할당·해제 위치는 디버거의 정지 위치와 연결해서 해석하면 유용합니다.

사용 지점의 검사보다 수명 규칙 고치기

해제 후 사용하는 포인터가 널이 아니면 단순한 널 검사로는 막을 수 없습니다. 해제한 변수 하나를 널로 만들어도 이미 복사된 다른 포인터에는 영향을 주지 않습니다. 수정은 객체를 누가 소유하는지, 비동기 작업이 끝날 때까지 어떻게 살려 둘지, 종료 시 남은 작업을 어떻게 처리할지라는 규칙에서 시작해야 합니다. 언어와 구조에 맞는 소유권 표현을 사용하고 같은 객체를 두 곳에서 중복 해제하지 않도록 정리하세요. 수정한 뒤에는 정상 실행뿐 아니라 빠른 취소, 반복 생성과 종료, 실패 도중의 정리 경로처럼 수명이 짧아지는 조건도 재현해 확인합니다.

종료 직후 콜백을 확인하는 예

화면을 닫을 때 객체를 해제하는 코드와 백그라운드 작업이 결과를 전달하는 코드를 함께 생각해 보세요. 작업이 빠를 때는 결과 전달 후 화면이 닫혀 문제가 없지만, 느릴 때는 해제한 화면 객체를 콜백이 사용할 수 있습니다. 이 경우 계산 결과가 맞는지보다 콜백 실행 시점에 수신자가 살아 있는지가 질문입니다. 작업 취소가 완료를 보장하는지, 취소 요청 뒤에도 이미 예약된 호출이 남는지 확인해야 합니다. 빠르게 열고 닫는 반복과 의도적으로 느린 작업을 조합하면 조건을 재현하기 좋습니다. 수정은 종료와 결과 전달이 만나는 규칙을 명시하는 방향으로 검토합니다.