본문 바로가기

프로그래밍 언어/자바

[Java] I/O

학습할 것 (필수)

  • 스트림 (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와 같이 보조하는 역할을 하는 스트림을 보조 스트림이라고 한다.

 

 

java에서 volatile 키워드의 의미는?

답변 : 변수의 값에 수정이 있을 경우 메인 메모리에 즉각 저장하도록 한다. 또한, 해당 변수의 값이 변경되었을 경우 다른 쓰레드들이 이를 알 수 있도록 한다.

다중 쓰레드들이 코드블럭을 병렬적으로 실행하는 것은 괜찮지만, 반드시 쓰레드들 간의 가시성을 확보해야될 때 유용하게 사용된다. 예를 들어, A라는 쓰레드는 volatile 변수의 값을 수정하고, B라는 쓰레드는 해당 값을 Read만 할 경우, A & B 쓰레드 간의 가시성을 확보할수 있다.

반면, A & B 쓰레드가 counter++ 와 같은 변수값을 수정하는 동작을 둘다 수행한다면, increment 연산이 시스템적으로 one at once하게 동작하지 않기 때문에,synchronized하지 않을 수 있다.

volatile 키워드를 붙이면 다음과 같은 효과를 준다.
- 변수의 값 수정에 대한 가시성
- 최적화를 위한 명령어 재배치 금지
- volatile 변수 수정 시, 쓰레드에게 보여지는 모든 변수 값들을 함께 메모리에 저장

그렇다면, JVM에서는 어떻게 volatile을 구현했을까?
1. 컴파일러와 런타임이 volatile 변수를 레지스터에 저장하는 것을 금지
2. 컴파일러와 옵티마이저가 code를 재배치하는 것을 비허용
3. 컴파일러와 런타임에게 volatile 변수가 수정될 경우 곧바로 메인 메모리에 저장하도록 강제
4. volatile 변수에 대해서 cache에 저장되있지 않다고 마킹해둠.

 

Buffer를 사용하는 이유는?

답변 : 입력 or 출력 값에 대해 버퍼에 저장한 이후, 시스템콜을 호출하여 한꺼번에 처리함으로써 시스템콜을 자주 호출하는 것에 대한 비효율을 줄여준다. 다만, 상대적으로 작은 양의 데이터를 여러번 다룰 때 효과적이며, 시스템콜의 명령어(ex. flush - 4KB)에서 사용하는 데이터 사이즈 하고 유사할 때 효과적이다.
Bufferd***에서 buf[]가 volatile 변수인 이유는?

답변 : 멀티스레드 환경에서도 정상적으로 작동하기 위함이다.
실행 도중, buf 변수에 다른 배열이 할당될 수 있으며, 이를 모든 스레드가 알 수있게 하기 위함이다. buf 이외에도 InputStream을 담은 in변수도 volatile로 지정되있다.(FilterInputStream에서)

 

 

 

채널

채널은 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

  • 메소드
    리턴 타입 메소드 이름 및 매개 변수 설명
    int available() 스트림에서 중단없이 읽을 수 있는 바이트의 개수를 리턴한다.
    void mark(int readlimit) 스트림의 현재 위치를 표시(mark)해 둔다. 여기서 매개 변수로 넘긴 int 값은 표시해둔 자리의 최대 유효 길이이다. 이 값을 넘어가면, 표시해 둔 자리는 더 이상 의미가 없어진다.
    void reset() 위치를 mark() 메소드가 호출되었던 위치로 되돌린다.
    boolean markSupported() mark()나 reset() 메소드가 수행 가능한지를 확인한다.
    abstract int read() 스트림에서 다음 바이트를 읽는다. 이 클래스에 유일한 추상 메소드이다.
    int read(byte[] b) 매개 변수로 넘어온 바이트 배열에 데이터를 담는다. 리턴 값은 데이터를 담은 개수다.
    int read(byte[] b, int off, int len) 매개 변수로 넘어온 바이트 배열에 특정 위치(off)부터 지정한 길이(len) 만큼의 데이터를 담는다. 리턴 값은 데이터를 담은 개수다.
    long skip(long n) 매개 변수로 넘어온 길이(n)만큼의 데이터를 건너 뛴다.
    void close() 스트림에서 작업중인 대상을 해제한다. 이 메소드를 수행한 이후에는 다른 메소드를 사용하여 데이터를 처리할 수 없다.
  • 자식 클래스
  •  
  • 위 클래스들 중 많이 사용되는 클래스는 다음과 같다.
    클래스 설명
    FileInputStream 파일을 읽는 데 사용한다. 주로 우리가 쉽게 읽을 수 있는 텍스트 파일을 읽기 위한 용도라기보다, 이미지와 같이 바이트 코드로 된 데이터를 읽을 때 사용한다.
    FilterInputStream 이 클래스는 다른 입력 스트림을 포괄하며, 단순히 InputStream 클래스가 Override되어 있다.
    ObjectInputStream ObjectOutputStream으로 저장한 데이터를 읽는데 사용한다.
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로 선언되어 있기 때문에, 확장한 클래스에서만 객체를 생성할 수 있다. 따라서, 이 클래스를 확장한 클래스를 통해 객체를 생성한다. 데코레이터 디자인 패턴이 적용된 것이다. FilterInputStream 자체만으로는 의미가 없는 클래스이지만, 데코레이터 패턴을 지원하는데 의미가 있다.
public class FilterInputStream extends InputStream {
		protected FilterInputStream(InputStream in) {
		        this.in = in;
    }
		...
}
  • FilterInputStream을 확장한 클래스

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

 

 

OutputStream

  • 메소드

  • 리턴 타입 메소드 이름 및 매개 변수 설명
    void write(byte[] b) 매개 변수로 받은 바이트 배열(b)를 저장한다.
    void write(byte[] b, int off, int len) 매개 변수로 받은 바이트 배열(b)의 특정 위치(off)부터 지정한 길이(len)만큼 저장한다.
    abstract void write(int b) 매개 변수로 받은 바이트를 저장한다. 타입은 int이지만, 실제 저장되는 것은 바이트로 저장된다.
    void flush() 버퍼에 쓰려고 대기하고 있는 데이터를 강제로 쓰도록 한다.
    void close() 쓰기 위해 열은 스트림을 해제한다.
  • 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 Class Description
InputStream 스트림 Input을 정의하는 추상 클래스이다.
FileInputStream 파일을 읽기 위해 사용된다.
DataInputStream 자바 표준 데이터 타입을 읽기 위한 메소드들이 정의되있다.
BufferedInputStream InputStream에 Buffer를 추가하였다.
PrintStream 자주 사용되는 print() , println() 을 포함하고 있다.
OutputStream 스트림 Output을 정의하는 추상 클래스이다.
FileOutputStream 파일에 쓰기 위한 클래스이다.
DataOutputStream DataOuputStream
BufferdOutputStream OutputStream에 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 Classes Description
Reader character stream input을 정의하는 추상 클래스이다.
Writer character stream output을 정의하는 추상 클래스이다.
FileReader 파일을 읽는 input stream 이다.
FileWriter 파일에 쓰는 output stream 이다.
BufferedReader 버퍼가 추가된 input stream을 다룬다.
BufferedWriter 버퍼가 추가된 output stream을 다룬다.
InputStreamReader byte를 char로 변환하기 위해 사용되는 input stream이다.
OutputStreamReader byte를 char로 변환하기 위해 사용되는 output stream이다.
PrintWriter print() , 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!!