Notice
Recent Posts
Recent Comments
Link
«   2025/12   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

기록하자..

I/O | 백기선님 LIVE-STUDY 본문

자바

I/O | 백기선님 LIVE-STUDY

P23Yong 2022. 8. 6. 23:55

I/O

학습할 것

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

입출력

프로그램에서 데이터를 읽고 쓰는 작업이 빈번히 일어난다. 데이터는 사용자로부터 외부장치를 통해 입력받을 수 있고,
파일 또는 네트워크로부터 입력될 수도 있다. 반대도 마찬가지이다.

자바에서 데이터는 스트림을 통해 입출력된다.

스트림 (Stream)

Stream은 단일 방향으로 연속적으로 흐르는 것을 말한다. 데이터가 출발지에서 나와 도착지로 흐른다는 개념으로 이해하면 좋다.

프로그램이 출발지냐 도착지냐에 따라 스트림의 종류가 결정된다. 프로그램이 출발지일 경우에는 입력 스트림, 도착지일 경우에는 출력 스트림이라 부른다.

  • 입력 스트림(InputStream)
  • 출력 스트림(OutputStream)

스트림은 단방향이기 때문에 프로그램이 다른 프로그램과 통신하기 위해서는 입력 스트림과 출력 스트림 모두가 필요하다.

자바의 기본적인 데이터 입출력은 API는 java.io 패키지에서 제공하고 있다.

java.io 패키지의 주요 클래스 설명
File 파일 시스템의 정보를 얻기위한 클래스
Console 콘솔로부터 문자를 입출력하기 위한 클래스
InputStream / OutputStream 바이트 단위 입출력을 위한 최상위 입출력 스트림 클래스
FileInputStream / FileOutputStream
DataInputStream / DataOutputStream
ObjectInputStream / ObjectOutputStream
PrintStream
BufferedInputStream / BufferedOutputStream
바이트 단위 입출력을 위한 하위 클래스
Reader / Writer 문자 단위 입출력을 위한 최상위 입출력 스트림 클래스
FileReader / FileWriter
InputStreamReader / OutputStreamReader
PrintWriter
BufferedReader / BufferedWriter
문자 단위 입출력을 위한 하위 스트림 클래스

스트림은 크게 두 종류로 구분된다.

  • 바이트 기반 스트림 (Byte Stream)
  • 문자 기반 스트림 (Character Stream)

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

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

위의 하위 클래스들은 접미사로 InputStream 또는 OutputStream을 받는다. 어떠한 대상에 대해 작업을 할 것인지에 대해 해당 스트림을 선택해서 사용할 수 있다.

바이트 기반 스트림 메서드

InputStream WriteStream
int read() 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)
void close() void close()
void flush()

read의 반환 타입이 int 인 이유는 read()의 반환 범위가 -1~255 이기 때문이다.
read() 메서드는 입력 스트림으로부터 바이트를 읽을 수 없으면 -1을 반환한다.

java.io 패키지의 read(), write() 메서드는 추상 메서드로 정의되어 있는데, 실제 구현 코드를 보면 나머지 메서드들도 결국 read(), write() 메서드를 호출하고 있음을 확인할 수 있다. 따라서 read(), write() 메서드는 반드시 구현되어야 함을 알 수 있다.

  • read()
    • 입력 스트림으로부터 1바이트를 읽고 4바이트를 리턴한다. 따라서 4바이트 끝 1바이트에만 데이터가 들어가 있다.
      더이상 입력 스트림으로부터 데이터를 읽을 수 없는 경우 -1을 리턴하게 된다.
  • read(byte[] b)
    • 입력 스트림으로부터 매개변수로 주어진 값만큼을 읽고 읽은 바이트 수를 리턴한다.
      마찬가지로 입력 스트림으로부터 데이터를 읽을 수 없는 경우 -1을 리턴한다.
  • read(byte[] b, int off, int len)
    • 입력 스트림으로부터 len 만큼 바이트를 읽고 매개 값으로 주어진 b[off]부터 len개 저장한다.
      역시 입력 스트림으로부터 데이터를 읽을 수 없는 경우 -1을 리턴한다.
  • close()
    • InputStream에서 사용했던 시스템 자원을 풀어준다.

  • write(int b)
    • 매개 값으로 주어진 int 값에서 끝에 있는 1바이트만 출력 스트림으로 보낸다.
      4바이트를 모두 보내는 것 같지만, 그렇지 않다.
  • write(byte[] b)
    • 매개 값으로 주어진 바이트 배열 b를 모두 출력 스트림으로 보낸다.
  • write(byte[] b, int off, int len)
    • b[off] 부터 len개의 바이트를 출력 스트림으로 보낸다.
  • close()
    • OutputStream에서 사용했던 시스템 자원을 풀어준다.
  • flush()
    • 출력스트림은 내부에 작은 버퍼가 있는데 이를 이용해 출력되기 전 버퍼에 데이터를 쌓아두었다가 순서대로 내보낸다. flush()는 버퍼에 잔류하고 있던 데이터를 모두 출력시키고 버퍼를 비운다.

문자 기반 스트림 - Reader / Writer

Reader Writer
int read() void write(int c)
int read(byte[] b) void write(char[] cbuf)
int read(byte[] b, int off, int len) void write(char[] b, int off, int len)
write(String str)
write(String str, int off, int len)

문자 기반 스트림 역시 위의 바이트 기반 스트림과 같다.

텍스트 파일 같이, 문자 단위로 읽어들이거나 저장할 때 사용한다.

  • write(String str) | write(String str, int off, int len)
    • Writer는 만자열을 좀 더 쉽게 보내기 위해 write(String str) | write(String str, int off, int len)을 지원한다.
    • write(String str)은 문자열 전체를 출력 스트림으로 내보낸다.
    • write(String str, int off, int len)은 문자열을 off 위치부터 len만큼 출력 스트림으로 보낸다.

파일 입출력

java.io 패키지에서 제공하는 File 클래스는 파일의 정보를 얻어내는 기능과 파일 생성, 삭제 기능을 제공한다.
하지만 File 클래스는 파일을 읽고 쓰는 기능을 제공하지 않는다. 파일을 읽고 쓰기 위해서는 스트림을 사용해야 한다.

FileInputStream

FileInputStream 클래스는 파일로부터 바이트 단위로 읽어들일 때 사용하는 바이트 기반 스트림이다. 바이트 단위로 읽기 때문에 모든 종류의 파일(그림, 오디오, 비디오 등)을 읽을 수 있다.

FileInputStream fis = new FileInputStream("/Users/user/Documents/Java/java/src/fileex/hello.txt");

File file = new File("/Users/user/Documents/Java/java/src/fileex/hello.txt");
FileInputStream fis2 = new FileInputStream(file);

위와 같이 파일의 경로를 가지고 바로 FileInputStream을 생성할 수 있고, File 객체로 생성된 파일을 생성할 수도 있다.

import java.io.FileInputStream;
import java.io.IOException;

public class FileEx {

    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("/Users/user/Documents/Java/java/src/fileex/FileEx.java");

            int data;
            while ((data = fis.read()) != -1) {
                System.out.write(data);
            }
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

FileOutputStream

FileOutputStream은 바이트 단위로 데이터를 파일에 저장할 때 사용하는 바이트 기반 출력 스트림이다. FileInputStream과 마찬가지로 바이트 단위로 저장하기 때문에 모든 종류의 파일을 저장할 수 있다.

FileOutputStream fos = new FileOutputStream("/Users/user/Documents/Java/java/src/fileex/FileEx.java");

File file = new File("/Users/user/Documents/Java/java/src/fileex/FileEx.java");
FileOutputStream fos2 = new FileOutputStream(file);

위와 같이 파일의 경로를 가지고 FileInputStream을 생성할 수 있고, File 객체를 통해서도 생성할 수 있다. 주의할 점은 파일이 이미 존재하는 경우, 데이터를 출력하면 파일을 덮어쓰게 되어 기존의 내용이 사라지게 된다. 기존의 파일에 내용을 추가할 경우 FileOutputStream의 두 번째 매개변수 값을 true로 주면된다.

FileOutputStream(file, true);
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileEx {

    public static void main(String[] args) throws IOException {
        String originalFileName = "/Users/user/Documents/Java/java/src/fileex/FileEx.java";
        String targetFileName = "/Users/user/Documents/Java/java/src/fileex/hello.txt";

        FileInputStream fis = new FileInputStream(originalFileName);
        FileOutputStream fos = new FileOutputStream(targetFileName);

        int readByteNo;
        byte[] readBytes = new byte[100];

        while ((readByteNo = fis.read(readBytes)) != -1) {
            fos.write(readBytes, 0, readByteNo);
        }

        fos.flush();
        fos.close();
        fis.close();
    }
}

실제로 읽은 바이트 수를 readByteNo에 저장하고 fis.read(byte[] b) 메서드로 100바이트를 한 번에 읽어 들여 readByte에 저장한다. 마지막으로 write(byte[] b, int off, int len) 메서드를 통해 파일에 읽은 바이트를 저장하게 된다.

FileReader / FileWriter

FileReader와 FileWriter도 FileInputStream, FileOutputStream과 같다. 다른 점은 문자 기반 스트림이기 때문에 텍스트가 아닌 파일은 읽을 수 없다는 것이다.

표준 스트림

표준 스트림, 표준 입출력은 콘솔로부터의 데이터 입출력을 뜻한다.

콘솔은 시스템을 사용하기 위해 키보드로 입력을 받고 화면을 출력하는 소프트웨어를 말한다. 자바는 콘솔로부터 데이터를 입력받을 때 System.in을 사용하고 콘솔에 데이터를 출력할 때 System.out을 사용한다. 그리고 에러를 출력할 때는 System.err를 사용한다.

System.in

자바는 콘솔로부터 데이터를 입력받을 수 있도록 System 클래스의 in 정적 필드를 제공한다.
System.in은 InputStream 타입의 필드이기 때문에 다음과 같이 InputStream 변수로 참조 가능하다.

InputStream is = System.in;

키보드를 통해 어떤 키를 입력 받았는지 확인하려면 InputStream의 read() 메서드로 한 바이트를 읽으면 된다. 리턴된 int 값에는 아스키 코드가 들어가 있다.

키보드에서 입력한 문자를 그대로 얻기 위해서는 char로 타입 변환해서 얻을 수 있다.

InputStream is = System.in;
char intputChar = (char) is.read();
System.out.println(inputChar);

System.out

콘솔로 데이터를 출력하기 위해서는 System 클래스의 out 정적 필드를 사용한다. out은 PrintStream 타입의 필드이다. PrintStream은 OutpuStream의 하위 클래스이기 때문에 System.in과 마찬가지로 OutputStream 타입으로 변환해서 사용할 수 있다.

OutputStream os = System.out;

콘솔로 1개의 바이트를 출력하려면 OutputStream의 write(int b) 메서드를 이용하면 된다. 이때 int 값은 아스키 코드로 write() 메서드는 아스키 코드를 문자로 콘솔에 출력해준다.

byte b = 97;
os.write(b);
a

System클래스의 out 필드를 OutputStream으로 변환해서 콘솔에 출력하는 것보다 PrintStream의 print() 또는 println() 메서드를 사용하면 우리가 흔히 알고 있는 System.out.println()이된다.

보조 스트림

보조 스트림은 다른 스트림과 연결되어 여러 가지 편리한 기능을 제공해주는 스트림이다.
보조 스트림은 자체적으로 입출력을 수행할 수 없지만 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다.

보조 스트림을 생성할 때는 자신이 연결될 스트림을 다음과 같이 생성자의 매개값으로 받는다.

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

위와 같이 보조 스트림은 또 다른 보조 스트림에도 연결되어 스트림 체인을 구성할 수 있다. (데코레이터 패턴)

프린터 보조 스트림

PrintStream과 PrintWriter는 프린터와 유사하게 출력하는 print(), println() 메서드를 가지고 있는 보조 스트림이다. PrintStream은 바이트 출력 스트림과 연결되고, PrintWriter는 문자 출력 스트림과 연결된다.

버퍼

프로그램의 실행 성능은 입출력이 가장 낮은 장치를 따라가게 된다. CPU와 메모리의 성능이 좋아도 하드 디스크의 입출력이 늦어지면 프로그램의 성능은 입출력 장치에 맞춰진다. 이는 병목(bottleneck)현상의 원인이 되기도 한다.

이런 문제의 해결책으로 프로그램이 입출력 장치에 직접 데이터를 작업하지 않고 중간에 버퍼를 두어 실행 성능을 향상 시킬 수 있다.

위에서 flush() 메서드에서 언급했듯이, 버퍼는 일반적인 입출려과 다르게 한 곳에 저장시킨 뒤에 저장된 데이터를 한 번에 보내는 역할을 수행한다. 이는 출력 횟수를 줄여주게 된다.
-> 시스템 콜의 횟수가 줄었기 떄문에 성능상 이점이 생기는 것이다.

BufferedInputStream / BufferedReader

BufferedInputStream은 바이트 입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림이고, BufferedReader는 문자 입력 스트림에 연결되어 버퍼를 제공해주는 스트림이다.
두 스트림 모두 입력 소스로부터 자신의 내부 버퍼 크기만큼 데이터를 미리 읽고 버퍼에 저장해둔다.
그러면 프로그램은 입력 소스로부터 직접 읽지 않고 버퍼로부터 읽기 떄문에 읽기 성능이 향상된다.

BufferedOutputStream / BufferedWriter

BufferedOutputStream은 바이트 출력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림이고, BufferedWriter는 문자 출력 스트림에 연결되어 버퍼를 제공해주는 스트림이다.

위와 마찬가지로 외부의 목적지로 데이터를 직접 쓰는 것이 아니라 버퍼를 통해 한 번에 보내기 때문에 성능이 향상되는 것이다.

NIO

NIO는 기존 IO에 대한 속도 단점을 개선하기 위해 자바 4부터 새로운 입출력이라는 뜻에서 java.nio 패키지가 포함이 되었다.
자바 7로 버전업이 되면서 IO와 NIO 사이의 일관성 없는 클래스 설계를 바로 잡고 비동기 채널 등의 네트워크 지원을 대폭 강화한 NIO.2 API가 추가되었다.

IO와 NIO 차이

IO와 NIO는 데이터를 입출력한다는 목적은 동일하지만, 방식에 있어서 크게 차이가 난다.

구분 IO NIO
입출력 방식 스트림 방식 채널 방식
버퍼 방식 넌버퍼 버퍼
비동기 방식 지원 안 함 지원
블로킹/넌블로킹 방식 블로킹 방식만 지원 블로킹 / 넌블로킹 방식 모두 지원

스트림과 채널

IO는 스트림 기반, NIO는 채널 기반이다.
스트림은 입력 스트림과 출력 스트림으로 구분되어 있어서 데이터를 읽기 위해서는 입력 스트림을, 데이터를 쓰기 위해서는 출력 스트림을 생성해야 한다.

하지만 채널은 스트림과 달리 양방향으로 입력과 출력이 가능하다. 그렇기에 입출력을 위한 별도의 채널을 만들필요가 없다.

넌버퍼와 버퍼

IO에서는 출력 스트림이 1바이트를 쓰면 입력 스트림이 1바이트를 읽는다. 이런 방식은 디스크까지 데이터를 직접 쓰고 읽어오는 횟수가 많아 속도가 느리다.
이 방식보다는 버퍼를 이용해 여러 바이트를 한 번에 읽는 것이 효율적이다. 그래서 위에서 설명했듯이 IO는 BufferedInputStream, BufferedOutputStream을 사용해서 성능을 향상시킨다.

NIO는 기본적으로 버퍼를 사용한다. 채널은 버퍼에 저장된 데이터를 출력하고 입력된 데이터를 버퍼에 저장한다.

NIO는 스트림에서 읽은 데이터를 즉시 처리하는 IO와 달리 데이터를 무조건 버퍼에 저장하기 떄문에 데이터의 위치를 이동해 가면서 필요한 부분만 읽고 쓸 수 있다.

블로킹과 넌블로킹

NIO는 블로킹과 넌블로킹 방식을 모두 지원하고 있다. 블로킹이라는 것은 무언가를 막는다는 것인데, IO의 경우 입력 스트림의 read()를 호출하면 데이터가 입력되기 전까지 스레드는 블로킹(대기상태)된다. IO 스레드가 블로킹되면 블로킹을 빠져나오기 위해 인터럽트(interrupt)도 할 수 없다. 블로킹을 빠져나오는 방법은 입력 스트림을 닫는 것이다. 그런데, NIO의 블로킹은 스레드를 인터럽트함으로써 빠져나올 수 있다.

넌블로킹은 입출력 작업 시 스레드가 블로킹되지 않는 것을 말한다. NIO는 입출력 준비가 완료된 채널만 사용해서 작업 스레드가 처리하기 떄문에 작업 스레드가 블로킹되지 않는다.
NIO 넌블로킹의 핵심은 멀티플렉설인 셀렉터이다. 셀렉터는 복수 개의 채널 중에서 준비 완료된 채널을 선택하는 방법을 제공해준다. 하나의 스레드가 여러 개의 채널을 모니터링할 수 있고 이러한 특징은 다수의 스레드로 IO를 관리하는 방식에 비해 Thread switching을 줄여 성능에 이점을 준다.

참고 자료

이것이 자바다