Post

자바에서 입출력 스트림과 버퍼가 왜 필요할까?

자바에서 입출력 스트림과 버퍼가 왜 필요할까?

이 글은 Stream API가 아니라 I/O Stream에 대한 내용입니다.

자바 입출력 스트림에 대해 강의를 듣던 중 문득 이런 생각이 들었다.

입출력 스트림이 왜 필요할까? 파일 시스템, 입출력, 네트워크 통신 등에서 왜 Stream이라는 것이 필요할까?

나는 개발 용어를 보면, 그 뜻이나 어원부터 찾아보는 습관이 있다. Stream도 한 번 알아보자.

스트림이 무슨 뜻인가요?

Stream은 한글로 ‘개울’ 이라는 뜻이다.

 또는 시내개울개천(川)은 골짜기나 흐르는 작은 물줄기를 뜻한다. 위키백과

Stream이 뭔 지 알았으니 입출력 스트림이 무엇인 지 한 번 짐작해보자. 위키백과에 따르면 스트림은 흐르는 작은 물줄기 라는 뜻 이다. 컴퓨터의 데이터도 물줄기처럼 끊임없이 흐른다(flow). 그러면 흐르는(flow) 데이터의 줄기를 스트림이라고 볼 수 있겠다. 스트림은 멈추지 않는다. 열거나 닫을 뿐, 중간에 끊을 순 없다. 끝이 없으면 시작조차 하지 않는다. 흐르는 물줄기처럼 말이다.

흐르는 데이터 줄기가 왜 필요하지?

산에 놀러가서 흐르는 계곡물을 본 적이 있는가? 그 계곡물의 산 꼭대기 시작점부터 강과 만나는 끝점 까지의 물을 모두 모아 한 번에 강에다가 물폭탄을 던져보자. 물 폭탄을 맞은 강은 어떻게 될까? 자연 과학 전문가가 아니라서 어떻게 되는 지는 나도 모르겠다. 아마 강이 범람하고 계곡물의 규모에 따라 한강 대교에 엄청난 피해를 줄지도 모른다. 하나 분명한 것은 무슨 문제건 하나는 생길 것이다..

위 예시를 이번엔 흐르는 데이터 줄기에 대입해보자. 예를 들어 10GB짜리 파일 시스템을 몽땅 가져와서 자바 애플리케이션에 데이터 폭탄을 던져보자. 당신의 자바 애플리케이션은 과연 안녕할까? 아마 메모리가 강처럼 범람하여 OutOfMemory 이슈가 발생할 확률이 크다. 또한, 10GB의 대용량 파일은 한 번에 불러오는 것은 유연하지 못하고 제어할 수 없다. 5GB 쯤 가져왔을 때, 상태 체크를 해보고 싶은데 그럴 틈이 없다. 스트림으로 가져오는 데이터는 기본적으로 힙 메모리에 저장된다. 10GB를 힙 메모리에 모조리 올릴 수도 없다.

이제 우리에게 필요한 건 데이터를 적당히 조금씩 가져오는 것이다. 바로, 흐르는 데이터 줄기인 Stream이다. Stream은 1GB든 10GB든 1byte씩 청킹하여 read 하거나 write한다. 10GB는 1byte에게 너무나 큰 산이지만 적어도 OutOfMemory가 발생하지는 않는다. 근데 10GB는… 100억 byte이다. 1바이트씩 청킹하면 100억번의 디스크 I/O가 발생한다. 너무너무 비효율적이다.

이럴 때 필요한 것이 바로 Buffer(버퍼) 이다.

Buffer

입출력 스트림은 기본적으로 1byte씩 가져온다. 1 byte씩 100억번의 I/O 가 발생하면 수행시간은 정말 어마어마할 것이다.

테스트 시간이 너무 오래 걸릴 것 같아, GPT에게 성능 및 수행 시간 측정을 부탁했다.

image

GPT에게 물어보니 대략 이 정도 시간이 걸릴 것이라 추정한다. SSD를 사용하더라도, 파일 하나 입출력 테스트 하려면 퇴근하고 다음 주에나 결과를 확인할 수 있다.

그러면 우리는 1byte씩 가져오면 안되겠다. 한 번에 좀 많이 가져오면 되지않을까?

흐르는 물줄기의 물을 스포이드로 한 방울씩 가져오지 말고, 헬리콥터에 실어서 운반해보자.

image

스트림에서는 이 헬리콥터 물 바구니(용어 모름)를 버퍼라고 부른다. 청킹된 데이터를 담아두는 공간이다. 버퍼를 사용하면 1byte씩 가져오는 것이 아니라, 공간의 크기만큼 데이터를 가져올 수 있다. 버퍼를 사용하여 10GB를 가져와보자.

image

확연하게 시간이 줄었다. SSD 기준으로 6.2일에서 1분으로 시간이 줄었다. 이 정도면 성능개선 몇 %..?

이렇듯 Buffer를 사용하면 1byte씩 사용하던 데이터를 크기에 맞게 모아서 한꺼번에 처리하여 I/O Stream의 성능을 획기적으로 향상시킬 수 있다.

자바가 기본적으로 사용하는 버퍼 사이즈는 8KB이다.

image

자바에서는 Buffer가 적용된 서브 스트림들이 존재한다. 이 스트림들은 단독으로 사용될 순 없다. BufferedInputStream은 서브 스트림에 해당하며 InputStreamBuffer를 적용한 버전이며, 단독으로 사용될 수 없고 반드시 InputStream이 필요하다.

1
new BufferedInputStream(new FileInputStream("filename.txt"));

왜 굳이 8KB(8192byte)인 지는 알 수 없으나, 8KB보다 작으면 잦은 디스크 I/O 발생으로 인해 성능 저하가 있을 것이다. 8KB보다 크면 디스크 I/O 빈도가 줄어들어 성능이 향상될 수도 있다. 하지만 버퍼 크기가 커서 메모리 사용량이 증가하게 되어 시스템 리소스를 더 많이 사용하게 되고, 시간이 오래 걸릴 수 있다. 실제로 버퍼 크기를 8KB 이상으로 해도 드라마틱하게 성능이 향상되지는 않는다.

나가며

나는 깊이 있는 학습은 어떻게 하는 건지 항상 고민해왔다. 이제 조금 알 것 같다. (정말 조금.. little bit) 깊이 있는 학습은 내가 필요한 걸 공부하되 이 기술이 왜 세상에 나오게 되었는 지, 왜 필요한 기술인 지를 공부하는 것부터 시작하는 것 같다. I/O 스트림은 자바를 사용하면서 항상 자신이 없던 부분이었는데, 도대체 왜 스트림이 필요하지? 를 통해 그 메커니즘을 조금 이해하게 되었다. 시간이 걸리더라도 앞으로는 이런 학습 방향을 추구하는 것이 장기적으로 봤을 때 유익하다고 생각한다.

This post is licensed under CC BY 4.0 by the author.