[Java] 입출력 스트림
우선 입출력 스트림의 특징부터 알아보자.
1. 입출력 스트림은 선입선출 구조이다. 따라서 순차적(단방향)으로만 접근이 가능하다.
사실 양방향으로 다 되는 게 있긴 하다. (임의 접근 파일 스트림)
그래도 스트림은 기본적으로 순차적이다.
2. 입출력 스트림은 객체로 구성되어 있다. (자바에서는 기초형을 제외하고는 전부 다 객체이다.)
3.출력스트림과 입력스트림은 서로 연결해서 파이프라인 방식으로 만들 수 있다.
4. 지연 가능성이 있다. 출력을 하는데 CPU 속도와 프린터의 속도가 다를 수도 있으니까.
입출력 스트림은 다음과 같은 구조를 가지고 있다.
입출력 스트림은 크게 바이트 스트림과 문자 스트림으로 나눌 수 있다.
바이트 스트림은 일반적인 이진 데이터 파일을 처리할 때 사용한다. 이진스트림이라고도 한다.
문자 스트림은 한글, 영어 등 언어로 되어 있는 파일을 처리할 때 유용하다. 동영상, 이미지 등을 처리할 때에는 적합하지 않다.
InputStream, OutputStream, Reader, Writer 4개의 클래스는 모두 추상클래스이다.
왜 추상클래스일까?
입력 스트림의 경우 입력이 키보드에서 될 수도 있고 마우스에서 입력될 수도 있고
출력 스트림의 경우 모니터에 출력할 수도 있고 프린터에 출력할 수도 있고 네트워크를 통해서 다른 곳으로 출력할 수도 있고 다 다르다.
그렇기 때문에 입출력 메소드인 read(), write() 메소드를 구현할 수가 없다.
따라서 4개의 추상클래스를 만들어 놓고 사용할 때에는 구현된 자식 클래스를 이용한다.
입출력 스트림의 사용 과정은 다음과 같다.
1. 스트림을 열고
2. 스트림으로 처리하고
3. 스트림을 닫는다.
마지막에는 close() 메소드로 반드시 스트림을 닫아줘야 한다.
스트림을 여는 open()이라는 메소드는 없다. 스트림 객체를 생성하는 것 자체가 스트림을 여는 것이다.
먼저 바이트 스트림부터 살펴보자.
살펴보기 전에 각 클래스의 특징부터 설명하자면
바이트 스트림은 이미지나 동영상을 처리할 때 적합하다. 바이트 스트림은 모두 InputStream, OutputStream의 자식클래스들이다.
InputStream에서는 FileInputStream, DataInputStream, BufferedInputStream을 자주 사용한다.
파일을 다룰 때에는 FileInputStream을, 데이터 자체를 다루고 싶을 때에는 DataInputStream을 사용하면 된다.
데이터 자체를 다룬다는 말은 메모리에 저장되어 있는 그 데이터를 그대로 다루겠다는 것이다.
만약에 int 타입의 1이라는 데이터가 있다면 00000000000000000000000000000001로 다루는 것을 말한다.
BufferedStream은 말 그대로 버퍼를 이용하는 것이다. 속도가 차이나기 때문에 버퍼에 잠시 보관하는 것이다.
이 중에서도 우리는 주로 FileInputStream과 BufferedInputStream을 자주 사용한다.
OutputStream에서는 FileOutputStream, BufferedOutputStream, PrintStream을 자주 쓰는데 PrintStream은 다른 것들과 특징이 조금 달라서 기억해 둘 필요가 있다. 그 특징은 뒤에서 배울 것이다.
이제 정말 바이트 스트림을 알아보자
InputStream과 OutputStream은 모든 자식 바이트 스트림에서 공통으로 사용하는 메소드(read(), write() 등)를 포함하는 바이트 스트림의 최상위 클래스이다.
InputStream 클래스에는 read(), OutputStream 클래스에는 write()라는 추상메소드가 있다. 앞에서 말했지만 이 두 메소드는 구현을 할 수가 없다.
오라클 홈페이지에서 Java API를 보면 InputStream은 추상클래스이고 Closeable의 구현 클래스인 것을 알 수 있다.
Closeable이라는 것은 자원을 close해야 한다는 것이다. Closeable의 자식클래스로는 AutoCloseable이 있다.
OutputStream도 마찬가지로 추상클래스이고 Closeable의 구현 클래스인 것을 볼 수 있다. 한 가지 다른 것은 Flushable의 구현 클래스라는 것이다. 뒤에서 배우겠지만 출력 스트림은 flush를 해줘야 한다.
InputStream의 주요 메소드
int available() | 읽을 수 있는 바이트의 개수 반환. |
void close() | 입력 스트림을 닫는다. |
abstract int read() | 한 바이트를 읽고 그 읽은 것을 int로 리턴한다. |
int read(byte[] b) | 1바이트씩 읽어 배열에 집어넣고 몇 바이트 읽었는지 반환 |
데이터 읽을 게 있는지 없는지 체크하려면 available() 메소드를 사용한다. 헷갈릴 수도 있는데 이 메소드는 boolean 타입이 아니다. 기억해 두자.
InputStream객체.available() 해보고 0보다 클 경우에만 읽으면 된다.
read() 메소드가 특이한 것은 1byte를 읽어서 int 타입으로 반환한다는 것이다. int는 4바이트이다. 즉, 앞쪽 3바이트는 사용을 안 한다는 것이다. read() 메소드는 1바이트를 읽어서 읽은 내용을 반환하는데 int 타입으로 반환한다는 것 잊지 말자.
read() 메소드는 만약 더 이상 읽을 것이 없으면 -1을 반환한다. -1을 반환하기 위해서 read() 메소드의 반환 타입이 int 타입인 것이다.
OutputStream의 주요 메소드
void close() | 출력 스트림을 닫는다. |
void flush() | 버퍼를 비운다. |
abstract void write(int b) | b를 바이트로 변환해서 1바이트를 쓴다. |
void write(byte[] b) | 바이트 배열 b를 쓴다. |
flush()라는 것은 InputStream에는 없고 출력 스트림에만 있는 메소드이다. flush는 뭘까?
대부분의 운영체제나 JVM은 read(입력), write(출력)를 효율적으로 하기 위해서 버퍼를 사용한다.
컴퓨터가 처리하는 속도와 출력장치가 출력하는 속도를 비교하면 출력장치가 출력하는 속도가 훨씬 느리다.
예를 들어 출력 장치가 모니터라고 하자.
컴퓨터의 처리와 모니터가 출력하는 것은 속도 차이가 나기 때문에 출력을 효율적으로 하기 위해서 일단 버퍼에 써서 모아놨다가 나중에 한꺼번에 출력(write)을 한다.
System.in, System.out, System.err은 각각 표준 입력, 표준 출력, 표준 오류 장치이다. 표준 장치들은 시스템에서 바로 쓰기 때문에 효율적으로 사용하는 것이 좋으므로 버퍼를 사용한다. 따라서 내가 System.out.write(b) 하면 모니터에 바로 b가 나타나는 것이 아니라 버퍼에 모아둔다.
그 이후에 계속 써도 일단 버퍼에 쓴다. 그러고 나서 버퍼가 꽉 차면 그제서야 모니터에 출력한다.
그런데 나는 아직 버퍼가 꽉 차지는 않았지만 지금 바로 실제로 모니터에 출력을 하고 싶다면 그 때 사용하는 것이 flush 메소드이다. 그러면 버퍼에 있는 내용을 비우고 모니터에 바로 출력한다.
그런데 close() 메소드에는 flush()의 기능이 있어서 close()를 하면 자동으로 flush()가 된다. 더 이상 이 스트림 객체를 안 쓴다는 것이므로 끝내기 전에 버퍼에 있던 내용들은 출력하고 끝내야 하기 때문이다.
write() 메소드는 read() 메소드와 마찬가지로 1byte를 쓰지만 매개변수는 int 타입이다. 인자로 int 타입을 받아서 byte 타입으로 변환해서 1바이트를 쓴다.
import java.io.IOException;
public class IOStreamDemo {
public static void main(String[] args) throws IOException {
int b, len = 0;
int ba[] = new int[100];
System.out.println("---입력 스트림---");
while ((b = System.in.read()) != '\n') {
System.out.printf("%c(%d)", (char)b, b);
ba[len++] = b;
}
System.out.println("\n\n---출력 스트림---");
for(int i = 0; i < len; i++)
System.out.write(ba[i]);
System.out.flush();
}
}
public static void main(String[] args) 뒤에 throws IOException이라고 예외 처리를 한 것을 볼 수 있는데
입출력 장치를 이용할 때에는 거의 IOException이 일어나기 때문에 반드시 예외 처리를 해 줘야 한다.
예외처리를 해주지 않으면 다음과 같이 컴파일 에러가 난다. 따라서 예외 처리는 선택이 아니라 필수이다.
이 글은 예외처리 글이 아니라서 예외처리를 자세히 설명하면 너무 산으로 갈 것 같아서 예외처리는 따로 설명을 하겠다.
아무튼 입출력을 할 때에는 무조건 IOException에 대한 예외 처리를 해줘야 한다는 것이다.
이 코드에서는 InputStream, OutputStream 객체를 따로 만들지는 않았고 표준 입력 장치인 System.in과 표준 출력 장치인 System.out을 사용했다. 키보드로부터 입력을 받고 모니터에 출력을 하겠다는 것이다.
while문을 돌면서 키보드로부터 1바이트를 읽어서 ba 배열에 저장한다.
여기에서 주목해야 할 것은 1바이트를 읽어서 b에 저장하는데 b는 int 타입이라는 것이다.
잊지 말자 read() 메소드는 1바이트를 읽어서 int를 리턴한다.
그렇게 읽은 것은 ba 배열에 저장하고 나중에 write 메소드로 모니터에 출력한다.
마지막에 System.out.flush()를 해준다. flush()를 해주지 않으면 모니터에 출력이 안 될 수도 있다.
실행 결과는 다음과 같다.