반응형

개요

이 게시글은 자바 IO, NIO에 대해 정리하려고 했으나, 공부를 하다보니 InputStream, OutputStream 및 Stream과 같은 용어가 많이 등장했다. 곧 이에 대한 개념이 잡히지 않았되지 않다는 것을 알았고, 필요성을 느껴 이에 대해 정리해보았다. InputStream과 OutputStream에 대해 이해해보기 전 Stream의 개념에 대해 알아보자.

 

Stream이 뭐에요?

스트림이란 데이터, 패킷, 비트 등의 일련의 연속성을 갖는 흐름을 의미한다.

 

스트림과 비슷한 용어인 '스트리밍'을 생각해보자. 유투브에서 제공하는 영상들은 '동영상 스트리밍' 형태로 보게 되는데, 요즘은 영상 화질도 좋아져서 동영상의 용량이 어마어마하다는 것은 누구나 알것이다.

1.2GB 크기의 유투브 영상 추상화

 

하지만 이렇게 큰 용량을 가진 영상을 우리는 오랜 기다림 없이 바로 볼 수 있다. 그 이유가 뭘까? 서버에서 영상파일을 작은 단위로 쪼개어 클라이언트에게 전달하기 때문이다. 이처럼 데이터를 잘라 연속적인 데이터의 흐름 형태로 전달하는 것을 스트림이라고 한다.

 

네트워크가 불안정할 경우 영상이 끊긴 경험이 있을 것이다. 이 모두 영상 데이터에 대해 스트림 형태로 전달받다가 중간에 네트워크 문제가 생겨 다음 데이터를 전달을 받지 못해 발생하는 현상인 것이다. 만약 스트림이 아닌 영상 데이터를 한번에 전달받는 방식이었다면 1.2GB 크기의 유투브 영상을 모두 전달받기 전까지 영상을 재생할 수 없을 것이다.

 

InputStream이 뭐에요?

java.io 패키지에서 제공하는 이 InputStream는 데이터를 입력받기위한 스트림을 제공하는 추상 클래스이다.  입력받는다는 것은 데이터를 읽어오는 것이다. 

서브 클래스로 ByteArrayInputStream, FileInputStream, AudioInputStream 클래스 등 다양한 InputStream 클래스가 있는 것으로 보아 바이트 배열, 파일, 오디오 파일 등 다양한 형태의 데이터에 대한 스트림을 제공한다는 것을 알 수 있다. 한번 사용해보자.

 

Input, Output이 헷갈려요... 😫

Java 입장에서 생각해보자. Java 입장에서 Input은 들어오는 것이니 데이터를 읽어오는 것, Output은 나가는 것이니 데이터를 쓰는 것으로 이해하면 쉽다.

 

 

InputStream을 통해 텍스트 파일 읽어보기

// C:/testFile/txtFile.txt
aaaaaaaa
bbbbbbb
cccccccc
ddddddddd
eeeeeeee
ffffffff

 

위 텍스트 파일을 만든 후 InputStream으로 읽어오는 것을 테스트해보았다. 참고로 try/resources 를 사용해 close 메서드를 자동호출하도록 하였다. println 메서드를 통해 결과를 출력하면 '97'이 출력된다.

@Test
void fileInputStreamTest(){
    try(InputStream inputStream = new FileInputStream("C:/testFile/txtFile.txt")){
        System.out.println(inputStream.read()); // 97 출력
    }catch (IOException e){
        e.printStackTrace();
    }
}

 

 

 

아까 말했듯이 Stream은 잘게 잘린 데이터의 연속적인 흐름이다. FileInputStream을 사용하면 파일 데이터를 byte 타입의 쪼개진 데이터들로 받을 수 있고, read() 메서드는 스트림을 통해 1byte씩 읽는 메서드이기 때문에 97이라는 값이 출력된 것이다. 참고로 97은 'a' 문자의 아스키코드이다.

 

read()
Reads the next byte of data from the input stream.
= 입력 스트림에서 데이터의 다음 바이트를 읽습니다.

 

 

그럼 read() 메서드를 계속 호출하면 어떻게 될까? 🙄

 

아래와 같이 더 이상 읽을 데이터가 없는 시점부터 -1이 출력된다. 스트림이 비어있는 것이다.

@Test
void fileInputStreamTest(){
    try(InputStream inputStream = new FileInputStream("C:/testFile/txtFile.txt")){
        
        System.out.println(inputStream.read());
        System.out.println(inputStream.read());
        System.out.println(inputStream.read());
        ...
        ...
        System.out.println(inputStream.read()); //-1 출력
        System.out.println(inputStream.read()); //-1 출력
        System.out.println(inputStream.read()); //-1 출력
       
    }catch (IOException e){
        e.printStackTrace();
    }
}

 

 

이러한 특성때문에 일반적으로 스트림을 통해 데이터를 read 할때에는 while과 같은 반복문을 사용해 -1을 체크하는 구문이 들어가게 된다.

void fileInputStreamTest(){
    try(InputStream inputStream = new FileInputStream("C:/testFile/txtFile.txt")){

        int i = 0;
        while((i = inputStream.read()) != -1){
            System.out.write(i);
        }
    }catch (IOException e){
        e.printStackTrace();
    }
}

 

출력결과

 


잠깐, System.out.print와 System.out.write 의 차이

 

System.out.print는 다양한 타입의 데이터를 '텍스트 형식'으로 출력하도록 설계된 메서드이다. 예를들어 System.out.print(97)을 사용하면 숫자 97이 그대로 출력된다.

 

이에반해 System.out.write는 바이트 데이터를 출력하기 위해 사용하는 메서드이다. 메서드로 전달된 정수 값을 바이트로 변환하여 출력하기때문에 아스키 코드에 해당하는 문자를 출력한다. 예를들어 System.out.write(97)을 사용하면 97은 'a'로 변환되어 출력된다.

 

print는 사용자 친화적인 텍스트 출력에, write는 바이트 단위의 데이터 처리와 출력에 사용된다.

 


read() 메서드는 1byte씩 스트림으로 전달한다? 너무 느리지않나요? 🤔

1 바이트씩 처리하는 InputStream.read()

 

 

@Test
void fileInputStreamTest(){
    long start = System.currentTimeMillis();
    try(InputStream inputStream = new FileInputStream("C:/testFile/txtFile.txt")){
        int i = 0;
        while((i = inputStream.read()) != -1){
            //System.out.write(i);
        }
    }catch (IOException e){
        e.printStackTrace();
    }
    long end = System.currentTimeMillis();
    System.out.println("걸린시간 :"+(end - start) +"ms"); // 2589ms
}

 

실제로 10만줄의 텍스트파일을 만들고, 불필요한 로직(System.out.write)을 제거한 후 실행해보니 온전히 InputStream을 통해 데이터를 읽는데만 약 2.5초(2589ms)가 걸렸다. Stream에서 1byte 씩 빼다보니 속도가 느린 것이다. 만약 Stream을 통해 데이터를 읽어올 때 1byte보다 큰 크기의 byte 데이터들을 읽어들인다면 속도가 개선되지 않을까?

 

 

b 바이트 만큼의 버퍼 단위로 처리하는 InputStream.read(byte[] b)

 

 
read(byte[] b)
Reads some number of bytes from the input stream and stores them into the buffer array b.
= 입력 스트림에서 일부 바이트를 읽고 이를 버퍼 배열에 저장합니다.

 

 

찾아보니 버퍼를 활용하는 read(byte[] b) 메서드가 있었다. 256byte의 버퍼 사이즈만큼 스트림에서 데이터를 읽도록 하니 속도가 12ms로 개선되었다. 아무래도 1byte씩 읽는 방식보다는 버퍼를 활용하는 방식을 사용하는 것이 좋아보인다. 

@Test
void fileInputStreamTest2(){
    long start = System.currentTimeMillis();
    try(InputStream inputStream = new FileInputStream("C:/testFile/txtFile.txt")){
        int i = 0;
        byte[] buf = new byte[256];
        while(inputStream.read(buf) != -1){
            //System.out.write(buf);
        }
    }catch (IOException e){
        e.printStackTrace();
    }
    long end = System.currentTimeMillis();
    System.out.println("걸린시간 :"+(end - start) +"ms"); // 12ms
}

 

 

출력했더니 데이터가 누락됐어요! 😨

이제 작성한 코드에 있던 주석을 해제하고 읽어온 데이터를 콘솔에 출력해보았다. 그런데 ff 문자 두개가 누락된것 같다. 예외도 발생하지 않았다. 어째서 이런일이 발생하는 것일까? 

ff 문자가 누락된 모습

 

 

알고보니 데이터가 누락된게 아니었다. 이는 버퍼를 사용하는 메커니즘을 이해하지 못한 필자의 착각이었다. read(byte[] b) 메서드를 호출하면 직전 buffer에 스트림에서 읽은 데이터가 덮어 씌워진다. 필자는 buffer가 자동으로 비워질줄 알았는데 덮어 씌워지는 것이었다. 누락보다는 추가됐다고 할 수 있겠다.

 

Buffer는 있는 그대로 덮어 씌워버려요~

 

 

이때문에 InputStream.read(byte[] b) 메서드를 사용하려면 앞선 예제처럼 사용하면 안된다. buffer에서 read한 사이즈만큼 처리하는 로직을 따로 구현해야 한다.

앞서 read() 메서드는 스트림을 통해 읽은 바이트를 그대로 리턴한다. 이에반해 read(byte[] b) 메서드는 읽은 바이트의 수를 리턴한다. 이를 활용하여 i와 같은 임시 변수를 만든 후 읽은 바이트 수를 저장하고, 이 길이만큼 버퍼에서 읽도록 구현하자.

@Test
void fileInputStreamTest2(){
    long start = System.currentTimeMillis();
    try(InputStream inputStream = new FileInputStream("C:/testFile/txtFile.txt")){
        int i;
        byte[] buf = new byte[256];
        while((i = inputStream.read(buf)) != -1){
            for(int len = 0; len<i; len++){
                System.out.write(buf[len]);
            }
        }
    }catch (IOException e){
        e.printStackTrace();
    }
    long end = System.currentTimeMillis();
    System.out.println("걸린시간 :"+(end - start) +"ms");
}

 


OutputStream은 뭐야?

OutputStream은 데이터를 출력하기 위한 스트림을 제공하는 추상 클래스이다. 자바 입장에서 데이터를 나가게 하므로 외부 파일에 데이터를 쓰는 것이라고 생각하면 쉽다.

 

이것도 마찬가지로 write(byte b) 메서드를 통해 1byte씩 출력 스트림을 통해 데이터를 전달할 수 있다.

@Test
void fileOutputStreamTest(){

    byte[] bytes = {97,97,97,97,97};

    try(OutputStream outputStream = new FileOutputStream("C:/testFile/txtFile.txt")){
        for(byte b : bytes){
        	outputStream.write(b);
        }
        outputStream.flush();
    }catch (Exception e){
        e.printStackTrace();
    }

}

 

 

txtFile.txt에 97을 가진 byte 배열을 쓰기(쓰기작업) 위해 write() 메서드를 호출한다. 그럼 txtFile.txt에는 97에 해당하는 아스키 코드 값인 a가 5개 작성된다.

OutputStream.write() 를 통해 aaaaa를 넣어보아요

 

 


1Byte씩? 이것도 느리지않나요? 🤔

속도 테스트를 위해 bigFile이라는 많은 데이터를 가진 텍스트파일을 만들고 이 byte 값을 OutputStream을 통해 전달해보았다. 텍스트 내 데이터를 복사하는 것이다. 첫번째는 InputStream을 통해 읽어들인 byte 데이터를 1byte씩 write하고, 두번째는 버퍼 사이즈만큼 write하도록 하였다. 결론부터 말하면 느렸고, 버퍼를 활용하는 것이 훨씬 빠르다.

 

1Byte씩 write

2654ms 초가 걸렸다. 1byte씩 Stream을 통해 데이터를 전달해서 그런지 속도가 느리다.

@Test
void fileOutputStreamTest2(){
    long start = System.currentTimeMillis();
    try(
            OutputStream outputStream = new FileOutputStream("C:/testFile/txtFile.txt");
            InputStream inputStream = new FileInputStream("C:/testFile/bigFile.txt"))
    {
        byte[] buf = new byte[256];
        int i;
        while((i = inputStream.read(buf)) != -1){
            for(int len = 0; len<i; len++){
                outputStream.write(buf[len]); // 1byte씩 write
            }
        }
    }catch (Exception e){
        e.printStackTrace();
    }
    long end = System.currentTimeMillis();
    System.out.println("걸린시간 :"+(end - start) +"ms");
}

 

 

Buffer write

25ms가 걸렸다. InputStream 때와 마찬가지로 Buffer를 사용하니 스트림으로의 쓰기 작업 횟수가 줄어들어 속도가 굉장히 빨라진 것을 확인할 수 있었다.

@Test
void fileOutputStreamTest3(){
    long start = System.currentTimeMillis();
    try(
            OutputStream outputStream = new FileOutputStream("C:/testFile/txtFile.txt");
            InputStream inputStream = new FileInputStream("C:/testFile/bigFile.txt"))
    {
        byte[] buf = new byte[256];
        int i;
        while((i = inputStream.read(buf)) != -1){
            outputStream.write(buf,0,i); // 버퍼의 0부터 읽은 사이즈까지 한번에 write
        }
        outputStream.flush();
    }catch (Exception e){
        e.printStackTrace();
    }
    long end = System.currentTimeMillis();
    System.out.println("걸린시간 :"+(end - start) +"ms");
}

 

 

 

InputStream, OutputStream을 사용할때 무조건적으로 예제에서 제공하는 코드를 사용하다가는 큰코다칠 수 있다. 버퍼를 적극 활용하고 입출력에 대한 실행 시간을 고려해보는 습관을 갖자.

반응형

+ Recent posts