[Java] 자바에서 입출력이 이루어지는 과정
자바에서 입출력이 이루어지는 과정에 대해 알아보자. 주로 Stream, Buffer에 대하여 다룬다. 입출력에 대해 이해하기 위해서는 먼저 Stream을 알아야 하고, 입출력 과정에서 Buffer를 사용하는 것과 사용하지 않는 것의 차이를 아는 것이 중요하다. 자바에서 입출력은 Stream을 이용하는 단방향 통신의 IO와 Channel을 이용하는 양방향 통신의 NIO가 있다. Stream의 경우 단방향이기 때문에 입력 혹은 출력을 하기 위해서는 용도에 맞는 스트림을 각각 열어야만 하고 입력 스트림은 입력으로만, 출력 스트림은 출력으로만 사용가능하다. Channel의 경우 Stream과 달리 하나의 채널로 양방향으로 통신이 가능하다. 본 포스팅에서는 Stream에서 출력이 이루어지는 과정에 대해 알아본다.
Stream
Stream 이란 입력 장치와 프로그램 간에 데이터가 이동하는 가상의 통로로, 데이터를 입력받는 InputStream과 데이터를 출력하는 OutputStream으로 나뉜다. Consol 입출력, 파일 입출력, 네트워크 통신 등 스트림은 다양하게 사용되며 자바에서는 각 상황에 맞는 다양한 Stream 클래스를 제공하고 있다. Stream은 FIFO 특성을 가지며 데이터를 입력하는 순서대로 들어오고 들어온 순서대로 도착한다. 자바에서 모든 IO는 Stream을 통해서 이루어진다.
자세히 살펴보면 알 수 있듯이 IO Stream은 항상 Read(input), Write(Output) 두 가지가 존재한다. 즉 단방향 통신이라는 이야기다. 입력을 받지 않고 출력만을 한다면 OutputStream에 관한 클래스만 사용해도 되고, 입력받아서 처리한 후 출력할 내용이 없다면 InputStream만 써도 된다는 뜻이다.
입출력에 있어서 Stream 개념이 중요한 이유는 우리가 입력하고 출력하는 데이터들이 화면에 표시되기 전에 이 Stream이라는 통로를 거치기 때문이다. 이 개념을 이해하지 못하면 입출력이 이루어지는 과정을 이해하는데 어려움이 있기 때문에 잘 숙지해야 한다.
Buffer
Stream은 입력 장치와 프로그램 간의 데이터 통로라고 했다. 사용자가 키보드에서 'a'를 입력하면 그 즉시 프로그램에 'a'가 전달된다. 사용자의 입력이 프로그램에 즉시 전달되는 것은 성능에 나쁜 영향을 준다. 하지만 실제로 처리되는 과정에서 대량의 데이터가 입출력되는 와중에 하나의 문자를 계속해서 전달하는 것은 굉장히 비효율적이다. n 개의 문자가 있을 때 n번 데이터 전송이 이루어진다는 것이다.
이 문제를 해결하기 위해서 buffer라는 개념이 등장한다. 데이터를 계속해서 보내지 말고 어딘가에 쌓아두다가 일정량만큼 쌓이면 데이터를 전송하자는 것이다.
버퍼를 사용하지 않는 예와 버퍼를 사용하는 예를 보자. 중간에 화살표가 Stream이라고 생각하자.
버퍼를 사용하지 않고 "Hello world"를 입력하는 경우 위와 같은 일이 일어난다. "Hello world"라는 타이핑하는데 3초도 걸리지 않음에도 문자가 입력될 때마다 프로그램에 전송하고 있는 것을 볼 수 있다.
버퍼를 사용하여 "Hello world"를 입력할 때 버퍼를 두게 되면 버퍼에 입력하는 데이터가 쌓이게 된다. 그리고 버퍼가 다 찼거나 사용자가 임의적으로 데이터를 출력하면 버퍼에 있는 데이터가 Program으로 전송된다. 단순히 생각해봐도 버퍼의 크기가 1,024char(2,048byte)일 때 버퍼를 사용하지 않는 경우 1024번 데이터가 전송되는 것을 버퍼를 사용함으로써 1번으로 줄일 수 있다는 것을 알 수 있다.
위는 입력받는 과정을 나타냈지만 출력도 동일하다. 화살표 방향만 바꾸어 보면 감이 올 것이다.
Buffer는 반드시 사용해야 할까?
그렇다면 "버퍼를 사용하지 않고 입력을 받는 것은 잘못된 것인가?" 라고 묻는다면 그렇지는 않다. 다만 대부분의 경우 효율성이 떨어진다는 이야기다.
문자 1개만을 입력받는 상황을 예로 들어보자. 이러한 상황에서 버퍼는 도움이 될까? BufferedReader 클래스를 사용할 때 기본 Buffer 크기는 8192char(16,384byte)다. 문자 1개만을 입력받고자 할 때 굳이 8,192의 크기를 할당받는 것이 맞는 것일까? 출력할 때에는 BufferedWriter를 사용할 수 있다. 마찬가지로 기본 버퍼 크기가 8,192인데 문자 하나를 출력한다고 하면 버퍼가 무슨 소용이 있겠는가?
물론 실제 상황에서는 문자 하나, 숫자 하나를 입력받고 끝나는 케이스보다는 긴 문자열을 입력받는 경우가 커먼 케이스일 것이다. 다만 알고리즘처럼 어떤 상황에서든 최고의 성능을 내는 알고리즘은 없듯이 이 또한 필요한 상황이 있고 필요없는 상황이 있다. 필요에 따라서 적절하게 구현해서 사용해야 한다. 물론 그러기 위해선 제대로 된 이해가 동반되어야 할 것이다.
마치며
알고리즘 문제를 해결할 때 문제에서 요구하는 결과를 얻어도 실행시간이 초과되어 실패하는 경우가 종종 있다. 알고리즘이 비효율적이기 때문일 수도 있지만 입출력 속도에 문제가 있을 때도 있다. 알고리즘을 해결하는 것도 중요하지만 해결했을 때 다른 개발자 분들이 해결한 알고리즘에 비해서 속도가 현저히 떨어진다면 어느 부분에서 속도가 차이나는 것인지 또한 생각해보는 것이 좋겠다.