새로운 프로젝트를 진행 중, 내부 망에서 진행한 테스트였음에도 반응속도가 더뎌지는 경우가 발생했다. 다행히 그 원인을 발견하는 데는 오랜 시간이 걸리지 않았는데, 다름아닌 사내 로깅(Logging) 라이브러리가 그 주범이었다. 안타깝게도, 라이브러리 담당자도 원인을 찾아주지는 못했다. 이 프로젝트는 철저히 혼자 만드는 것이었고, (존재한다면 반드시 사내 라이브러리를 사용해야 한다는 등의)기술적인 간섭 받지 않았기 때문에 로깅 라이브러리를 간단히 새로 만들었고, 결과는 만족스러웠다. 물론, 사내 라이브러리에 대한 불신은 좀 더 커졌다.
기존 프로젝트에도 꾸준히 문제되는 부분이 있었는데, 이 역시 다름아닌 로깅이었다. 이 프로젝트에서 사용되는 로깅 라이브러리는 위에서 언급했던 사내 로깅 라이브러리보다 더 오래된 것으로, 큰 문제가 없어서 그냥 사용하고 있었지만, 로그 양이 갑자기 증가하는 경우에는 문제가 발생했다. 결국, 기존의 소스코드를 약간 수정해서 성능이 개선되는 것을 확인했다.
수정한 부분은 기본 API인 WriteFile() 함수대신 fputs() 함수를 사용한 것인데, 선뜻 이해가 되지 않았던 것은, fputs() 함수도 결국 기본 API를 사용하므로, 성능이 느려지면 느려져야지, 빨리질 수 없다는 사실이었다. 그러나 분명히 성능이 개선되었으므로 소스를 커밋했다.
수정된 소스가 반영되어 세상에 나가기 이틀 전. 전체회의에서 ‘WriteFile() 함수가 fputs() 함수보다 느릴 수 없다’라는 이유로, 소스 적용이 취소되었고, 난 다시 원인을 찾기 시작했다.
내가 가장 의심했던 부분은, WriteFile()함수와 CreateFile() 함수에 붙어있는 무수한 플래그였다. 특히, WriteFile() 함수를 중심으로 살펴봤는데, 별다른 문제를 발견하지 못했다. 그렇지만, 역시나 대부분의 시간은 이 함수가 소모하고 있었다.
결국, 파일과 관련되지 않은 다른 모든 부분을 제거했다. 여전히 느렸고, 따라서 다른 부분은 문제가 아니라고 확신할 수 있었다.
그 전에, 내가 만든 로깅 라이브러리의 파일 입출력 부분을 모두 WriteFile() 함수를 사용하여 시간을 재어보니, 속도가 미미하게나마 더 빨라졌다. 당연히 이래야 하는 거잖아~!?
시간을 되돌려 다시 CreateFile() 함수와 WriteFile() 함수를 살펴보고 있었다. 그리고, 바로 전에 테스트 한 부분과의 차이가 눈에 들어왔다. 내가 만든 라이브러리에서 CreateFile() 함수를 사용할 때는 FILE_FLAG_WRITE_THROUGH를 사용하지 않았다.
다시 테스트해본 결과, 문제의 원인은 FILE_FLAG_WRITE_THROUGH이 확실했다. 구세주 MSDN과 StackOverflow의 글을 살펴본 결과 위 플래그의 역할은 다음과 같다.
C API를 사용하여 파일에 데이터를 기록하려고 할 때, 기본적으로 flush()함수를 호출하지 않으면 파일에 데이터가 기록되어있다는 것을 보장할 수 없다. 이는 쓰려는 데이터가 어딘가에 임시로 저장되어 있다는 것이며, 이로 인해 성능(속도)가 증가할 수 있다. FILE_FLAG_WRITE_THROUGH 플래그는 이 동작을 제어하며, 이 플래그를 사용하여 파일을 생성하였을 경우, WriteFile() 함수는 데이터를 버퍼링하지 않고 바로 디스크캐시로 보낸다. 그러나 디스크캐시로 보낸다는 것이 물리 디스크에 기록하는 것과 동일한 의미는 아니며, 이를 위해서는 추가로 FILE_FLAG_NO_BUFFERING 플래그를 사용해야 한다. 그러면, WriteFile() 함수의 종료 후, 데이터가 물리 디스크에 저장되었다고 보장할 수 있으며, 이에 대한 트레이드오프로 성능(속도)이 떨어지게 된다. 테스트결과, FILE_FLAG_WRITE_THROUGH 플래그만으로도 수 백배의 성능차이가 발생한다.
아마도 라이브러리 개발자는 버퍼링되는 로그가 파일에 남지 않게 될 것을 우려해서 FILE_FLAG_WRITE_THROUGH 플래그를 기본 옵션으로 설정해 둔 듯 하나, 엄밀하게는 FILE_FLAG_NO_BUFFERING 플래그도 함께 설정해야 했다.
그래도, 로깅의 성능은 상당히 크리티컬한 부분인 만큼, 그 의도는 납득하기 어려우며, 오히려 flush() 함수와 같은 인터페이스를 만드는 것이 옳지 않았을까 싶다.
결론: 네가 이해 못하는 코드는 있어도, 이해할 수 없는 현상이 발생하는 코드는 없다.
참고사이트 MSDN, StackOverflow
잘 배우고 갑니다~