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

 

I/O?

Input : 입력, Output : 출력

Application(JVM) 외부의 입력을 받고, 내부의 결과를 외부로 전달하는 모든 흐름을 의미한다.

  • Scanner, InputStream을 통한 키보드, CSV 등의 포맷 파일 입력 등
  • OutputStream을 통한 Console 출력, TCP/IP를 이용한 외부 Application(JVM)에 전달 등

 

 

Stream - Java I/O?

데이터를 전달하고, 입력받을 때 필요한 단방향 통로를 말한다. 기본적인 Java I/O API에서는 입력과 출력을 위한 각각의 Stream이 필요하다.

  • 기본적으로 Buffer를 사용하지 않는 방식이며, 데이터를 받은 즉시 처리한다.
  • 데이터를 순차적으로 처리한다. (FIFO : 들어간 순서대로 나온다.)
  • Blocking 방식으로 동작한다. 스트림 호출 시 동작 스레드는 대기 상태가 된다.
  • 시스템 콜과 CPU 자원 사용, 커널에서 Direct Buffer → 다시 JVM 내부 Buffer로 메모리를 복사하는 과정이 존재하는 등 큰 오버헤드가 존재한다.

 

 

Buffer?

일반적으로 데이터가 한 위치에서 다른 위치로 이동하는 동안 일시적으로 데이터를 보유하는 데이터 구조 또는 메모리 영역을 말한다.

 

각각의 데이터가 큰 경우에 해당 영역에 버퍼 크기만큼의 데이터를 저장하고 전송함으로써 디스크와의 상호 작용 (호출) 수를 줄일 수 있다. - I/O 비용을 최소화

  • 데이터를 받는 순서대로 쌓아 보관하다 일정량이 되면 한번에 전송한다.
  • 데이터를 실시간으로 처리할 필요가 없고, 지속적으로 데이터를 넘긴다면 사용하는 것이 좋다.
  • 서버가 요청을 처리하는 시간이 받는 요청을 따라가지 못한다면, 요청이 유실될 수 있다. → 이때 요청들을 담아둘 버퍼를 이용할 수 있다. server socket 구현체에는 buffer가 포함되어 있다.

 

 

Channel - Java N I/O?

데이터가 양방향으로 전송될 수 있는 통로를 말하며, Java 4, 7에 추가된 N I/O 1, 2 API는 기본적으로 Channel과 Buffer 그리고 Selector를 통하여 기능을 제공한다.

 

셀럭터에는 채널들을 등록하고, 사용 가능한 채널을 통해서 각각의 데이터를 전송하거나 받고 이를 버퍼에 보관한다. Application은 버퍼를 통해 데이터를 처리한다.

  • "Non-blocking 방식으로도" 동작 가능하다. files API의 기능들은 Blocking으로 동작한다.
  • Blocking 방식으로 동작하는 API에 대해서 인터럽트를 던짐으로써 대기 상태에서 빠져나올 수 있다.
  • 기존 I/O 방식에서 접근할 수 없었던 Direct Buffer를 직접 참조할 수 있다. → 버퍼 내용을 복사할 필요가 없다.

 

 

InputStream과 OutputStream

바이트 기반 입출력 스트림들의 최상위 추상 클래스들이 InputStream, OutputStream이다.

 

InputStream?

입력과 관련된 API와 Closeable 인터페이스가 구현되어 있다.

바이트 기반의 입력 스트림을 구현하는데 사용되는 클래스이다.

 

하위 구현체로는

  • FileInputStream : 파일을 읽는 데 사용하는 구현체 - 텍스트, 바이트 코드 등을 읽을 때 사용한다.
  • FilterInputStream : InputStream을 재정의한 구현체 - 다른 입력 스트림을 포괄한다.
  • ObjectInputStream : ObjectOutputStream으로 저장된 데이터를 읽는 데 사용한다. → 역직렬화

등이 있다.

 

 

OutPutStream?

출력과 관련된 API와 Closeable 인터페이스가 구현되어 있다.

바이트 기반의 출력 스트림을 구현하는데 사용되는 클래스이다.

하위 구현체로는

  • FileOutputStream : 파일을 출력하는 데 사용하는 구현체
  • FilterOutputStream : OutputStream을 재정의한 구현체
  • ObjectOutputStream : Object를 Byte 기반으로 직렬 화할 때 사용한다.

등이 있다.

 

 

 

Byte와 Character 스트림

 

Byte Stream

기본적으로 사용되는 InputStream, OutputStream과 같이 1 바이트 단위를 처리하는 Stream들을 의미한다. 외부 요청을 서버가 처리하는 경우에는 이러한 데이터를 InputStreamReader를 통해 char로 변환하여 처리하게 되는데, 이 경우 한글 등으로 작성된 Unicode 기반의 정보들이 깨지는 문제가 발생하게 된다.

이를 위해 Character Stream이나 BufferdReader라는 것들이 만들어지게 되었다.

 

 

Character Stream

2 바이트 단위의 데이터를 처리하는 Stream들을 말하며, 기본적으로 Unicode charset에 맞게 데이터를 처리한다. 즉 Text 기반의 데이터를 입출력하는데 사용되는 Stream이다. JSON, HTML, XML...

 

 

보조 스트림

입출력 대상이 되는 파일이나 Binary 데이터를 직접 읽거나 쓰는 기능이 존재하지 않지만, 부가적인 기능을 추가하는 데 사용되는 Stream들을 말한다. Wrapper Stream이라고 하기도 한다.

 

이러한 구조를 가지는 구현 방식을 Decorator Pattern이라고 한다. 보조 스트림 중의 최상위는 FilterInputStream & FilterOutputStream이다.

 

하위 구현체로는

  • BufferedInputStream : 입력받는 바이트 스트림에 버퍼 기능을 제공하는 보조 스트림
  • BufferedOutputStream : 출력하는 바이트 스트림에 버퍼 기능을 제공하는 보조 스트림
  • BufferedReader : 문자 단위로 입력받는 스트림에 버퍼 기능을 제공하는 보조 스트림
  • BufferedWriter : 문자 단위로 출력하는 스트림에 버퍼 기능을 제공하는 보조 스트림
  • InputStreamReader : 입력되는 바이트 정보를 문자로 변환해주는 보조 스트림
  • OutputStreamReader : 출력되는 바이트 정보를 문자로 변환해주는 보조 스트림
  • DataInputStream : 입력되는 바이트를 자료형에 맞게 제공해주는 보조 스트림
  • DataOutputStream : 입력되는 바이트를 자료형에 맞게 출력해주는 보조 스트림

등이 있다.

 

 

 

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

 

System.in

System.in 은 Static 한 InputStream 타입의 변수이다.

JVM이 메모리로 올라오면서 사용될 하위 구현체를 인스턴스화 하고, 기본적으로 키보드의 입력을 받을 때 사용한다.

바이트 단위로 동작한다.

 

 

System.out

System.out 은 Static 한 PrintStream 타입의 변수이다.

자체적인 Buffer를 가지고 있으며, print 요청을 모아두었다가 적절한 출력 시점에 flush()를 호출한다.

 

콘솔에 특정 문자열을 출력하는 데에 주로 사용된다. 로직 Logging을 위해서 등.. 하지만

https://lob-dev.tistory.com/entry/Logging-slf4j-Logback-Framework

여기에 정리하였듯이 Application의 전체 성능에 큰 영향을 줄 수 있으므로 조심하여야 한다.

 

이는 print가 단순히 구문에 대해서만 최적화를 시도하는 반면, Logger들은 Application이 다른 요청을 처리하지 않을 때 동작하기 때문이다.

 

 

System.err

System.err 도 System.out과 마찬가지로 static 한 PrintStream 타입의 변수이다.

 

그렇기에 마찬가지로 자체적인 Buffer를 가지고 있지만, out과 다른 점은 출력 시점을 미루지 않고, 호출되는 순간마다 flush() 하기 때문에, out과 동시로 사용했을 경우 코드 흐름과 다른 결과를 낼 수 있다.

 

그 외에는 동일한 사용법을 제공한다.

 

 

 

파일 읽고 쓰기

 

텍스트 파일 읽기

 

Java 6

해당 버전에서 Text File을 쓸 때에는 FileInputStream과 InputStreamReader를 사용한다.

BufferdReader는 부가적으로 사용한다.

File file = new File("../test.txt")

BufferedReader br = null;
StringBulider sb = new StringBulider();

try {
        br = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));

        for (String line; (line = br.readLine()) != null) {
                sb.append(line);
        }
} catch (UnsupportedEncodingException exception || FileNotFoundException exception ||
                 IOExcecption exception) {
        //
} finally {
        if (br != null) {
            try {
                    br.close();
            }    catch (IOException exception) {
                    //
            }
        }
}

 

 

Java 7

해당 버전부터는 Files API의 newBufferedReader 메서드를 이용하여 구현할 수 있다.

Path path = Paths.get("../test.txt");

StringBulider sb = new StringBulider();

try (BufferedWriter br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {

        for (String line; (line = br.readLine()) != null;) {
                sb.append(line);
        }

} catch (IOException exception) {
        //
}

 

 

텍스트 파일 쓰기

 

Java 6

해당 버전에서는 FileOutputStream과 OutputStreamWriter 클래스를 사용하여 파일을 기록할 수 있다.

File file = new File("../test.txt");

BufferedWriter bw = null;

try {
        bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))

        bw.append("1");
        bw.newLine();
        bw.append("2");

} catch (UnsupportedEncodingException exception || IOExcecption exception) {
        //
} finally {
        if (bw != null) {
            try {
                    bw.close();
            }    catch (IOException exception) {
                    //
            }
        }
}

 

 

Java 7

해당 버전에서는 Files API의 newBufferedWriter() 메서드를 이용하여 구현할 수 있다.

Path path = Paths.get("../test.txt");

try (BufferedWriter bw = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {

        bw.append("1");
        bw.newLine();
        bw.append("2");

} catch (IOException exception) {
        //
}

 

 

Files API

파일과 관련된 다양한 Utils Method 들을 Static 하게 제공하는 API이다. Path 객체를 이용하여 **사용할 수 있다.

  • Files.createFile(Path...) : 파일을 만들 때 사용하는 Static Method.
  • Paths.get("test.csv").toAbsolutePath() : Path 객체를 사용하여 파일의 절대 경로를 획득.
  • Files.exists(Path...) : 파일 존재 여부를 확인하는 Static Method.
  • Files.createDirectory(Path...) : 디렉터리를 생성하는 Static Method.
  • Files.delete(Path...) : 디렉터리나 파일을 삭제하는 Static Method.
  • Files.write(Path, "text". getBytes(charset), StandardOpenOption) : 파일 쓰기 Static Method.
  • Files.copy(기존 Path, 복사할 Path, StandardCopyOption) : 파일 복사 Static Method.
  • Files.lines(Path). count() : 파일의 현재 줄 수를 가져오는 Static Method.

 

자료 출처

  • 자바 마스터북

'Live Study' 카테고리의 다른 글

Live Study_Week 10. Multithreading programming  (3) 2021.03.02
Live Study_Week 15. Lambda Expression  (0) 2021.03.01
Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11

+ Recent posts