C++의 ifstream의 small buffer 문제
C++ 프로그램에서 ifstream을 자주 사용하게 되는데, 주로 이 클래스의 멤버 함수인 getline()을 이용하여 한 줄씩 처리하는 경우가 대부분이다.
이럴 때 아무 생각없이 getline()에 사용할 buffer의 크기를 너무 작게 잡으면 해결하기 어려운 문제가 발생할 수 있다.
다음의 데이터를 포함한 텍스트 파일이 있다.
hello
world
Extracts characters from the input sequence and stores them as a c-string into the array beginning at s.
java
coffee
일반적으로 사용하는 코드는 다음과 같다. 물론 C++ 프로그래머마다 약간씩의 스타일의 차이는 있겠지만, Stroustrup의 C++ 책에도 getline()의 사용방식에 대해 특별한 제한이나 가이드를 두고 있지 않으므로 while loop에서 getline()을 단순 반복하는 것이다.
#include#include #include #include using namespace std; const int MAX_LINE_LEN = 20; int main(void) { ifstream fstrm; string keyword_file = "test.txt"; char line[MAX_LINE_LEN]; fstrm.open(keyword_file.c_str()); if (fstrm.is_open() == false) { cout << "Error: can't open keyword file '" << keyword_file << "', " << errno << ": " << strerror(errno) << endl; return -1; } while (fstrm.getline(line, MAX_LINE_LEN)) { cout << line << endl; } fstrm.close(); }
이 코드는 처음 두 줄만을 출력하고 20 바이트를 초과하는 입력을 만나게 되면 바로 종료하게 된다. getline()이 buffer overflow에 대해서 0을 반환하기 때문에 while loop를 탈출하게 되는 것이다.
hello
world
while loop를 분리하여 다음과 같은 코드를 사용하게 되면 어떻게 될까?
while (1) { fstrm.getline(line, MAX_LINE_LEN); cout << line << endl; }
getline()에 대해서 체크를 하지 않기 때문에 무한 루프에 빠진다. EOF를 확인하도록 다음 코드를 써도 마찬가지이다.
while (fstrm.eof() == false) { fstrm.getline(line, MAX_LINE_LEN); cout << line << endl; }
그럼 EOF 대신에 fstream 객체의 상태를 확인하는 good() 함수를 사용하면 어떻게 될까?
while (fstrm.good()) { fstrm.getline(line, MAX_LINE_LEN); cout << line << endl; }
딱 20바이트까지만 읽고 종료하게 된다. 파일의 나머지 부분을 읽지 못하는 것이다.
hello
world
Extracts characters
여태까지의 예제 코드들은 모두 원하는 결과를 내지 못했다. 이제 두 가지 선택이 존재한다.
첫번째는 버퍼 이상의 입력은 무시하고 다음 줄로 넘어가는 것이다.
두번째는 버퍼를 동적으로 늘려서라도 모두 읽어내는 것이다. 그러나 이 방법은 사용자의 입력을 무한히 받으려고 시도하다가 메모리 부족을 겪거나 스택 오버플로우로 인해 보안 상의 결점을 노출시키게 된다. 이 방법에 대한 자세한 설명은 생략한다.
첫번째 방법은 다음과 같이 구현할 수 있다. getline() 호출 후에 fstream의 상태를 체크해보는 것이다. fstream의 상태가 좋지 않지만 EOF는 아닌 상태라면 에러 상태를 초기화하고 다음 줄부터 다시 읽도록 하는 것이다.
while (fstrm.eof() == false) { fstrm.getline(line, MAX_LINE_LEN); if (fstrm.good() == false && fstrm.eof() == false) { fstrm.clear(); fstrm.ignore(MAX_LINE_LEN, '\n'); continue; } cout << line << endl; }
hello
world
java
coffee
ignore() 함수를 호출하게 되면 버퍼보다 큰 입력의 (버퍼 크기만한) 뒷쪽 부분을 버리는 효과를 가진다. ignore() 함수를 호출하지 않는다면 다음과 같은 결과를 얻게 될 것이다.
hello
world
ing at s.
java
coffee
세번째 방법도 있긴 하다. gcount()를 이용하여 getline()이 읽어낸 데이터의 크기를 확인하여 버퍼 크기보다 작거나 같은지를 확인하는 것이다. do while loop으로 감싸면 된다. 개인적으로 그다지 좋아하는 스타일은 아니다.
* 1년 동안 마음에 담아뒀던 내용을 정리하려고 하니 잘 기억이 나질 않아서, 막상 글로 쓰려고 하니까 거친 표현이 난무할 뿐 다듬어지질 않는다. 이 문제에는 사실 단 한 가지의 정답은 없고 여러가지 방법이 있을 수 있다. 그러나 의외로 이 문제에 대해 주위 개발자들에게 물어보면 고민을 해 본 사람이 없어서 한 번 정리해봐야겠다는 생각을 하고 있었다. 아무래도 내가 한동안 맡았던 개발 업무가 검색어들을 정해진 시간 내에 집계하고 처리하는-필요하면 버리기도 하고-일이다보니 나만의 고민이 아니었을까 싶다.