본문 바로가기

프로그래밍/C++

스트림에서 스트림으로 전송하기

이 글을 읽기 전에 다음 글들을 읽기를 권장합니다.

  1. 한 스트림을 다른 스트림으로 연결하기
  2. 스트림에서 이진 파일 다루기
  3. 스트림에서 STL 사용하기

이번 글에서는 이전과는 색다르게 문제풀이 방식으로 진행해 보겠습니다. 한 스트림으로부터 다른 스트림으로 내용을 전달하는 방법을 여러가지 소개하는 데에는 역시 예시문이 좋을 것 같아 이렇게 편집해보았습니다.

그럼 질문 스타트 !

  1. 스트림에서 스트림으로의 복사

    한 파일이 존재한다고 합시다. 있는 내용을 그대로 복사해서 다른 파일로 복사한다고 해보죠. 어떻게 짜겠습니까 ?

    void foo()
    {
    	FILE *pIn = fopen( "in.txt", "rb" );
    	FILE *pOut = fopen( "out.txt", "wb" );
    	char buffer[1024] = { 0, };
    
    	while( !feof( pIn ) )
    	{
    		const size_t nRead = fread( buffer, 1, 1024, pIn );
    		fwrite( buffer, 1, nRead, pIn );
    	}
    	fclose( pOut );
    	fclose( pIn );	
    }	
    	

    휘유, 고생하셨습니다. 연필값.. 아니 키보드 자판 닳은 값을 드리고 싶군요. 네. 지금은 그래도 스트림에 대해 논하고 있으니 스트림을 쓰는 쪽으로 해봅시다.

    void bar()
    {
    	ifstream ifs( "in.txt", ios::binary );
    	ofstream ofs( "out.txt", ios::binary );
    	char buffer[1024] = { 0, };
    
    	while( ifs ){
    		ifs.read( buffer, 1024 );
    		ofs.write( buffer, ifs.gcount() );
    	}
    }
    	

    네. 감사합니다. 진짜 매우 흡사하군요. 확실히 1024씩 읽으니 효율도 좋을 것 같습니다. 컴퓨터가 좀 좋으면 1024를 훨씬 크게 잡아도 잘 돌 것 같지만 컴퓨터가 좀 안 좋으신 모양이군요. 하하하.

    블럭 단위로 데이터를 보내는 것은 효용성면에 있어서 참 좋습니다. 그럼 여기서 일단 효용은 빼고 더 편한 방법을 찾아볼까요 ? 앞서서 언급했던 반복자를 이용한 코딩을 해봅시다. 우리에게는 스트림에 쓸 수 있는 copy()라는 존재가 있습니다.

    void foobar()
    {
    	ifstream ifs( "in.txt", ios::binary );
    	ofstream ofs( "out.txt", ios::binary );
    
    	copy( istreambuf_iterator< char >( ifs ), 
    		  istreambuf_iterator< char >(),
    		  ostreambuf_iterator< char >( ofs ) );
    }
    	

    오호라, 실제 코드는 한 줄이 되어버렸군요. 게다가 내가 하려는 것이 복사라고 명확히 알려주기까지 했습니다. 블럭단위보다 효용은 떨어지겠지만 확실히 코드가 예뻐 보이는군요.

  2. 개체 단위 스트림 처리

    이번엔 간단한 암호화를 걸어보고 싶습니다. 어떤 키를 제공받아 비트단위 XOR을 걸고 싶군요. 어떤 식으로 하면 예쁘고 편하게 짤 수 있을까요 ?

    struct Encrypt : public unary_function< char, char >
    {
    public:
    	Encrypt( const char key_ ) : key( key_ ) {}
    	const char operator()( const char c ) const
    	{	return c ^ key;	}
    private:
    	char key;
    };
    
    void foofoo( const char key )
    {
    	ifstream ifs( "in.txt", ios::binary );
    	ofstream ofs( "out.txt", ios::binary );
    
    	transform( istreambuf_iterator< char >( ifs ),
    			   istreambuf_iterator< char >(),
    			   ostreambuf_iterator< char >( ofs ), Encrypt( key ) );
    }
    	

    놀랍습니다. 그레이트, 원더풀 ! 깔끔하게 하고자하는 것을 잘 표현했군요. STL이 위력을 발휘하는 건 역시 컨테이너 내부의 원소를 하나하나 처리할 때이죠. 좋습니다. 혹시 struct Encrypt의 효용, 즉 함수객체에 대해 잘 모르시는 분은 STL 책의 함수객체 부분을 읽어봐주세요. Effective STL의 함수객체 파트를 읽어보시면 이해가 잘 될 겁니다.

  3. 스트림 토크나이저

    자, 조금 난이도를 올려보도록 하죠. 주석이 #으로 표시되어있는 프로그래밍 언어가 있다고 합시다. 이 주석은 개행문자까지 주석으로 처리하는 단행주석( Single-line comments )라고 합시다. #를 제거하여 새로운 파일로 만들고 싶어요. 어찌하면 좋을까요 ?

    자, 일단은 #을 찾아야 될 겁니다. 그럼 먼저 떠오르는 게 있을 겁니다. find(). 그렇죠 ! 네. 바로 그 함수입니다. 어, 하지만 find()로 스트림을 검색해버리면 스트림은 그만큼 진행되버립니다 ! find()와 동시에 copy()를 해줘야만 해요 ! 방법이 없을까요 ?

    네, 있습니다. 있고요. 이제 설명해 드릴 타임이 왔군요. 바로 get()입니다 ! 이 함수는 우리에게 익숙한 getline()의 비서식화 용법입니다 ! ( 비서식화 용법이 궁금하신 분은 밑의 스트림에서 이진파일 다루기 글을 읽어주세요. )

    이 함수는 getline()과 용법이 같은데다가 추가로 스트림버퍼를 인자로 받기도 합니다. ( 스트림과 스트림버퍼의 관계가 궁금하신 분은 밑의 한 스트림을 다른 스트림으로 연결하기를 읽어주세요. ) 즉, 스트림에서 스트림으로 바로 복사가 가능하다는 점이죠 ! 게다가 구분자를 인자로 받기 때문에 찾기까지 동시에 행하고 있습니다 ! 다만 안타까운 점은 구분자를 필수적으로 찾기 때문에 구분자에 아무것도 넣지 않아도 개행문자를 만나면 거기까지만 복사를 행합니다. 따라서 안타깝지만 위에서 보여주었던 copy()예제를 따라하기가 쉽지 않습니다.

    하지만 복사와 찾기를 동시에 행할 수 있다는 것은 큰 장점입니다. 이것으로 우리는 #로 되어있는 주석을 제거하는 데 좀 더 쉽게 프로그래밍할 수 있습니다. 자 다음을 보시죠.

    istream &Transfer( istream &is, ostream &os, const char delim = '\n' )
    {
    	is.get( *os.rdbuf(), delim ); // (1)
    	if( is.fail() ) // (2)
    		is.clear();
    	is.get(); // (3)
    	return is;
    }
    	

    이 구문은 get()의 내용을 질문에 맞게 보완하기 위해 만든 함수입니다. 그럼 각 항목을 설명하도록 하겠습니다. 위의 예시에서 크게 화두가 될 만한 내용은 (1)부터 (3)까지 입니다. 전부 다군요 하하.

    일단, (1)입니다. 인자로 *os.rdbuf()를 넘겨주는군요. get()의 첫번째 인자는 스트림버퍼를 받습니다. 하지만 스트림버퍼의 생성자는 protected로 우리가 만들어 줄 수는 없죠. 따라서 스트림으로부터 스트림버퍼를 직접 넘겨줘야합니다. 우리가 저장하려는 스트림의 스트림버퍼를 불러오는 것이 바로 *os.rdbuf()입니다.

    그다음, (2)를 보죠. fail()체크를 하는군요. 이건 왜 필요할까요 ? 일단 스트림의 플래그에 대해서 알아봅시다. 스트림의 플래그에는 3 종류가 있습니다. eofbit, failbit, badbit. 이 세가지입니다. goodbit라고도 존재하는데 이것은 아까 열거한 세 가지 플래그가 하나도 켜져있지 않으면 이 플래그가 켜져있는 겁니다.

    eofbit. 말그대로 End of File인지를 검출합니다.
    failbit. 이녀석이 조금 까다롭습니다. 이건 에러가 아니고 실패입니다. 즉 파일을 읽으라 시켰는데 제 명령을 제대로 못 수행해낸 경우죠. 이 경우는 각 함수마다 조금씩 다릅니다. 스트림으로부터 값을 뽑아낼 때 받을 인자의 길이를 제공하는 함수에서( read(), getline(), get() 등 ) 인자의 길이만큼 뽑아낼 수 없는 경우에 이 실패 플래그가 켜집니다. 즉, 어찌보면 eofbit과 동시에 발생한 것인데 이 때 eofbit와 failbit은 동시에 플래그가 켜지죠. 또 get()에서 스트림 버퍼를 인자로 받을 때만 발생하는 경우가 있습니다. 바로 한 글자도 뽑아내지 못한 경우입니다. get()은 getline()과는 달라서 스트림 내부 포인터가 구분자를 만나면 구분자를 지나서 멈추는 것이 아니라 구분자 전에 멈춰버립니다. 다시말하면, 내가 같은 구분자로 get()을 두 번 실행해 버리면 방금 멈췄던 그 구분자에서 또 멈춰버린다는 겁니다. getline()의 경우는 계속 호출해도 한 줄씩 뽑아져 나오는 것과는 다르죠. 게다가 MSVC7.0의 getline()의 경우에는 get()과는 달리 한 글자도 뽑아내지 못해도 FAIL의 플래그가 켜지지 않습니다.

    자 호흡이 너무 길어진 듯 하니 문단을 나눌만한 상황은 아니지만 나눠보도록 하겠습니다. 조금 간단히 정리해보자면 get()을 쓸 때는 failbit 플래그를 조심해야 한다는 겁니다. 익숙하지 않기 때문에 생각지도 않은 곳에서 failbit이 떠버리면 난감하다는 거죠.
    badbit. 이것은 failbit에 해당하지 않는 다른 모든 에러 상황에서 켜집니다.

    일반적으로 우리가 이 스트림으로부터 더 뽑아낼 수 있는가 없는가를 판별할 때에는 다음과 같은 방법을 쓰는 편입니다.

    void foofoofoo( istream &is )
    {
    	while(is) 
    		// Todo ...
    }
    	

    이것은 스트림의 암시적 변환이 (void *) 하나만 존재하기에 가능한 구문입니다. 스트림 자체에서 진리값을 판별할 수는 없지만 포인터로의 암시적 변환 함수를 제공함으로써 저런 구문을 가능케 했습니다. 저 구문을 MSVC 7.0에서 잠시 차용좀 하겠습니다.

    operator void *() const
    {	// test if any stream operation has failed
    	return (fail() ? 0 : (void *)this);
    }
    	

    별 내용은 없습니다. fail() 함수는 failbit 혹은 badbit 플래그가 켜져있는가 검사하는 함수입니다. 의외로 eofbit 플래그를 검사하지 않습니다. eofbit을 검사하지 않는 이유는 잘 모르겠습니다만, failbit이나 badbit 플래그를 검사하는 것은 진행이 되지 않고 무한루프를 방지할 수 있는 차원에서 좋은 종료조건이라고 생각합니다. 또한 eofbit은 대체로 failbit과 동시에 켜지는 경우가 많기 때문에 보통 크게 문제되지는 않습니다.

    어쩃든 그런 연유로 get()을 연달아 호출하고 그 사이에 스트림의 적법성을 호출하게 되면 파일을 끝까지 읽지도 못하고 도중에 빠져나가는 사태가 발생해버립니다. 특히 구분자가 연달아 두 번 들어가 있는 경우에도 실패로 처리하고 빠져나가버리니 이는 우리가 의도하는 바가 아님이 분명하죠. 그래서 (2)는 fail()일 경우는 모든 플래그를 초기화시키는 clear()를 호출해주는 겁니다. get()의 실패는 우리의 입장에서는 eofbit 말고는 반가운 것이 없기 때문이죠.

    자 이제 (3)입니다. (3)이 있어야 하는 이유는 위에서 설명을 다 했습니다. 첫번째는 구분자 전에 멈추기 때문이고, 두번째는 eofbit을 검출하기 위해서입니다. 참고로 3의 역할은 스트림으로부터 한 글자만 뽑아내는 겁니다.

    조금 복잡한 문제를 냈을 뿐인데 이렇게 저렇게 할말이 많이 들어가버렸군요. 스트림이니 STL이니 접근 장벽이 높은 것이 이해가 됩니다. 알기 전에는 제대로 쓰기도 힘들고 친절한 문서도 잘 없지요. 자, 이제 기운내서 본문을 봐 줍시다.

    void barbar()
    {
    	ifstream ifs( "in.txt", ios::binary );
    	ofstream ofs( "out.txt", ios::binary );
    	ostringstream osTemp;
    
    	while( ifs ){
    		Transfer( ofs, ifs, '#' ); // #을 만날 때까지 out.txt에 기록합니다.
    		// 개행이 나올떄까지 주석이므로 빈 임시 스트림에 기록합니다.
    		Transfer( osTemp, ifs ); 
    	}
    }	
    	

    자. 결국 완성된 본문입니다. 이제는 별 내용 없습니다. 아주 간단한 코드죠. " 쓸모없는 osTemp라는 변수때문에 괜히 눈에 거슬린다. "고 생각하신다면 find()로 대체하여도 좋습니다.

    void barbarian()
    {
    	ifstream ifs( "in.txt", ios::binary );
    	ofstream ofs( "out.txt", ios::binary );
    
    	while( ifs ){
    		Transfer( ofs, ifs, '#' );
    		find( istreambuf_iterator< char >( ifs ),
    			  istreambuf_iterator< char >(), '\n' );
    	}
    }
    	

    아래 구문이 좀 더 보기 좋군요. 정확하게 말하면 주석을 건너뛰는 부분을 Transfer로 칭하기엔 의미적으로 맞지 않으니까요.

이번 내용은 여기까지입니다. 많이 알면 알수록 더 좋은 선택을 할 수 있을 가능성이 커지죠. 스트림은 C++을 쓰는 곳이면 어디나 존재하는 내용이니 스트림을 공부해서 써먹을 수 있을 곳에는 적재적소에 써먹는 것이 쉽고 편하고 나은 C++ 프로그래밍으로 한발짝 더 다가갈 수 있는 것이 아닌가 싶습니다.