대부분의 개발자들이 원하지 않는 파일이 삭제되어 패닉 상태에 빠진 경험 한 두번쯤 있을 것이다. 오늘은 그와 반대로 삭제하려는 파일이 삭제되지 않아 고생했던 나의 경험을 나누어 보려고 한다.
어느날 시스템 운용자로부터 연락을 받았다.
"OOO 파일이 삭제되지 않고 계속해서 용량이 증가하고 있어요."
코드는 단편적으로 보았을 때 온데 삭제 코드를 넣어 두어서 반드시 파일이 삭제될 것만 같아 보였는데 삭제가 되지 않았다니 이상했다.
아래는 파일 처리를 담당하는, 별도의 쓰레드로 동작하는 문제 코드의 대략의 구조이다.
DWORD __stdcall ThreadFunc(LPVOID param) {
unlink("Temp.dat"); // 파일 삭제
while (TRUE) {
...
fpPB = fopen("Temp.dat", "ab"); // 파일 열기
fwrite(TempData, size, 1, fpPB); // 파일 쓰기
fclose(fpPB); // 파일 닫기
...
}
unlink("Temp.dat"); // 파일 삭제
}
코드를 분석해 보았더니 쓰레드 외부에서 TerminateThread() API를 통해 아무런 동기화 없이 위 쓰레드를 강제 종료시켜 버리고 있었다. 그래서 쓰레드 수행 중 파일이 열린 상태에서 쓰레드가 강제 종료되는 케이스가 매우 간헐적으로 일어 났고, 닫히지 않은 파일은 동일 프로세스 내에서 삭제할 수 없도록 제한되고 있었다.
문제를 해결하기 위해서 쓰레드를 안전하게 종료해 주도록 코드를 수정해야 했다. 쓰레드를 안전하게 종료시키기 위한 내용들은 아래에 정리해 두었다.
Make : 안전한 쓰레드 종료 코드 직접 만들기
그런데 막상 코드를 수정하려고 달려들어 보니 관련된 코드들이 프로그램 곳곳에 흩어져있고 관련 코드들은 모듈화 되어 있지 않아 사이드 이펙트들을 예상하기 매우 힘들었다. 그리고 이 엄청난 수정을 감행하더라도 수정한 곳곳의 코드를 시험하기 위한 하드웨어 시스템은 크리티컬한 미션을 수행하기 위해 24시간 운영중이라 실제 시험을 거의 할 수가 없었다.
그래서 더 간단하고, 사이드 이펙트 걱정이 적은 방법을 찾는 것이 옳겠다고 판단했다.
결론은 아래와 같이 보다 간단하게 수정하기로 결정했고, 적용해서 문제들을 수정할 수 있었다.
CRITICAL_SECTION g_criticalSection;
...
DWORD __stdcall ThreadFunc(LPVOID param) {
unlink("Temp.dat"); // 파일 삭제
while (TRUE) {
...
EnterCriticalSection(&g_criticalSection);
fpPB = fopen("Temp.dat", "ab"); // 파일 열기
fwrite(TempData, size, 1, fpPB); // 파일 쓰기
fclose(fpPB); // 파일 닫기
LeaveCriticalSection(&g_criticalSection);
...
}
unlink("Temp.dat"); // 파일 삭제
}
void TeminateThreadSafe(HANDLE handle, DWORD exitCode) {
EnterCriticalSection(&g_criticalSection);
TerminateThread(threadHandle, exitCode);
LeaveCriticalSection(&g_criticalSection);
}
void TerminateFunc(LPVOID param) {
...
// TerminateThread(handle, exitCode);
TerminateThreadSafe(handle, exitCode);
...
}
어떤 프레임워크에서든 파일 삭제 API를 호출했는데 파일이 삭제되지 않는다면 파일을 열고 닫지 않아 누수가 발생하는 코드가 없는지 확인해 볼 필요가 있다. 그리고 그 대표적인 원인 가운데 하나는 쓰레드를 아름답지 않게(?) 마구마구 종료시켜주는 코드가 원인이 될 수 있음을 기억하자! 🤣