[Java] I/O

JM·2022년 9월 21일
2

Java_Live_Study

목록 보기
13/15
post-thumbnail

학습할 것 (필수)

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

참고자료



스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O

스트림

자바에서는 I/O를 구현하기 위해 stream 이라는 개념을 도입하였다. stream 은 데이터의 연속이라고 묘사된다. 왜냐하면 계속해서 데이터가 흐르는 것과 유사하기 때문이다. 배열의 개념과는 다르게, streamindexing 개념을 지원하지 않는다. stream 은 오직 데이터의 근원과 목적지에만 집중한다. 이러한 특징 때문에, stream 에서는 데이터 안에서 앞 혹은 뒤로 움직일 수 없다. 그리고 단방향이며, FIFO로 동작한다.

또한, stream 은 blocking으로 동작한다는 특징이 있다. 아래 코드는 사용자 입력을 출력하는 코드이다. 사용자의 입력이 들어올 때까지 read() 이후의 코드는 실행되지 않는다.

public class BlockingIOTest {
    public static void main(String[] args) throws IOException {
        InputStreamReader inputStreamReader =
                new InputStreamReader(System.in);
        System.out.println(inputStreamReader.read());

        System.out.println("finished!");
    }
}
// 입력
// a
// 출력
// 97
// finished!


위 그림은 stream의 동작 방식을 묘사하고 있다. stream의 주요한 요소는 `source` , `Element` , `Destination` 이다. `Source` 와 `Destination` 은 파일 or 네트워크 연결 or 파이프 or 메모리 버퍼 등이 될 수 있다. `Element` 는 그저 데이터의 조각이며 `stream` 은 그러한 데이터들의 모음을 가지고 있는 추상적인 개념이다. 자바의 기본 I/O 들은 대부분 `stream` 으로 구현되어있다.

자바8부터 도입된 stream API로 인해 stream I/O의 개념에 대한 혼동이 있을 수 있다. I/O stream은 자바에서 I/O를 다루기 위해 도입한 추상적인 개념이다. stream API는 I/O stream과 상관 없으며, 데이터의 집합을 다루는 함수적인 접근법을 제공하는 API이다.



버퍼

프로그래밍에서 버퍼는 CPU와 보조기억장치 사이의 임시 저장공간을 의미한다. CPU 매우 빠른데 비해, 보조기억장치에서 CPU에게 데이터를 전달하는 속도를 느리다. 따라서, CPU가 보조기억장치에서 전달하는 데이터를 기다리는 것은 손해이다.

이러한 문제를 해결하기 위해 버퍼라는 임시 저장공간을 두어, 보조기억장치에서 버퍼에 데이터를 적재하는 것을 완료하면, 그때 CPU가 버퍼에서 데이터를 읽어들이도록 한다. CPU는 보조기억장치를 기다리지 않아도 되기 때문에 효율적으로 일을 처리할 수 있다.

버퍼 I/O를 처리하기 위해 unbuffered stream을 버퍼 스트림으로 감싼다.

버퍼를 사용하는 I/O를 위해 BufferedInputStream 클래스를 사용하겠다. 해당 클래스는 내부적으로 버퍼를 사용하여 퍼포먼스에서 우위를 갖는다. stream에서 데이터를 skip 하거나 읽거오며, 내부 버퍼는 자동적으로 input stream으로 부터 데이터를 채운다. 많은 바이트를 한번에 채운다. BufferedInputStream이 생성되면 내부 버퍼 배열도 생성된다.


public class BufferInputStreamTest {
    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("text.txt");
        BufferedInputStream br = new BufferedInputStream(fileInputStream);
        int c = 1;
        while((c = br.read()) > 0) {
            System.out.print((char) c);
        }
    }
}

BufferedInputStream의 인스턴스가 파일을 읽어들이는 것 처럼 보이지만, 실제로는 BufferedInputStream은 buffer만 제공할 뿐이며, read()를 수행하는 것은 FileInputStream의 인스턴스이다. BufferedInputStream와 같이 보조하는 역할을 하는 스트림을 보조 스트림이라고 한다.



채널

채널은 new I/O인 NIO에서 지원하는 I/O 클래스이다. 단방향으로 동작하는 stream 과는 다르게, channel 은 양방향이다. 그리고, stream 은 단독으로 source와 연결되어 I/O가 가능하지만, NIO는 항상 버퍼로 데이터를 읽어들이거나 쓴다. NIO는 비동기와 Non-blocking을 지원한다는 장점이 있다.

public class NioSample {
    public static void main(String[] args) {
        NioSample nioSample = new NioSample();
        nioSample.basicWriteAndRead();
    }

    public void basicWriteAndRead(){
        String filename = "text.txt";
        try {
            writeFile(filename,"My first NIO sample");
            readFile(filename);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public void writeFile(String filename, String data) throws IOException {
        // FileChannel 객체를 만들려면, FileOutputStream 클래스에 선언된 getChannel()을 호출한다.
        FileChannel channel = new FileOutputStream(filename).getChannel();
        byte[] byteData = data.getBytes();
        // ByteBuffer 클래스의 static 메소드인 wrap()을 호출하면 ByteBuffer 객체를 반환한다.
        // ByteBuffer는 abstract 클래스이기 때문에, 해당 클래스를 구현한 구현체의 인스턴스를
        // 리턴하는 것이다. 또한, Buffer객체가 필요한 이유는 Channel 클래스에서
        // Buffer객체를 이용해 대상과 데이터를 주고받기 때문이다.
        ByteBuffer buffer = ByteBuffer.wrap(byteData);
        // write()메소드에 buffer 객체를 넘겨주면 파일에 데이터를 쓰이게 된다.
        channel.write(buffer);
        channel.close();
    }

    public void readFile(String fileName) throws Exception{
        // writeFile() 메소드에서와 동일하게 File***Stream 객체로 부터 Channel 객체를 가져온다.
        FileChannel channel = new FileInputStream(fileName).getChannel();
        // Buffer 인스턴스를 생성하는 다른 방법이다.
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // read() 메소드에 buffer 객체를 넘겨주어 읽어드리기 위한 준비를 한다.
        channel.read(buffer);
        // buffer 객체는 CD와 같이 현재 위치를 가지고 있다.
        // flip()은 그러한 위치를 처음으로 옮기고, 버퍼가 읽거나 쓸 수 없는 첫 번째 위치를
        // 나타내는 limit을 맨 끝으로 이동시켜준다.
        buffer.flip();
        while(buffer.hasRemaining()){
            System.out.print((char)buffer.get());
        }
        channel.close();
    }
}

channel 이 양방향으로 동작한다는 말 때문에, 하나의 버퍼를 통해 데이터가 오고 갈 수 있다는 말인지 아니면, Channel 의 인스턴스가 input일 수도 있고 output일 수도 있다는 말인지 혼동이 되었다.

확인해보니, Channel 클래스는 Input과 Output의 역할을 모두 할 수 있다는 의미였다.



InputStream과 OutputStream

InputStream과 OutputStream은 자바 스트림들의 부모 클래스이다.

자바 IO는 InputStreamOutputStream 이라는 추상 클래스를 통해 제공된다.

public abstract class InputStream implements Closeable {}
public abstract class OutputStream implements Closeable, Flushable {}

public interface Closeable extends AutoCloseable {
		public void close() throws IOException;
}

public interface Flushable {
		void flush() throws IOException;
}

두 클래스 모두 Closeable 인터페이스를 구현하고 있다. Closeable 인터페이스는 close() 메소드를 가지고 있는데, 해당 메소드를 통해 스트림으로 연결되 있던 대상을 닫아주는 것이다. 닫아주지 않으면 해당 대상을 다른 클래스에서 작업할 수 없기 때문이다.

OutputStream 클래스는 추가적으로 Flushable 인터페이스를 구현하고 있다. flush() 는 버퍼에 있는 데이터를 stream 으로 연결된 목적지에 전부 쓰도록 한다. 다시 말해, “현재 버퍼에 있는 내용을 지금 저장해!” 라고 명령을 내리는 것이다.


InputStream

  • 메소드

    리턴 타입메소드 이름 및 매개 변수설명
    intavailable()스트림에서 중단없이 읽을 수 있는 바이트의 개수를 리턴한다.
    voidmark(int readlimit)스트림의 현재 위치를 표시(mark)해 둔다. 여기서 매개 변수로 넘긴 int 값은 표시해둔 자리의 최대 유효 길이이다. 이 값을 넘어가면, 표시해 둔 자리는 더 이상 의미가 없어진다.
    voidreset()위치를 mark() 메소드가 호출되었던 위치로 되돌린다.
    booleanmarkSupported()mark()나 reset() 메소드가 수행 가능한지를 확인한다.
    abstract intread()스트림에서 다음 바이트를 읽는다. 이 클래스에 유일한 추상 메소드이다.
    intread(byte[] b)매개 변수로 넘어온 바이트 배열에 데이터를 담는다. 리턴 값은 데이터를 담은 개수다.
    intread(byte[] b, int off, int len)매개 변수로 넘어온 바이트 배열에 특정 위치(off)부터 지정한 길이(len) 만큼의 데이터를 담는다. 리턴 값은 데이터를 담은 개수다.
    longskip(long n)매개 변수로 넘어온 길이(n)만큼의 데이터를 건너 뛴다.
    voidclose()스트림에서 작업중인 대상을 해제한다. 이 메소드를 수행한 이후에는 다른 메소드를 사용하여 데이터를 처리할 수 없다.
  • 자식 클래스

    위 클래스들 중 많이 사용되는 클래스는 다음과 같다.

    클래스설명
    FileInputStream파일을 읽는 데 사용한다. 주로 우리가 쉽게 읽을 수 있는 텍스트 파일을 읽기 위한 용도라기보다, 이미지와 같이 바이트 코드로 된 데이터를 읽을 때 사용한다.
    FilterInputStream이 클래스는 다른 입력 스트림을 포괄하며, 단순히 InputStream 클래스가 Override되어 있다.
    ObjectInputStreamObjectOutputStream으로 저장한 데이터를 읽는데 사용한다.
    public class FileInputStreamTest {
        public static void main(String[] args) throws IOException {
            FileInputStream fileInputStream = new FileInputStream("file.txt");
            int c = 1;
            while( (c = fileInputStream.read()) > 0){
                System.out.print((char) c);
            }
            fileInputStream.close();
        }
    }
    // 출력
    // ABCDEFGHIJ
    public class ObjectInputStreamTest {
        public static void main(String[] args) throws IOException {
            String fileName = "object.txt";
    
            try(ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(fileName))){
                Data data = new Data("JM");
                objectOutputStream.writeObject(data);
            }catch (Exception e){
                e.printStackTrace();
            }
            try(ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(fileName))){
                Data data2 = (Data) inputStream.readObject();
                System.out.println(data2.getName());
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    
    class Data implements Serializable{
        private String name;
        Data(String name) {this.name = name;}
        public String getName() {return name;}
        public void setName(String name) {this.name = name;}
    }
    // 출력
    // JM

    FilterInputStream 클래스의 생성자는 protected로 선언되어 있기 때문에, 확장한 클래스에서만 객체를 생성할 수 있다. 따라서, 이 클래스를 확장한 클래스를 통해 객체를 생성한다.

    public class FilterInputStream extends InputStream {
    		protected FilterInputStream(InputStream in) {
    		        this.in = in;
        }
    		...
    }

    FilterInputStream을 확장한 클래스

    BufferedInputStream , CheckedInputStream , CipherInputStream , DataInputStream , DeflaterInputStream , DigestInputStream , InflaterInputStream , LineNumberInputStream , ProgressMonitorInputStream , PushbackInputStream



OutputStream

  • 메소드
    리턴 타입메소드 이름 및 매개 변수설명
    voidwrite(byte[] b)매개 변수로 받은 바이트 배열(b)를 저장한다.
    voidwrite(byte[] b, int off, int len)매개 변수로 받은 바이트 배열(b)의 특정 위치(off)부터 지정한 길이(len)만큼 저장한다.
    abstract voidwrite(int b)매개 변수로 받은 바이트를 저장한다. 타입은 int이지만, 실제 저장되는 것은 바이트로 저장된다.
    voidflush()버퍼에 쓰려고 대기하고 있는 데이터를 강제로 쓰도록 한다.
    voidclose()쓰기 위해 열은 스트림을 해제한다.
    public class OutputStreamExample {
        public static void main(String[] args) throws IOException {
            OutputStream output = new FileOutputStream("file.txt");
            byte b[] = {65,66,67,68,69,70};
    
            // illustrating write(nyte[] b) method
            // b 배열에서 요소 1개씩 write한다.
            output.write(b);
            // illustrating write(int b) method
            for(int i = 71; i < 75; i++){
                output.write(i);
            }
            output.close();
        }
    }
    // file.txt
    // ABCDEFGHIJ
  • 자식 클래스



Byte와 Character 스트림

stream 은 다음과 같은 타입들을 갖는다.

Byte stream 은 8-bit를 기준으로 하며, Character stream 은 16-bit를 기준으로 한다.

Byte Stream

***Stream 이라고 명시된 클래스는 Byte Stream 에 해당한다. 대표적인 클래스는 FileInputStreamFileOutputStream 이며, 클래스들은 다음과 같다.

Stream ClassDescription
InputStream스트림 Input을 정의하는 추상 클래스이다.
FileInputStream파일을 읽기 위해 사용된다.
DataInputStream자바 표준 데이터 타입을 읽기 위한 메소드들이 정의되있다.
BufferedInputStreamInputStream에 Buffer를 추가하였다.
PrintStream자주 사용되는 print() , println() 을 포함하고 있다.
OutputStream스트림 Output을 정의하는 추상 클래스이다.
FileOutputStream파일에 쓰기 위한 클래스이다.
DataOutputStreamDataOuputStream
BufferdOutputStreamOutputStream에 Buffer를 추가하였다.

Character Stream

char 기반의 문자열을 처리하기 위한 클래스이다. Byte Stream 에서의 InputStreamOutputStream 처럼, Reader , Writer 클래스가 있다.

public abstract class Reader implements Readable, Closeable {
		protected Reader() {
        this.lock = this;
    }
		...
}
public abstract class Writer implements Appendable, Closeable, Flushable {
		protected Writer() {
        this.lock = this;
    }
}

Readable 인터페이스는 CharBuffer 를 읽는 메소드를 선언한다. Appendable 인터페이스는 각종 문자열을 추가하기 위해 선언되었다. ReaderWriter 의 메소드는 InputStream , OutputStream 과 거의 유사하다. 추가된 사항들만 열거하겠다.

  • Reader
    • ready() : Reader에서 작업할 대상이 읽을 준비가 되어 있는지를 확인한다.
    • read(CharBuffer target) : 매개 변수로 넘어온 CharBuffer 클래스의 객체에 데이터를 담는다. 리턴 값은 데이터를 담은 개수다.
  • Writer
    • append(char c) : 매개변수로 넘어온 char를 추가한다.

    • append(CharSequence csq) : 매개변수로 넘어온 CharSequence를 추가한다.

      CharSequence는 인터페이스이다. 이 인터페이스를 구현한 대표적인 클래스에는 String, StringBuilder, StringBuffer가 있다. 그래서, 매개 변수로 CharSequence를 넘긴다는 것은 대부분의 문자열을 다 받아서 처리한다는 말이다.


Char Stream의 대표적인 클래스는 다음과 같다.

Stream ClassesDescription
Readercharacter stream input을 정의하는 추상 클래스이다.
Writercharacter stream output을 정의하는 추상 클래스이다.
FileReader파일을 읽는 input stream 이다.
FileWriter파일에 쓰는 output stream 이다.
BufferedReader버퍼가 추가된 input stream을 다룬다.
BufferedWriter버퍼가 추가된 output stream을 다룬다.
InputStreamReaderbyte를 char로 변환하기 위해 사용되는 input stream이다.
OutputStreamReaderbyte를 char로 변환하기 위해 사용되는 output stream이다.
PrintWriterprint() , println() 을 포함하고 있다.



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

  • System.in : 사용자의 프로그램으로 부터 입력을 받기 위한 표준 Input 클래스이다. 보통 키보드가 표준 입력 스트림으로 사용된다.
  • System.out : 사용자의 프로그램으로 부터 생성된 데이터를 출력하기 위한 표준 output 클래스이다. 보통 모니터가 표준 출력 스트림으로 사용된다.
  • System.err : 사용자의 프로그램으로 부터 발생한 에러 데이터를 출력하기 위한 표준 error 클래스이다. 보통 모니터가 표준 오류 스트림으로 사용된다.



파일 읽고 쓰기

파일에 char 기반의 내용을 쓰기 위해 FileWriter 라는 클래스를 사용하겠다. FileWriter 클래스의 생성자는 다음과 같다.

생성자설명
FileWriter(File file)File 객체를 매개 변수로 받아 객체를 생성한다.
FileWriter(Flie file, boolean append)File 객체를 매개 변수로 받아 객체를 생성한다. append 값을 통하여 해당 파일의 뒤에 붙일지(append = true), 해당 파일을 덮어 쓸지(append = false)를 정한다.
FileWriter(FileDescriptor fd)FileDescriptor 객체를 매개 변수로 받아 객체를 생성한다.
FileWriter(String fileName)지정한 문자열의 디렉토리와 파일 이름에 해당하는 객체를 생성한다.
FileWriter(String fileName)지정한 문자열의 디렉토리와 파일 이름에 해당하는 객체를 생성한다. append 값에 따라서, 데이터를 추가할지, 덮어쓸지를 정한다.

그런데, Writer에 있는 write() 메소드나 append() 메소드는 파일에 직접 접근하여 데이터를 쓰기 때문에 비효율적이다. 이를 해결하기 위해 BufferWriter 클래스를 활용한다.


생성자설명
BufferedWriter(Writer out)Writer 객체를 매개 변수로 받아 객체를 생성한다.
BufferedWriter(Writer out, int size)Writer 객체를 매개 변수로 받아 객체를 생성한다. 그리고, 두 번째 매개 변수에 있는 size를 사용하여, 버퍼의 크기를 정한다.

BufferWriter 클래스는 Writer 클래스의 인스턴스를 받아, 버퍼가 차게되면 데이터를 저장하도록 도와주는 역할을 수행한다.

public class ManageTextFile {
    public static void main(String[] args) {
        ManageTextFile manager = new ManageTextFile();
        int numberCount = 10;
        String fileName = "numbers.txt";
        manager.writeFile(fileName, numberCount);
        manager.readFile(fileName);
    }

    public void writeFile(String fileName, int numberCount){
        FileWriter fileWriter = null;
        BufferedWriter bufferedWriter = null;
        try{
            fileWriter = new FileWriter(fileName);
            bufferedWriter = new BufferedWriter(fileWriter);
            for(int loop = 0; loop < numberCount; loop++){
                bufferedWriter.write(Integer.toString(loop));
                bufferedWriter.write(" ");
            }
            System.out.println("Write success!!");
        } catch (Exception e){
            e.printStackTrace();
        }finally {
            if(bufferedWriter != null){
                try {
                    bufferedWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fileWriter != null){
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void readFile(String fileName){
        FileReader fileReader = null;
        BufferedReader bufferedReader = null;
        try{
            fileReader = new FileReader(fileName);
            bufferedReader = new BufferedReader(fileReader);
            String data;
            while ((data = bufferedReader.readLine()) != null){
                System.out.print(data + " ");
            }
            System.out.println("\nRead success!!");
        } catch (Exception e){
            e.printStackTrace();
        }finally {
            if(bufferedReader != null){
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fileReader != null){
                try {
                    fileReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
// Write success!!
// 0 1 2 3 4 5 6 7 8 9  
// Read success!!
profile
나는 사는데로 생각하지 않고, 생각하는데로 살겠다

2개의 댓글

comment-user-thumbnail
2023년 10월 14일

정리 잘하시네요.
잘 읽고갑니다.

1개의 답글