Tomcat 구현 미션을 진행하면서 IO Stream이라는 개념을 다루게 되었습니다. 이번 아티클에서는 Stream이란 무엇인지, 그리고 IO Stream은 무엇이며 어떻게 사용될 수 있는지까지 알아보겠습니다.
Stream이란?
우선 Stream이란 무엇인지부터 알아보겠습니다. Stream이란, '흐르는 시냇물' 이라는 사전적 의미를 가집니다. 그리고 이는 컴퓨터 공학 분야에서 자주 사용되는데, 주로 데이터, 패킷, 비트 등의 일련의 연속성을 갖는 흐름을 의미한다고 합니다.
보다 쉽게 이해하기 위해 실생활의 예시를 한번 들어보겠습니다. Streaming(스트리밍)이라는 키워드는 많이 들어보셨을 것 같습니다. 우리가 영상을 보거나 음악을 들을 때 자주 접하던 개념입니다. 영상을 보거나 음악을 들을 때 데이터는 과연 어떻게 올까요? 특히 영상의 경우에는 크게는 수 기가바이트에 달하기도 하는데, 적은 용량의 메모리를 가진 휴대폰으로도 시청하는데 무리가 없습니다.
이는 비디오를 시청하거나 음악을 들을 때 스트림이라는 개념이 사용되기 때문입니다. 데이터를 한 번에 보내는게 아니라 연속된 흐름으로 계속 흘려보냅니다. 이 때문에 스트림을 데이터의 흐름이라고 부르기도 하는 것입니다.
Java IO Stream
그리고 Java에서는 이러한 Stream의 개념을 통해 I/O(Input, Output)을 다룹니다. Java 프로그램이 데이터를 받는 경우에는 입력 스트림(Input Stream)을 사용하고 데이터를 전송하는 경우에는 출력 스트림(Output Stream)을 사용합니다.
Java를 공부하면 흔히 사용하는 메소드인 System.out.println 역시 내부적으로는 Stream으로 구현됩니다. println 메소드가 PrintStream이라는 클래스에 구현이 되어 있는데, PrintStream 클래스는 OutputStream의 서브클래스입니다.
public class PrintStream extends FilterOutputStream
implements Appendable, Closeable
{
...
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
...
}
I/O Stream의 종류
Java의 I/O Stream은 두 가지로 나뉠 수 있습니다.
- 바이트 단위 입출력 스트림(InputStream, OutputStream)
- 문자 단위 입출력 스트림(Reader, Writer)
전송하는, 혹은 전송받는 데이터가 바이트냐 문자냐에 따라 적절히 선택해 사용하면 됩니다. 바이트 단위로 입출력 스트림을 사용하고 싶을 때에는 InputStream 혹은 OutputStream을 사용하면 되고, 문자 단위로 입출력 스트림을 사용하고 싶다면 Reader 혹은 Writer를 사용하면 됩니다.
설명으로는 추상적이므로 간단한 예시를 통해 기본 개념을 이해해보겠습니다.
바이트 단위 입출력 스트림 사용 예시
우선 바이트 단위 입출력 스트림 사용 예시를 살펴보겠습니다. 앞서 말씀드렸다시피 InputStream이나 OutputStream을 사용하는 경우입니다.
InputStream
URL resource = getClass().getClassLoader().getResource("hello.txt");
InputStream inputStream = new FileInputStream(resource.getPath());
byte[] buffer = new byte[1024];
inputStream.read(buffer);
String content = new String(buffer).trim();
inputStream.close();
우선 InputStream 사용 예시입니다. hello.txt 라는 파일의 내용을 읽고자 한다면, 위와 같이 InputStream의 서브클래스 중 하나인 FileInputStream을 사용해 읽을 수 있습니다.
다만 주의깊게 보아야 할 점은 buffer라는 바이트 배열 단위로 데이터를 읽는 점입니다. 앞서 설명드렸다시피 InputStream은 바이트 단위 입출력 스트림이므로 바이트 단위로 데이터를 읽을 수 밖에 없고, 따라서 buffer라는 byte array를 사용해야 합니다.
OutputStream
OutputStream outputStream = new FileOutputStream("target.txt");
String content = "hello world!";
outputStream.write(content.getBytes());
outputStream.close();
OutputStream을 통해 파일에 데이터를 추가하고 싶은 경우는 위와 같이 작성할 수 있습니다. 마찬가지로 데이터를 바이트 형식으로 전송합니다. 위 코드를 실행시키면 target.txt 라는 파일에 "hello world!"가 기록됩니다.
문자 단위 입출력 스트림 사용 예시
이번에는 문자 단위로 입출력 스트림을 사용하는 예시를 살펴보겠습니다. Reader 혹은 Writer를 사용하는 경우입니다.
Reader
URL resource = getClass().getClassLoader().getResource("hello.txt");
Reader reader = new FileReader(resource.getPath());
char[] buffer = new char[1024];
reader.read(buffer);
reader.close();
InputStream을 사용했을 때와의 사용법의 차이는 그렇게 크지 않습니다. 다만 다른 부분이 하나 있는데, byte 배열로 스트림에서 읽는게 아닌 char 배열로 읽는다는 것입니다. Reader를 사용하는 경우 별다른 파싱 작업을 거치지 않아도 문자가 그대로 buffer에 기록됩니다.
Writer
Writer writer = new FileWriter("target.txt");
String content = "hello world!";
writer.write(content);
writer.close();
Writer를 사용하는 경우도 OutputStream 대비 사용법의 차이는 그렇게 크지 않습니다. 다만 바이트 단위로 write하지 않고 문자열을 그대로 write할 수 있다는 점에서 차이점을 가집니다.
기반 스트림, 보조 스트림
앞서서 스트림이란 무엇인지, 그리고 Java에서는 어떤 종류의 스트림을 통해 입출력을 다루는지까지 살펴보았습니다. 사실 스트림은 또 기반 스트림과 보조 스트림으로 나뉠 수 있습니다.
기반 스트림이란 실제로 Java 프로그램 외부와 통신하면서 데이터를 읽거나 받아오는 스트림을 의미하고, 보조 스트림이란 그런 기반 스트림을 보조하면서 성능 향상 등 추가적인 기능을 제공합니다. 중요한 점은, 보조 스트림 혼자서는 입출력을 수행할 수 없다는 것입니다.
보조 스트림이란 기반 스트림을 감싸는 데코레이터, 혹은 Wrapper 정도로 생각하시면 될 것 같습니다. 그리고 이런 보조 스트림의 최상위 클래스는 FilterInputStream, FilterOutputStream입니다.
보조 스트림의 대표적인 예시, BufferedInputStream
보다 쉽게 이해하기 위해서 보조 스트림의 일종인 BufferedInputStream을 한 번 사용해보겠습니다.
URL resource = getClass().getClassLoader().getResource("hello.txt");
InputStream inputStream = new BufferedInputStream(new FileInputStream(resource.getPath()));
byte[] buffer = new byte[1024];
inputStream.read(buffer);
inputStream.close();
사용법의 측면에서 앞선 예시와 크게 다른 점은 없지만 BufferedInputStream이라는 보조 스트림을 통해 FileInputStream이라는 기반 스트림을 감싸고 있다는 점이 다릅니다.
이렇게 BufferedInputStream으로 기반 스트림인 FileInputStream을 감싸서 사용하면 어떤 이점이 있을까요? BufferedInputStream은 내부적으로 아래와 같이 구성됩니다.
위와 같이 내부적으로 버퍼를 둠으로써 성능을 향상시킵니다. 매번 데이터를 필요로 할 때마다 입출력 스트림에서 동기적으로 기다릴 필요가 없으며, 버퍼에서 읽어오면 됩니다. 이렇듯 보조 스트림을 사용하면 다양한 이점을 누릴 수 있습니다.
참고 자료
정보통신기술용어해설 - Stream 스트림
스트리밍이란: 스트림(Stream), 버퍼(Buffer) 원리
Streams in Computer Engineering
Java에서 스트림을 사용한 입출력(InputStream, OutputStream)
'Java' 카테고리의 다른 글
리플렉션이란? (+ @GetMapping 만들어보기) (0) | 2023.09.17 |
---|---|
Java: 커스텀 예외 사용에 대한 생각 (0) | 2023.03.16 |
Java: Throwable 소개 & API (0) | 2023.03.07 |
Java: enum의 구현방식 알아보기 (8) | 2023.03.06 |
Java: enum 소개 및 API 파헤쳐 보기 (3) | 2023.03.05 |