[java] 입출력

I/O는 Input/Output의 약자로, 입력/출력을 말한다.
입출력은 컴퓨터 내/외부 장치와 프로그램간의 데이터를 주고받는 것을 말한다.

스트림

데이터를 주고받으려면 두 대상을 연결하고 데이터를 전송할 수 있는 연결통로 같은것이 필요한데, 이를 스트림이라고 한다.
스트림은 단방향으로만 가능하므로 하나의 스트림으로 입,출력을 동시에 진행할 수 없다.
그러므로 입력 스트림, 출력 스트림이 따로 존재하고, 입력과 출력을 동시에 수행하려면 2개의 스트림이 필요하다.
스트림은 FIFO 구조로 되어있다.

자바에서 제공하는 스트림은 크게 2가지가 있다.

  • 바이트 단위로 데이터를 전송하는 바이트기반 스트림과 해당 스트림의 기능을 보완하기 위한 필터 스트림
  • 문자 단위로 데이터를 전송하는 문자기반 스트림과 해당 스트림의 기능을 보완하기 위한 필터 스트림

java i/o package

바이트 기반 스트림

바이트 단위로 데이터를 전송하는 스트림이다.
최상위 클래스는 InputStreamOutputStream이고,
입출력 대상에 따라 이를 상속한 FileInputStream, ByteArrayInputStream, PipedInputStream 등이 있다.

InputStream, OutputStream

  • InputStream
    입력 소스마다 입력 방식이 다르므로, abstract 메서드로 read()를 제공하고 자식쪽에서 이를 구현하도록 했다.
    구현 클래스에서는 입력 소스로부터 1byte를 읽어오는 내용을 구현하면 된다
    이 같은 추상화로 인해 입출력의 방식이 달라져도 일관된 방법으로 입출력이 가능하다.

read()는 입력 소스로부터 가져온 바이트를 반환하는데, 바이트를 받음에도 불구하고 byte가 아닌 int를 사용하고 있음을 볼 수 있다.
1byte의 데이터를 받으러면 0~255 까지의 데이터를 담아야하는데, java는 unsigned 형이 없어서 java의 byte로는 양수로 127까지 밖에 담지 못한다
그리고 만약 java에 unsigned 형이 있어서 255까지 받을 수 있다고 하더라도, EOF 값(-1)을 받지 못하기 때문에(unsigned 로는 음수를 표현할 수 없으니까) 어찌됐든 byte 형태는 사용할 수 없다

그렇다면 short를 사용하면 되지, 왜 int를 사용하는가?
이는 대부분의 연산의 기본 정수형이 int 타입이기 때문이다(JVM 기본 정수형도 int 타입이다)
그러므로 사실상 int를 사용하는 것이 가장 연산이 빠르고(추가적으로 형변환을 하지 않아도 되기 때문에?), 대부분의 정수 관련 연산은 int 타입을 사용하므로 int를 택한것으로 보인다

참고 : https://stackoverflow.com/questions/21062744/why-does-inputstream-read-return-an-int-and-not-a-short

read(byte[] b), read(byte[] b, int off, int len)
1 byte씩 데이터를 가져오면 너무 느리므로, byte 배열을 전달하고 이 배열에 데이터를 담아오도록 한다
하나씩 전달하는 것 보다 바구니에 담아서 전달하는 것이 더 빠른것을 생각하면 된다.
반환되는 int 값은 읽은 바이트의 개수이다.

  • read(byte[] b): 배열 b의 크기만큼 데이터를 읽어와서 b에 저장한다.
  • read(byte[] b, int off, int len) : len의 크기만큼 데이터를 읽어와서 배열 b의 off 위치부터 저장한다.
  • OutputStream
    abstract 메서드로 write(int b) 를 제공한다. 인자 b는 출력소스로 보낼 데이터이다
    (flush의 경우 버퍼가 있는 출력 스트림의 경우에만 의미가 있다.)

write(byte[] b), write(byte[] b, int off, int len)

  • write(byte[] b) : 배열 b에 저장된 모든 내용을 출력소스에 쓴다
  • write(byte[] b, int off, int len) : 배열 b에 저장된 내용을 off 위치부터 len개 만큼 출력소스에 쓴다.

FileInputStream, FileOutputStream

파일에 입출력을 하기 위한 스트림으로, 실제 프로그래밍에서 많이 사용된다.

아래는 file 복사의 간단한 예제이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void fileCopy(){
try(
FileInputStream fileInputStream = new FileInputStream("/Users/home/test.mov");
FileOutputStream fileOutputStream = new FileOutputStream("/Users/home/test_copy.mov")
){
byte[] temp = new byte[8192];

long start = System.currentTimeMillis();
while(fileInputStream.read(temp) > 0){
fileOutputStream.write(temp);
}
long end = System.currentTimeMillis();

System.out.println((end - start) / 1000.0);
} catch(IOException e){
e.printStackTrace();
}
}

ByteArrayInputStream, ByteArrayOutputStream

바이트 배열에 입출력 하기 위한 스트림이다.
입출력 대상이 메모리(배열은 java heap에 저장되므로) 이므로 close를 하지 않아줘도 된다는 특징이 있다.
자주 사용되진 않지만 read(byte[] b) 사용 시 발생할 수 있는 실수를 알아보기 위해 첨부하였음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void readFromByte() {
byte[] src = {1,2,3,4,5,6,7,8,9,10};
byte[] dst;

byte[] temp = new byte[4];

ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(src);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

try{
while(byteArrayInputStream.read(temp) > 0){
byteArrayOutputStream.write(temp);
}
} catch (IOException e){
e.printStackTrace();
}

dst = byteArrayOutputStream.toByteArray();

System.out.println(Arrays.toString(dst));
}

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 7, 8] 이 출력된다.
미자막 루프에서 9, 10 2byte만 읽었으므로 temp의 앞 2byte만 교체되는데,
write(temp)에서 temp의 모든 내용을 출력하도록 했기 때문이다.

1
2
3
4
5
6
7
8
int len;
try{
while((len = byteArrayInputStream.read(temp)) > 0){
byteArrayOutputStream.write(temp, 0, len);
}
} catch (IOException e){
e.printStackTrace();
}

이처럼 변경해줘야 한다.

바이트 기반 보조 스트림

바이트 기반 스트림의 기능을 보완(성능향상 및 기능추가)하기 위함.
자체적인 입출력 기능은 없다. 입출력 기능은 기반 스트림에 위임한다.
그러므로 생성자에서 항상 기반 스트림을 받는다.
부모 클래스는 FilterInputStream, FilterOutputStream이고,
자식으로는 BufferedInputStream/BufferedOutputStream, DataInputStream/DataOutputStream, SequenceInputStream, PrintStream 등이 있다.
보조 스트림을 close 하면 기반 스트림도 같이 close 된다.

FilterInputStream, FilterOutputStream

위 두 클래스는 생성자가 protected이므로 직접 생성이 불가능하고, 상속을 통해 오버라이딩 되어야 한다.

데코레이터 패턴을 기반으로 디자인 된 클래스 구조이기 때문에, 여러 Filter 클래스들을 겹쳐서 사용할 수 있다!

1
DataInputStream dis = new DataInputStream(new BufferedInputStream(System.in));

BufferedInputStream, BufferedOutputStream

생성할 때 지정한 버퍼의 크기만큼 입력소스로부터 읽고, 내부 버퍼에 저장해놓는다
예를 들어 2048의 크기로 BufferedInputStream 을 생성하고 read() 메서드를 호출하게 되면,
입력소스로부터 2048 byte 만큼 읽어와서 내부 배열에 저장하고, read() 메서드의 결과로는 2048 byte 중 1 byte만을 돌려주게 된다
read() 메서드로 내부 버퍼의 내용을 다 읽게 될 때 까지 입력소스에 추가적으로 접근하지 않게되고, 내부 버퍼의 내용을 다 읽으면 다시 입력소스에 접근해서 생성시 지정한 버퍼의 크기만큼 데이터를 읽어온다

BufferedInputStream 을 2048의 크기로 생성하고, read(byte[] b)에 100 크기의 배열을 전달하게 되면,
처음 read(byte[] b)를 호출하는 순간 입력소스로부터 2048 바이트를 읽어온 뒤 BufferedInputStream 에 있는 내부 배열에 저장해놓고, 거기서 100 byte만을 읽어서 반환해주게 된다
즉, read가 200번 호출될 떄 까지는 원본 소스에 직접 접근하는 일은 없을 것이다

BufferedOutputStream 또한 출력버퍼로 바로 전송하지 않고, 내부 버퍼에 데이터를 쌓아두었다가 버퍼의 내용이 가득차면 출력소스로 보낸다.
버퍼가 가득 차지 못해서 출력되지 못할 수도 있으니 항상 마지막에 flush()close()를 호출해서 버퍼를 비우도록 해야한다.

표준 입출력

표준 입출력이란 사용자가 별다른 입출
자바에서는 System 클래스의 in, out, err 를 통해 표준 입출력에 접근 가능하다
자바 어플리케이션의 실행과 동시에 자동적으로 생성되며, 내부적으로 BufferedInputStream, BufferedOutputStream 을 사용한다

아래는 표준 입출력을 이용해 문자를 입력받고 출력하는 소스이다

1
2
3
4
5
int input = 0;

while((input = System.in.read()) != -1) {
System.out.println((char)input);
}

표준 입출력의 끝을 나타내고 싶을 때는 Enter 키를 입력하거나, ^Z(맥에서는 ^D) 를 입력하면 된다
근데 여기서 조금 문제가 되는게, Enter 키를 입력할 경우 두 개의 특수문자 ₩r, ₩n 이 입력된것으로 간주된다는 점이다

₩r은 커서를 라인의 첫번째로 이동, ₩n은 커서를 다음 줄로 이동을 의미한다

이 때문에 Scanner의 nextInt() 같은 명령을 수행하게 되면 입력 버퍼에 ₩r₩n이 그대로 남아있게 되어, 이 후 문자열 입력 명령이 수행되어 버리는 문제점이 있다
그러므로 이를 방지하기 위해서 Scanner의 nextLine() 명령을 사용해주는 것이 좋다

nextLine()으로 받은 문자열에 Integer.parseInt 같은 명령을 사용하면 개행문자는 삭제되기 때문이다

문자 기반 스트림

java는 UTF16 인코딩을 사용하기 때문에 문자형을 2byte로 처리한다.
그러므로 바이트 기반 스트림으로 문자를 처리하기에는 어려움이 많다.
그래서 탄생한 것이 Reader, Writer이다. 이 또한 가장 최상위 클래스이다.
자식은 바이트 기반 스트림 자식에서 이름만 InputStream -> Reader, OutputStream -> Writer로 바꿔주면 된다.(FileReader/FileWriter, PipedReader/PipedWriter 등)

Reader, Writer

byte배열 대신 char배열을 사용한다는 것과, 추상메서드가 read(char[] cb, inf off, int len) 으로 달라졌다는게 차이점이다.
(프로그래밍 관점에서 이 추상메서드를 사용하는게 좀 더 바람직하기 때문이다)

FileReader, FileWriter

File로 부터 문자열을 입출력할 때 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void InputStream와Reader의문자처리() {
try(
FileInputStream fileInputStream = new FileInputStream("/Users/home/Desktop/대충 정리.md");
FileReader fileReader = new FileReader("/Users/home/Desktop/대충 정리.md")
){
int a;
while((a = fileInputStream.read()) != -1){
System.out.print((char)a);
}

System.out.println();

int b;
while((b = fileReader.read()) != -1){
System.out.print((char)b);
}

} catch(Exception e){
e.printStackTrace();
}
}

InputStream으로 읽었을 경우, 1byte만 읽어오므로 latin 인코딩이 아니면 문자가 깨진다.

문자 기반 보조 스트림

바이트 기반 보조 스트림과 용도는 동일하다.
FilterInputStream/FilterOutputStream 처럼 기반이 되는 클래스는 따로 없다.

BufferedReader, BufferedWriter

버퍼를 이용해 입출력의 성능을 높여준다.
BufferedReader는 라인 단위로 읽어올 수 있는 readLine() 메서드를 제공하고,
BufferedWriter는 개행을 출력할 수 있는 newLine() 메서드를 제공한다.

InputStreamReader, OutputStreamWriter

이름에서 알 수 있듯이 바이트 기반 스트림을 문자 기반 스트림으로 변환해주는 역할을 한다.
인코딩을 직접 지정할 수도 있다. 인코딩을 지정하지 않으면 OS에서 기본으로 사용하는 인코딩을 사용할 것이다.

콘솔 입력을 받을 때 주로 이용한다.(이 외에도 많겠지?)

1
2
3
4
5
InputStreamReader in = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(in);

// do something...
br.readLine();

참고 :