13주차 과제 : I/O

Lee·2021년 2월 19일
1
post-thumbnail

I/O

자바에서 제공하는 Input과 Output에 대해 공부해보자 📖

  • 스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O
  • InputStream과 OutputStream
  • Byte와 Character 스트림
  • 표준 스트림 (System.in, System.out, System.err)
  • 파일 읽고 쓰기

많은 글 주의..
이번주 스터디 내용을 학습하다보니 코드로 보면서 설명한다기 보단 개념 설명이 더 많습니다..

입출력이란? (I/O) 📌

I/O란 Input과 Output의 약자로 입력과 출력, 간단히 줄여서 입출력이라고 한다. 입출력은 컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터를 주고받는 것을 의미한다. 예를 들면 키보드로부터 데이터를 입력받는다든가 System.out.println()을 이용해서 화면에 출력한다던가 하는 것이 가장 기본적인 입출력의 예이다.

스트림(Stream)

자바에서 입출력을 수행하려면, 즉 어느 한쪽에서 다른 쪽으로 (A -> B) 데이터를 전달하려면, 두 대상을 연결하고 데이터를 전송할 수 있는 무언가가 필요한데 이것을 스트림(Stream)이라고 부른다. (다리같은 존재)

스트림이란? 데이터를 운반하는데 사용되는 연결통로이다.

스트림은 연속적은 데이터의 흐름을 물에 비유해서 붙여진 이름인데 그 이유는 물이 한쪽 방향으로만 흐르는 것과 같이 스트림은 단방향통신만 가능하기 때문에 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없다. 그래서 입출력을 동시에 수행하려면 입력을 위한 입력스트림과 출력을 위한 출력스트림 모두 2개의 스트림이 필요하다.

바이트기반 스트림 - InputStream, OutputStream

스트림은 먼저 보낸 데이터를 먼저 받게 되어 있으며 중간에 건너뜀 없이 연속적으로 데이터를 주고받는다. 또한 스트림은 바이트단위로 데이터를 전송하며 입출력 대상에 따라 다음과 같은 입출력스트림이 있다.

입력스트림출력스트림입출력 대상의 종류
FIleInputStreamFileOutputStream파일
ByteArrayInputStreamByteArrayOutputStream메모리(byte배열)
PipedInputStreamPipedOutputStream프로세스(프로세스간의 통신)
AudioInputStreamAudioOutputStream오디오장치

위 입출력들은 모두 InputStream, OutputStream 클래스의 하위클래스이며, 각각 읽고 쓰는데 필요한 추상 메소드를 목적에 맞게 구현해놓은 구현체 클래스이다. 위 입출력 클래스 말고도 몇가지가 더 있는데 자세한건 공식문서를 확인해보면 가능하다.

Java InputStream Document

자바에서 기본적으로 제공해주는 I/O 스트림의 최상위 클래스를 보면 입출력 관한 메소드들을 기본적으로 제공해준다. 그 중 말 그대로 읽기/쓰기와 관련된 메소드들을 정리해보자면

InputStreamOutputStream
abstract int read()abstract void write(int b)
int read(byte[] b)void write(byte[] b)
int read(byte[] b, int off, int len)void write(byte[] b, int off, int len)

공식 문서에 나와 있는대로 오로지 읽고/쓰기와 관련된 메소드의 사용법만 잘 알고 있다면 간단하게 데이터를 읽고 쓰는 것은 어렵지 않다.

보조스트림

스트림의 기능을 보완하기 위해 보조스트림이 제공한다. 보조스트림은 실제 데이터를 주고받는 스트림이 아니기 때문에 데이터를 입출력할 수 있는 기능은 없지만, 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다. 그래서 보조스트림만으로는 입출력을 처리할 수 없고, 스트림을 먼저 생성한 다음에 이를 이용해서 보조스트림을 생성해야한다.

에를 들어 test.txt라는 파일을 읽기 위해 FileInputStream을 사용할 때, 입력 성능을 향상시키기 위해 버퍼를 사용하는 보조스트림인 BufferedInputStream을 사용하는 코드이다.

// 먼저 기반스트림을 생성
FileInputStream fileInputStream = new FileInputStream("test.txt");
// 기반스트림을 이용한 보조스트림 생성
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
// 보조스트림인 BufferedInputStream으로부터 데이터를 읽는다.
bufferedInputStream.read();

코드 상으로는 보조스트림인 BufferedInputStream이 입력기능을 수행하는 것처럼 보이지만, 실제 입력기능은 BufferedInputStream과 연결된 FileInputStream이 수행하고, 보조스트림인 BufferedInputStream은 버퍼만 제공한다.

버퍼를 사용한 입출력과 사용하지 않은 입출력간의 성능차이는 상당하기 때문에 대부분의 경우에 버퍼를 이용한 보조스트림을 사용한다.

BufferedInputStream이 외에도 스트림의 기능을 향상시킬 수 있는 보조스트림이 더 있는데, 그냥 뭐가 있고 이런게 있다라는 식으로만 알아두면 좋을 것 같다.

입력출력설명
FilterInputStreamFilterOutputStream필터를 이용한 입출력 처리
BufferedInputStreamBufferedOutputStream버퍼를 이용한 입출력 성능향상
DataInputStreamDataOutputStreamint, float와 같은 기본형 단위(primitive type)로 데이터를 처리하는 기능
SequenceInputStreamSequenceOutputStream두 개의 스트림을 하나로 연결
LineNumberInputStream없음읽어 온 데이터의 라인 번호를 카운트(JDK 1.1부터 LineNumberReader로 대체)
ObjectInputStreamObjectOutputStream데이터를 객체단위로 읽고 쓰는데 사용. 주로 파일을 이용하며 객체 직렬화와 관련있음
없음PrintStream버퍼를 이용하며. 추가적인 print관련 기능 (print, printf, println)
PushbackInputStream없음버퍼를 이용해서 읽어 온 데이터를 다시 되돌리는 기능 (unread, push back to buffer)

문자기반 스트림 - Write, Reader

바이트기반의 입출력 스트림은 단점이 있다. 바로 자바에선 한 문자를 나타내는 자료형인 char은 1byte가 아닌 2byte이기 때문에 바이트기반의 스트림으로 2byte인 문자를 처리하는 데 어려움이 있다.

이러한 바이트기반의 입출력 스트림의 단점(1byte -> 2byte)을 보완하기 위해 문자기반의 스트림을 제공한다. 문자데이터를 입출력할 때는 바이트기반 스트림 대신 문자 기반스트림을 활용하는 것이 좋은 편이다.

  • InputStream -> Reader
  • OutputStream -> Writer

바이트기반과 문자기반 스트림 비교

바이트기반 스트림문자기반 스트림
FileInputStream/FileOutputStreamFileReader/FileWriter
ByteArrayInputStream/ByteArrayOutputStreamCharArrayReader/CharArrayWrtier
PipedInputStream/PipedOutputStreamPipedReader/PipedWriter
StringBufferInputStream(deprecated)/StringBufferOutputStream(deprecated)StringReader/StringWriter

StringBufferInputStream, StringBufferOutputStream은 StringReader와 StringWriter로 대체되어 더 이상 사용하지 않음 -> deprecated

네이밍 규칙을 보면 문자기반 스트림은 바이트기반의 스트림의 네이밍 중 InputStream은 Reader로, OutputStream은 Writer로 바꾸면 된다.

단, ByteArrayInputStream에 대응하는 문자기반 스트림은 char배열을 사용하는 CharArrayReader이다. 이와 같은 맥락으로 byte배열 대신 char배열을 사용한다는 것과 추상메소드가 달라졌다. 이름만 다소 다를 뿐 활용 방법은 동일하다고 생각하면 된다.

InputStream과 Reader

InputStreamReader
abstract int read()int read()
int read(byte[] b)int read(char[] cbuf)
int read(byte[] b, int off, int len)abstract int read(char[] cbuf, int off, int len)

OutputStream과 Writer

OutputStreamWriter
abstract void write(int b)void write(int c)
void write(byte[] b)void write(char[] cbuf)
void write(byte[] b, int off, int len)abstract void write(char[] cbuf, int off, int len)
void write(String str)
void write(String str, int off, int len)

보조스트림 역시 문자기반 보조스트림이 존재하며 사용목적과 방식은 바이트 기반 보조 스트림과 동일하다.

InputStream과 OutputStream

InputStream과 OutputStream은 모든 바이트기반의 스트림의 조상이며 아래와 같은 메소드들이 선언되어 있다.

InputStream의 메소드

메소드명설 명
int available()스트림으로부터 읽어 올 수 있는 데이터의 크기를 반환한다.
void close()스트림을 닫음으로써 사용하고 있던 자원을 반환한다.
void mark(int readlimit)현재위치를 표시해놓는다. 후에 reset()에 의해서 표시해 놓은 위치로 다시 돌아갈 수 있다. readlimit은 되돌아갈 수 있는 byte의 수이다.
boolean markSupported()mark()와 reset()을 지원하는지를 알려 준다. mark()와 reset()기능을 지원하는 것은 선택적이므로, mark()와 reset()을 사용하기 전에 markSupported()를 호출해서 지원여부를 확인해야 한다.
abstract int read()1 byte를 읽어 온다(0 ~ 255 사이의 값). 더 이상 읽어 올 데이터가 없으면 -1을 반환한다. abstract메소드라서 InputStream의 자손들은 자신의 상황에 알맞게 구현해야한다.
int read(byte[] b)배열 b의 크기만큼 읽어서 배열을 채우고 읽어 온 데이터의 수를 반환한다. 반환하는 값은 항상 배열의 크기보다 작거나 같다.
int read(byte[] b, int off, int len)최대 len개의 byte를 읽어서, 배열 b의 지정된 위치(off)부터 저장한다. 실제로 읽어 올 수 있는 데이터가 len개보다 적을 수 있다.
void reset()스트림에서의 위치를 마지막으로 mark()이 호출되었던 위치로 되돌린다.
long skip(long n)스트림에서 주어진 길이(n)만큼을 건너뛴다.

OutputStream의 메소드

메소드명설 명
void close()입력소스를 닫음으로써 사용하고 있던 자원을 반환한다.
void flush()스트림의 버퍼에 있는 모든 내용을 출력소스에 쓴다.
abstract void write(int b)주어진 값을 출력소스에 쓴다.
void write(byte[] b)주어진 배열 b에 저장된 모든 내용을 출력소스에 쓴다.
void write(byte[] b, int off, int len)주어진 배열 b에 저장된 내용 중에서 off번째부터 len개 만큼만을 읽어서 출력소스에 쓴다.

mark(), reset()

스트림의 종류에 따라서 mark()reset()을 사용하여 이미 읽은 데이터를 되돌려서 다시 읽을 수 있다. 이 기능을 지원하는 스트림을 찾기 위해선 markSupported()를 통해서 알 수 있다.

flush()

flush()는 버퍼가 있는 출력스트림의 경우에만 의미가 있으며, 부모 클래스인 OutputStream의 정의된 flush()는 아무런 일도 하지 않는다.

close()

프로그램이 종료될 때, 사용하고 닫지 않은 스트림을 JVM이 자동적으로 닫아 주기는 하지만, 스트림을 사용해서 모든 작업을 마치고 난 후에는 close()를 호출해서 반드시 닫아 주어야 한다.

ByteArrayInputStream과 같이 메모리를 사용하는 스트림과 System.in, System.out과 같은 표준입출력스트림은 닫아 주지 않아도 된다.

ByteArrayInputSream과 ByteArrayOutputStream

ByteArrayInputSream과 ByteArrayOutputStream은 메모리, 즉 바이트배열에 데이터를 입출력 하는데 사용되는 스트림이다. 주로 다른 곳에 입출력하기 전에 데이터를 임시로 바이트배열에 담아서 변환 등의 작업을 하는데 사용된다.

자주 사용되는 건 아니지만, 스트림이 읽고 쓰는 방법은 동일하기 때문에 알아둬야 한다.

read(), write()

public class ByteArrayIOStreamTest {
    public static void main(String[] args) {
        byte[] inSrc = {0,1,2,3,4,5,6,7,8,9};
        byte[] outSrc = null;

        ByteArrayInputStream input = new ByteArrayInputStream(inSrc);
        ByteArrayOutputStream output = new ByteArrayOutputStream();

        int data = 0;

        while((data = input.read())!=-1)
            output.write(data);	// void write(int b)

        outSrc = output.toByteArray(); // 스트림의 내용을 byte배열로 반환한다.

        System.out.println("Input Source  :" + Arrays.toString(inSrc));
        System.out.println("Output Source :" + Arrays.toString(outSrc));
    }
}
결과💡
Input Source  :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Output Source :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

ByteArrayInputStream/ByteArrayOutStream를 이용해서 바이트배열인 inSrc의 데이터를 outSrc로 복사하는 예제이다. read()와 write를 사용하는 가장 기본적인 방법이라고 하는데..다른 건 모르겠고, while문을 분석해보자

 while((data = input.read())!=-1)
            output.write(data);	// void write(int b)
  1. data = input.read() // read()를 호출한 반환값을 변수 data에 저장한다.
  2. data != -1 // data에 저장된 값이 -1이 아닌지 비교한다. (EOF End Of File을 의미하는 듯하다.)

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

위 예제는 한 번에 1byte만 읽고 쓰므로 데이터 양이 많을 때에는 비교적으로 비효율적인 방법일 수 도 있다. 이번 예제는 temp 배열을 이용하여 한 번에 하나씩 읽어오는게 아니라 여러 개를 읽어온다고 생각하면 이해가 빠르다.

public class ByteArrayIOStreamTest2 {
    public static void main(String[] args) {
        byte[] inSrc = {0,1,2,3,4,5,6,7,8,9};
        byte[] outSrc = null;
        byte[] temp = new byte[10];

        ByteArrayInputStream  input  = null;
        ByteArrayOutputStream output = null;

        input  = new ByteArrayInputStream(inSrc);
        output = new ByteArrayOutputStream();

        input.read(temp,0,temp.length); // 읽어 온 데이터를 배열 temp에 담는다.
        output.write(temp,5, 5);        // temp[5]부터 5개의 데이터를 write한다.

        outSrc = output.toByteArray();

        System.out.println("Input Source  :" + Arrays.toString(inSrc));
        System.out.println("temp          :" + Arrays.toString(temp));
        System.out.println("Output Source :" + Arrays.toString(outSrc));
    }
}
결과💡
Input Source  :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
temp          :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Output Source :[5, 6, 7, 8, 9]

여기서 byte배열 temp의 크기는 10이라서 10byte를 읽어왔지만 실질적으로 output에는 temp[5]번째 부터 5개의 데이터(5byte)를 출력하였다.

FileInputStream과 FileOutputStream

FileInputStream과 FileOutputStreamd은 파일에 입출력을 하기 위한 스트림이다.

FileInputStream, FileOutputStream의 생성자

생성자설 명
FileInputStream(String name)지정된 파일이름(name)을 가진 실제 파일과 연결된 FileInputStream을 생성한다.
FileInputStream(File file)파일의 이름이 String이 아닌 File인스턴스로 지정해주어야 하는 점을 제외하고 FileInputStream(String name)과 동일하다.
FileOutputStream(String name)지정된 파일이름(name)을 가진 실제 파일과 연결된 FileOutputStream을 생성한다.
FileOutputStream(String name, boolean append)지정된 파일이름(name)을 가진 실제 파일과 연결된 FileOutputStream을 생성한다. 두번째 인자인 append를 true로 하면, 출력 시 기존의 파일내용의 마지막에 덧붙인다. false면, 기존의 파일 내용을 덮어쓰게 된다.
FileOutputStream(File file)파일의 이름을 String이 아닌 File 인스턴스로 지정해주어야 하는 점을 제외하고 FileOutputStream(String name)과 같다.

test.txt 파일을 생성한 후 텍스트를 가져오는 예제이다.

테스트를 위해 src 디렉토리 아래에 test.txt 파일을 생성했다.

public class FileIOStreamTest {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("./src/test.txt");
        int data = 0;

        while((data=fis.read())!=-1) {
            char c = (char)data;
            System.out.print(c);
        }
    }
}
결과💡
bigmac

콘솔창에 입력하는대로 파일에 출력하기

이번엔 반대로 콘솔창에 텍스트를 입력하면 txt 파일 형태로 출력하는 예제이다.

public class FileIOStreamTest2 {
    public static void main(String[] args) {
        int data = 0;
        try {
            // FileOutputStream 은 무조건 해당 파일을 생성한다. append 값을 true로 했기 때문에 이어서 작성할 수 있다.
            FileOutputStream fos = new FileOutputStream("./src/test2.txt", true);
            while((data = System.in.read()) != -1) {
                fos.write(data);
            }
            fos.close(); // 파일을 닫는다.
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
결과💡
macchicken
cheese

바이트기반의 보조스트림

바이트기반의 보조스트림으로는 FilterInputStream/FilterOutputStream, BufferedInputStream/BufferOutputStream, DataInputStream/DataOutputStream, SequenceInputStream, PrintStream 등이 있지만 그 중 이번 스터디 주제 중 버퍼와 관련된 보조스트림만 다룰 예정이다. 나머지 보조스트림들은 자바 기본서적이나, 인터넷에 예제 코드로 잘 정리되어있기 때문에 필요한 순간에 참고하면 좋을 것 같다.

BufferedInputStream과 BufferedOutputStream

BufferedInputStream과 BufferedOutputStream은 스트림의 입출력 효율을 높이기 위해 버퍼를 사용하는 보조스트림이다. 한 바이트씩 입출력되는 것 보다는 버퍼(바이트배열)를 이용해서 한 번에 여러 바이트를 입출력하는 것이 빠르기 때문이다.

BufferedInputStream의 생성자

메소드 / 생성자설 명
BufferedInputStream(InputStream in, int size)주어진 InputStream 인스턴스를 입력소스로 하며 지정된 크기의 버퍼를 갖는 BufferedInputStream 인스턴스를 생성한다.
BufferedInputStream(InputStream in)주어진 InputStream 인스턴스를 입력소스로 하며 버퍼의 크기를 지정해주지 않으므로 기본적으로 8192 바이트 크기의 버퍼를 갖게 된다.

프로그램에서 입력소스로부터 데이터를 읽기 위해 처음으로 read메소드를 호출하면, BufferedInputStream은 입력소스로 부터 버퍼 크기만큼의 데이터를 읽어다 자신의 내부 버퍼에 저장한다. 프로그램은 이제부터 BufferedInputStream의 저장된 데이터를 읽어오기만 하면 된다.

BufferedOutputStream의 생성자

메소드 / 생성자설 명
BufferedOutputStream(OutputStream out, int size)주어진 OutputStream 인스턴스를 출력소스로 하며 지정된 크기의 버퍼를 갖는 BufferedOutputStream 인스턴스를 생성한다.
BufferedOutputStream(OutputStream out)주어진 OutputStream 인스턴스를 출력소스로 하며 버퍼의 크기를 지정해주지 않으므로 기본적으로 8192 바이트 크기의 버퍼를 갖게 된다.
flush()버퍼의 모든 내용을 출력소스에 출력한 다음, 버퍼를 비운다.
close()flush()를 호출해서 버퍼의 모든 내용을 출력소스에 출력하고, BufferedOutputStream 인스턴스가 사용하던 모든 자원을 반환한다.

BufferedOutputStream 역시 버퍼를 이용해서 출력해주게 되는데 읽을 때와 반대로 write메소드를 이용해서 BufferedOutputStream의 버퍼에 저장된다. 이 때 버퍼가 가득 차면 버퍼의 모든 내용을 출력하게 되며 그리고는 버퍼를 비운 후 다시 저장할 준비를 한다.

버퍼가 가득 찼을 때만 출력하기 때문에, 마지막 출력부분이 출력소스에 쓰여지지 못하고 종료될 수 있기 떄문에 항상 프로그램이 종료될 땐 close(), flush()를 호출하여 버퍼의 있는 출력해야 한다.

버퍼

버퍼란? 임시 저장 공간을 의미한다. A와 B가 서로 입출력을 수행하는데 있어서 속도차이를 극복하기 위해 사용하는 임시 저장을 의미한다.

쉽게 이해하는 버퍼

유튜브와 같은 동영상 스트리밍 사이트에 들어가면 버퍼를 쉽게 확인할 수 있다. 영상을 시작하게 되면 동영상이 진행되는 부분(빨간색)서버로부터 동영상을 내려받은 부분(회색)을 볼 수 있을 것이다. 여기서 회색부분이 버퍼이다. 동영상이 서버로부터 내려 받아지는 속도에 비해 우리가 동영상을 보는 속도가 차이가 나기 때문에 버퍼라는 임시 저장공간을 두고 최대한 빠르게 동영상을 서버로부터 내려받을 수 있게끔 만든 것이다.

문자기반 스트림

문자데이터를 다루는데 사용된 다는 것을 제외하고는 바이트기반 스트림과 문자기반 스트림의 사용방법은 거의 같다. 바이트기반 스트림의 조상이 InputStream/OutputStream인 것과 같이 문자기반의 스트림에서는 Reader/Writer가 동일한 역할을 한다. InputStream/OutputStream read(), write 메소드의 인자값의 타입으로 byte가 왔다면 Reader/Writer에선 char가 오게 된다.

Reader와 Writer

InputStream/OutputStream에서 byte 배열을 사용하는 대신 Reader/Writer는 char 배열을 사용하기 때문에 앞서 배웠던 바이트기반의 스트림과 다른 점은 거의 없다.

Reader의 메소드

메소드설 명
abstract void close()입력스트림을 닫음으로써 사용하고 있던 자원을 반환한다.
void mark(int readlimit)현재위치를 표시해놓는다. 후에 reset()에 의해서 표시해 놓은 위치로 다시 돌아갈 수 있다.
boolean markSupported()mark()와 reset()을 지원하는지를 알려 준다.
int read()입력소스로부터 하나의 문자를 읽어 온다. char의 범위인 0~65535범위의 정수를 반환하며, 입력스트림의 마지막 데이터에 도달하면, -1을 반환한다.
int read(char[] c)입력소스로부터 매개변수로 주어진 배열 c의 크기만큼 읽어서 배열 c에 저장한다. 읽어 온 데이터의 개수 또는 -1를 반환한다.
abstract int read(char[] c, int off, int len)입력소스로부터 최대 len개의 문자를 읽어서, 배열 c의 지정된 위치(off)부터 읽은 만큼 저장한다. 읽어 온 데이터의 개수 또는 -1를 반환한다.
boolean ready()입력소스로부터 데이터를 읽을 준비가 되어있는지 알려 준다.
void reset()입력소스에서의 위치를 마지막으로 mark()가 호출되었던 위치로 되돌린다.
long skip(long n)현재 위치에서 주어진 문자 수(n)만큼을 건너뛴다.

Writer의 메소드

메소드설 명
abstract void close()출력스트림을 닫음으로써 사용하고 있던 자원을 반환한다.
abstract void flush()스트림의 버퍼에 있는 모든 내용을 출력소스에 쓴다.(버퍼가 있는 스트림에만 해당됨)
void write(int b)주어진 값을 출력소스에 쓴다.
void write(char[] c)주어진 배열 c에 저장된 모든 내용을 출력소스에 쓴다.
abstract void write(char[] c, int off, int len)주어진 배열 c에 저장된 내용 중에서 off번째부터 len길이만큼만 출력소스에 쓴다.
void write(String str)주어진 문자열(str)을 출력소스에 쓴다.
void write(String str, int off, int len)주어진 문자열(str)의 일부를 출력소스에 쓴다.(off번째 문자부터 len개 만큼의 문자열)

FileRead와 FileWriter

FileRead와 FileWriter는 파일로부터 텍스트데이터를 읽고, 파일에 쓰는데 사용한다. 사용방법은 FileInputStream/FileOutputStream과 동일하다

FileInputStream과 FileReader의 차이점을 보여주기 위한 예제

바이트기반 스트림과 문자기반 스트림 사이에는 중요한 차이점이 있는데 문자기반 스트림을 상속받는 클래스 즉, Reader/Writer을 상속받는 클래스들은 유니코드에 대한 인코딩을 자동적으로 해준다.

public class FileReaderWriterTest {
    public static void main(String[] args) {
        try {
            String fileName = "./src/test.txt";
            FileInputStream fis = new FileInputStream(fileName);
            FileReader fr = new FileReader(fileName);

            int data =0;
            // FileInputStream을 이용해서 파일내용을 읽어 화면에 출력한다.
            while((data=fis.read())!=-1) {
                System.out.print((char)data);
            }
            System.out.println();
            fis.close();

            // FileReader를 이용해서 파일내용을 읽어 화면에 출력한다.
            while((data=fr.read())!=-1)
                System.out.print((char)data);
            System.out.println();
            fr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
결과💡
bigmac
한글 // 바이트기반인 FileInputStream으로 한글 데이터를 가져왔을땐 인코딩의 문제가 있다.
bigmac
한글 // 문자기반인 FileReader를 사용하여 한글 데이터를 가져오면 인코딩의 문제가 해결된다.

문자기반의 보조스트림

BufferedReader와 BufferedWriter

BufferdReader/BufferedWriter 또한 BufferInputStream/BufferedOutputStream과 마찬가지로 입출력의 효율을 높이는 역할을 하며 자세한 설명은 위에서 설명하였다 특이점은 readLine()을 사용하면 데이터를 라인단위로 읽어 올 수 있고, newLine()을 사용하면 줄바꿈도 해준다.

public class FileReaderWriterTest2 {
    public static void main(String[] args) {
        try {
            FileReader fr = new FileReader("./src/test2.txt");
            BufferedReader br = new BufferedReader(fr);

            String line = "";
            for (int i = 1; (line = br.readLine()) != null; i++) {
                System.out.println(line);
            }

            br.close();
        } catch (IOException e) {
        }
    }
}
결과💡
macchicken
cheese
안녕하세요

표준 스트림(System.in, System.out, System.err)

콘솔 입출력

자바에서는 표준 입출력(standard I/O)을 위해 3가지 입출력 스트림, System.in, System.out, System.err을 제공하는데, 이 들은 자바 애플리케이션의 실행과 동시에 사용할 수 있게 자동적으로 생성되기 때문에 개발자가 별도로 스트림을 생성하는 코드를 작성하지 않아도 된다.

Console : 시스템을 사용하기 위해 키보드로부터 입력을 받고 화면으로 출력하는 소프트웨어(리눅스/유닉스 : 터미널, 윈도우 : 명령 프롬프트)

System.in : 콘솔로부터 데이터를 입력 받을 때 사용 System 클래스의 in 정적 필드, System.in은 InputStream 타입의 필드

System.out : 콘솔로 데이터를 출력하기 위해 사용되는 System 클래스의 out 정적 필드이다. out은 PrintStream 타입의 필드이고 PrintStream은 OutputStream의 하위 클래스이다. out 필드를 OutputStream 타입으로 변환해서 사용할 수 있다.

System.err : 콘솔로 에러를 출력하기 위해 사용되는 System 클래스의 err 정적 필드이다. System.out과 마찬가지로 모니터에 출력되는 경우가 많다. 일반적으로 정상 적인 출력은 System.out으로 나가고, 오류가 발생한 경우 System.err로 출력된다고 볼 수 있다. 이또한 PrintStream 클래스의 타입으로 타입변환 또한 System.out과 동일하다.

NIO

자바 4버전부터 새로운 입출력(New Input Output)이라는 뜻에서 java.nio 패키지에 포함되었는데 자바 7버전으로 업하면서 자바 IO와 NIO 사이의 일관성 없는 클래스 설계를 바로 잡고 비동기 채널 등의 네트워크 지원을 대폭 강화한 NIO.2 API가 추가되었음

NIO는 왜 탄생하게 되었나?

특정 기술이 탄생할 땐 그만한 이유가 있다고 생각한다. 그렇기 때문에 기존에 IO에 어떤 불편한점이나, 개선할 사항이 있기 때문에 NIO 패키지가 탄생했다고 생각한다.

I/O의 기본 동작 원리

출처 : HowToDoInJava

용어 정리
  • User Space : 일반적인 프로세스들이 존재하는 '제한된 권한'을 갖는 영역, 하드웨어 장치나 다른 프로세스에 직접적으로 접근할 수 없다.
  • Kernal Space : 운영체제에 존재하는 영역으로 하드웨어 장치에 직접적으로 접근하고 다른 프로세스를 제어할 수 있는 권한이 있다.
read()함수 호출

사용자 프로세스는 User Space에서 동작하기 때문에 하드웨어에 직접적으로 접근할 수 없다. 때문에 OS에서 제공하는 시스템콜을 통해 I/O를 수행해야 한다.

여기서 read()시스템콜을 통해 커널에게 I/O 수행을 요청한다. 이 과정에서 유저 모드(User mode)에서 커널 모드(Kernal mode)로 스위칭이 발생한다.

제어권을 넘겨받은 커널은 우선 프로세스가 요청한 데이터가 이미 커널 영역 캐시 메모리에 존재하는지 확인한다. 만일 데이터가 캐시에 존재한다면 해당 데이터를 read()함수 호출 시 전달받은 메모리 영역에 복사한 뒤 제어권을 다시 사용자 프로세스에게 넘긴다.(커널 모드 -> 유저 모드로 스위칭)

데이터가 캐시에 존재하지 않는다면 디스크로부터 데이터를 가져오는 과정을 수행한다.

DMA 컨트롤러에게 데이터 로드를 요청

CPU는 디스크와 통신하여 직접 데이터를 읽지 않고(PIO, Programmed Input/Output의 줄임말로 CPU와 주변기기가 직접 통신하여 I/O를 수행하는 방법), DMA(Direct Memory Access) 컨트롤러에게 데이터 로드를 요청한다.

이는 CPU가 디스크보다 수백배는 빠르기 때문에, 디스크의 처리 시간을 기다리는 것이 낭비이기 때문이다.

DMA 컨트롤러는 다시 Disk Controller에게 디스크로부터 데이터 읽기를 요청한다.

DMA -> CPU 인터럽트

Disk Controller -> DMA로의 데이터 전송이 완료되면, DMA 컨트롤러는 DMA에 저장된 데이터를 다시 커널의 버퍼 메모리 영역에 복사한다. 이 과정이 완료된 후 DMA 컨트롤러는 CPU 인터럽트를 수행한다.

CPU 인터럽트 이후

DMA -> CPU 인터럽트가 일어나면 CPU는 커널 영역 버퍼 메모리의 데이터를 유지 영역 버퍼 메모리에 복사한 뒤 read()과정을 종료한다.

사용자 프로세스는 Block 되어 있던 메소드가 완료되며 요청한 데이터를 사용할 수 있게 된다.

위 그림에서와 같이 모든 I/O는 반드시 커널 영역을 직간접적으로 거쳐야만 한다.

예를 들어 자바에서 파일 읽기를 시도한다고 하면, 커널은 시스템 콜(read())를 통해서 디스크 컨트롤러가 물리적 디스크로부터 읽어온 파일 데이터를 커널 영역 안의 버퍼로 저장한다. 그 후 모든 파일 데이터가 커널 안의 버퍼로 복사되면 JVM(프로세서) 안의 버퍼로 복사를 시작한다.

쉽게 정리하면

  1. 제일 먼저 프로세스는 커널에 파일 읽기 명령을 내린다.
  2. 커널은 시스템 콜(read())를 사용해서 디스크 컨트롤러가 물리적 디스크로부터 읽어온 파일 데이터를 커널 영역안의 버퍼에 쓴다.
  3. 모든 파일 데이터가 버퍼에 복사되면 다시 프로레스안의 버퍼로 복사를 한다.
  4. 프로세스 안의 버퍼의 내용으로 프로그래밍한다.

이때, 비효율적인 과정이 있다.

  1. 커널 영역 버퍼에서 프로세스 영역 안의 버퍼로 데이터를 복사하는 것.
    • 만약 커널 영역의 버퍼에 저장된 데이터를 직접 사용한다면 복사하는 시간이 단축 될 것이다.
  2. 디스크 컨트롤러에서 커널 영역의 버퍼로 데이터를 복사하는 동안 프로세스 영역은 블록킹이 된다.
    • 운영체제가 효율을 높이기 위해 최대한 많은 양의 데이터를 커널 영역 버퍼에 저장한 후 프로세스 영역의 버퍼로 전달한다. 따라서 디스크의 파일 데이터를 커널 영역 안의 버퍼로 모두 복사할 때까지는 자바 프로세스는 블록킹이 된다.

NIO의 주요 키워드

Channel

읽기, 쓰기 하나씩 쓸 수 있는 스트림은 단방향식, 채널은 읽기 쓰기 둘다 가능한 양방향식 입출력클래스

Buffer

커널에 의해 관리되는 시스템 메모리를 직접 사용할 수 있는 Buffer 클래스

Selector

네트워크 프로그래밍의 효율을 높이기 위한 것이라는데, 현재 스터디 주제와는 맞지 않기 때문에 공부하지 않을 예정이다.

기존 IO와 NIO의 차이점

구분IONIO
입출력 방식스트림 방식채널 방식
버퍼 방식넌버퍼버퍼
비동기 방식지원 안 함지원
블로킹 / 넌블로킹 방식블로킹 방식만 지원둘 다 지원

자세한 설명들은 더 잘 정리된 글들을 참고하면 좋을 것이다. 이미 이 글 자체에..이론적인 부분이 너무 많기 때문에 넘어갈 예정이다..

NIO의 Channel 클래스

NIO의 Channel은 Buffer에 있는 내용을 다른 어디론가 보내거나 다른 어딘가에 있는 내용을 Buffer로 읽어들이기 위해 사용된다. 네트워크 프로그래밍을 할 때 다양한 예들이 있는데 주제와 맞지 않기 때문에 Channel 클래스를 이용해서 간단하게 파일에 있는 내용을 복사한 후 새로운 파일에 내용을 저장하는 예제를 만들 것이다.

FileChannel은 File에 있는 내용을 ByteBuffer로 불러오거나 ByteBuffer에 있는 내용을 File에 쓰는 역할을 수행한다. 그 전에 꼭 알아야 할것은, Channel은 직접 인스턴스화 할 수가 없다. 직접 생성자를 이용해서 인스턴스화는 것이 아니라, InputStream이나 OutputStream에서 getChannel() 메소드를 이용해서 Channel 타입의 객체를 반환받은 다음에 만들어야 한다.

  • Channel은 직접 인스턴스화 할 수가 없다.
  • OutputStream/InputStream에서 만들어야한다
Sample Code
public class ChannelTest {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("./src/test.txt");
        FileOutputStream fos = new FileOutputStream("output.txt");

        ByteBuffer buf = ByteBuffer.allocateDirect(10);
        FileChannel cin = fis.getChannel();

        FileChannel cout = fos.getChannel();
        cin.read(buf);

        buf.flip();
        cout.write(buf);


    }
}

Channel 기반으로 파일 입출력을 동시에 실행시킨 결과이다.

참고자료 🧾

0개의 댓글