입출력 I/O

최준호·2021년 12월 6일
0

java

목록 보기
21/25

java의 입출력(I/O)이란

입출력은 컴퓨터 내부 또는 외부 장치와 프로그램간 데이터를 주고받는 것을 말한다.

스트림(Stream)

저번에 학습했던 스트림과 같은 용어를 쓰지만 다른 개념의 입출력의 스트림이란 데이터를 운반하는데 사용되는 연결 통로이다. 물이 한쪽 방향으로만 흐르는 것과 같이 스트림은 단방향 통싱만 가능하기 때문에 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없다. 스트림은 먼저 보낸 데이터를 먼저 받게 되어 있으며 건너뜀 없이 연속적으로 데이터를 주고 받는다. Queue와 같은 FIFO 구조로 되어 있다고 생각하면 쉽다.

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

스트림은 바이트 단위로 데이터를 전송하며 입출력 대상에 따라 입출력 스트림이 있다.
FileInputStream FileOutputStream 파일
ByteArrayInputStream ByteOutputStream 메모리(byte 배열)
PipedInputStream PipedOutputStream 프로세스
AudioInputStream AudioOutputStream 오디오

또한 읽고 쓰기 위한 메서드들이 존재한다.

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

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

보조 스트림

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

FilteInputStream fis = new FileInputStream("text.txt");	//바이트 기반 FileInputStream 생성
BufferedInputStream bis = new BufferedInputStream(fis);	//보조 스트림 생성
bis.read();

보조 스트림의 종류로는

FilterInputStream FilterOutputStream 필터를 이용한 입출력 처리
BufferedInputStream BufferedOutputStream 버퍼를 이용한 입출력 성능 향상
DataInputStream DataOutputStream int, float과 같은 기본형 단위 데이터 처리
SequenceInputStream 두개의 스트림을 하나로 연결
LineNumberInputStream 읽어온 데이터의 라인 번호를 카운트(LineNumberReader로 대체됨)
ObjectInputStream ObjectOutputStream 데이터를 객체단위로 읽고 쓰는데 사용, 주로 파일을 이용하여 객체 직렬화 하는 곳에 관련 있음
PrintStream 버퍼를 이용하며, 추가적인 print 기능(print, printf, printls 메서드) 관련
pushbackInputStream 버퍼를 이용해서 읽어 온 데이터를 다시 되돌리는 기능

문자기반 스트림 - Reader, Writer

위에서 공부한 스트림은 모두 byte 기반의 스트림들이다. 문자열을 읽고 쓰는 경우 문자 기반 스트림을 사용하면 더 좋은데 InputStream, OutputStreamReader, Writer로만 변경하여 사용하면 된다. 단 ByteArrayInputStream의 경우 문자열은 CharArrayReader로 변경하여 사용하면 된다는 점만 다르다. 나머지는 보조 스트림 또한 같은 역할을 수행하므로 크게 어렵지 않다.

바이트 기반 스트림

바이트 기반 스트림의 메서드들에 대해 알아보자

InputStream과 OutputStream

InputStream method
int available() 스트림으로부터 읽어 올 수 있는 데이터의 크기 반환
void close() 스트림을 닫고 사용하던 자원을 반환
void mark(int readlimit) 현재 위치를 표시해 놓는다. 후에 reset()으로 돌아갈 수 있다.
markSupported() mark()와 reset()을 지원하는지 알려준다.
abstract int read() 1byte를 읽어 온다. 더이상 읽어 올 데이터가 없으면 -1을 반환한다.
int read(byte[] b) b 배열 크기만큼 읽어서 배열을 채우고 읽어 온 데이터의 수를 반환한다. 반환 데이터는 항상 배열보다 크거나 작다.
int read(byte[] b, int off, int len) 배열 b의 지정된 위치부터 저장한다. 실제 읽어 오는 데이터는 len보다 작거나 같다.
void reset() mark()가 호출되었던 위치로 되돌린다.
long skip(long n) 주어진 길이(n)만큼 건너 뛴다.

OutputStream method
void close() 사용하던 자원 반환
void flush() 스트림 버퍼에 있는 모든 내용을 출력소스에 쓴다.
abstract void write(int b) 주어진 값을 출력 소스에 쓴다.
void write(byte[] b) b 배열에 저장된 모든 내용을 출력 소스에 적는다.
void write(byte[] b, int off, int len) off부터 len만큼 읽어서 출력 소스에 쓴다.

flush()는 버퍼가 있는 출력 스트림의 경우에만 의미가 있으며, OutputStream에 정의된 flush()는 아무 일도 하지 않는다. 프로그램이 종료될 때, 사용하고 닫지 않은 스트림을 JVM에서 자동으로 닫아주지만, close()를 사용하여 닫아주는 것이 좋다.

ByteArrayInputStream과 ByteOutputStream

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

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

    ByteArrayInputStream input = null;
    ByteArrayOutputStream output = null;

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

    int data = 0;

    while ((data = input.read()) != -1){
        output.write(data);
    }

    outSrc = output.toByteArray();  //스트림 내용을 byte 배열로 반환
    System.out.println("input = " + Arrays.toString(inSrc));
    System.out.println("output = " + Arrays.toString(outSrc));
}

기본적으로 데이터를 읽어올 때 사용하는 방법은 아니지만 대체로 다른 스트림을 사용할 수 있는 코드여서 예시로 작성했다. 이보다 효율적으로 사용하기 위한 코드로는

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[4];  //한번에 읽어올 데이터의 크기

    ByteArrayInputStream input = null;
    ByteArrayOutputStream output = null;

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

    try{
        while (input.available() > 0){
            int len = input.read(temp);
            output.write(temp, 0, len); //temp 배열에 들어있는 데이터를 0~len까지 읽어옴.
        }
    }catch (IOException e){}

    outSrc = output.toByteArray();

    System.out.println("inSrc = " + Arrays.toString(inSrc));
    System.out.println("temp = " + Arrays.toString(temp));
    System.out.println("output = " + Arrays.toString(outSrc));
}

결과를 확인할 수 있는데. 여기서 temp의 값이 [8,9,6,7]인 것은 숫자를 크기 4의 배열만큼 읽어 올때
1 - 0,1,2,3
2 - 4,5,6,7
3 - 8,9
를 읽어오기 때문에 마지막에 8,9,6,7이 남아 있는 것이다.

FileInputStream과 FileOutputStream

파일을 입출력하기 위한 스트림으로 프로그래밍에 많이 사용된다.

바이트 기반 보조 스트림

FilterInputStream과 FilterOutputStream

Filter Stream은 상속을 통해 원하는 작업ㅇ르 수행하도록 읽고 쓰는 메서드를 오버라이딩 해야한다. Filter Stream의 자손으로는 Buffered, Data, Pushback 등이 존재한다.

BufferedInputStream과 BufferedOutputStream

스트림의 입출력 효율을 높이기 위해 버퍼를 사용하는 보조스트림이다. 한 바이트씩 입출력하는 것 보다 버퍼(byte 배열)을 통해 한번에 여러 바이트를 입출력하는 것이 빠르기 때문에 대부분 입출력 작업에 사용된다. Buffered Stream에서는 flush()를 사용하여 버퍼의 남은 모든 내용을 출력소스에 출력한 후 비우는 것이 중요하다. 하지만 프로그램 마지막에 close()를 호출해주면 자동으로 flush()를 호출하기 때문에 close()를 사용하는 것도 괜찮다.

하지만 주의해야할 점은

public static void main(String[] args){
    try {
        FileOutputStream fos = new FileOutputStream("test.txt");
        BufferedOutputStream bos = new BufferedOutputStream(fos, 5);    //버퍼 크기 5
        for(int i='1'; i <= '9'; i++){  //파일의 1~9까지 쓴다
            bos.write(i);
        }
        fos.close();
    }catch (IOException e){
        e.printStackTrace();
    }
}

다음 코드는 test.txt파일에 1~9까지 쓰는 코드이다. 버퍼를 사용하여 작성하고 fos.close()를 사용하여 닫아주었다. 하지만 결과를 확인하면

1~5까지만 적힌 것을 확인할 수 있다. 그 이유는 fos를 닫았기 때문이다. fos가 아닌 bos의 close()를 호출하여야 버퍼에 남아있던 모든 출력 소스를 출력하고 bos의 close는 자동으로 호출해준다.

public static void main(String[] args){
    try {
        FileOutputStream fos = new FileOutputStream("test.txt");
        BufferedOutputStream bos = new BufferedOutputStream(fos, 5);    //버퍼 크기 5
        for(int i='1'; i <= '9'; i++){  //파일의 1~9까지 쓴다
            bos.write(i);
        }
        bos.close();
    }catch (IOException e){
        e.printStackTrace();
    }
}

bos로 수정하여 close()를 호출한 결과를 확인하면

정상적으로 모두 입력된 것을 확인할 수 있다.

보조 스트림을 사용한 경우에는 기반 스트림의 close()나 flush()를 호출할 필요없이 단순히 보조스트림의 close()만 호출하면 된다.

DataInputStream과 DataOutputStream

위에서 공부한 바이트 기반 Stream과 문자열 Stream으로 읽어와서 형변환을 해도 되지만 Data Stream을 통해 기본 자료형 단위로 읽고 쓸 수 있는 Stream이다. DataOutputStream이 출력하는 형식은 각 기본 자료형 값을 16진수로 표현하여 저장한다. 그래서 int값을 입력하여 실제 파일에 출력해보면 int의 형태의 값들이 출력되어 있지 않고 16진수로 표현되어 있기 때문에 메모장을 열어보면 알수 없는 글씨가 적혀있을 것이다.

public static void main(String[] args){
    FileOutputStream fos = null;
    DataOutputStream dos = null;
    try{
        fos = new FileOutputStream("test.txt");
        dos = new DataOutputStream(fos);
        dos.writeInt(10);
        dos.writeFloat(20.0f);
        dos.writeBoolean(true);

        dos.close();
    }catch (IOException e){
        e.printStackTrace();
    }
}

코드를 실행하면

결과를 확인할 수 있다. 우리는 메모장으로 값을 확인할 수 없지만 코드를 작성하여 읽어와서 프로그램으로 출력해보자.

public static void main(String[] args){
    try {
        FileInputStream fis = new FileInputStream("test.txt");
        DataInputStream dis = new DataInputStream(fis);

        System.out.println(dis.readInt());
        System.out.println(dis.readFloat());
        System.out.println(dis.readBoolean());
        dis.close();
    }catch (IOException e){
        e.printStackTrace();
    }
}

우리가 작성한 내용을 확인할 수 있다. 데이터를 읽어올 때 주의해야할 것은 예제와 같이 여러 종류의 자료형으로 파일을 작성 했을 때 읽을 때는 반드시 작성한 자료형의 순서대로 읽어야한다는 것이다.

SequenceInputStream

여러개의 입력 스트림을 연속적으로 연결해서 하나의 스트림으로부터 데이터를 읽는 것과 같이 처리할 수 있도록 도와준다. 큰 파일을 여러개 작은 파일로 나누었다가 하나의 파일로 합치는 것과 같은 작업을 수행할 때 사용하면 좋을 것이다. Vector에 연결할 입력 스트림들을 저장한 다음 Vector의 Eunmeration elements()를 호출해서 생성자의 매개변수로 사용한다.

public static void main(String[] args){
    byte[] arr1 = {0,1,2};
    byte[] arr2 = {3,4,5};
    byte[] arr3 = {6,7,8};
    byte[] outSrc = null;

    Vector v = new Vector();
    v.add(new ByteArrayInputStream(arr1));
    v.add(new ByteArrayInputStream(arr2));
    v.add(new ByteArrayInputStream(arr3));

    SequenceInputStream input = new SequenceInputStream(v.elements());
    ByteArrayOutputStream output = new ByteArrayOutputStream();

    int data = 0;

    try{
        while ((data = input.read()) != -1){
            output.write(data);
        }
    }catch (IOException e) {
        e.printStackTrace();
    }
    outSrc = output.toByteArray();

    System.out.println(Arrays.toString(arr1));
    System.out.println(Arrays.toString(arr2));
    System.out.println(Arrays.toString(arr3));
    System.out.println(Arrays.toString(outSrc));
}

다음과 같이 여러개의 입력을 하나의 스트림으로 변경하여 출력할 수 있다.

PrintStream

PrintStream은 데이터를 기반 스트림에 다양한 형태로 출력할 수 있는 print, println, printf 메서드를 오버로딩하여 제공한다. 하지만 PrintWriter가 JDK1.1 부터 추가되었는데 PrintStream에 비해 다양한 언어의 문자를 처리하는데 적합하기 때문에 가능하면 PrintWriter를 사용하는 것이 좋다.

문자 기반 스트림

문자 데이터를 다루는데 사용된다는 것을 제외하고는 바이트기반 스트림과 사용방법은 거의 동일하다.

Reader와 Writer

바이트 기반 스트림의 조상이 InputStream/OutputStream인 것과 같이 문자기반의 스트림에서는 Reader/Writer가 그 역할이다. 바이트 기반의 스트림과 메서드도 크게 다르지 않은데 byte 배열 대신 cahr 배열을 사용한다는 점만 기억하자.

FileReader와 FileWriter

FileReader/FileWriter는 파일로부터 텍스트 데이터를 읽고 쓰는데 사용된다. 사용법은 FileInputStream과 FileOutputStream과 다르지 않으므로 자세한 설명은 생략한다.

PipeReader와 PipeWriter

쓰레드 간 데이터를 주고 받을 때 사용된다. 스트림을 생성한 다음에 어느 한쪽 쓰레드에서 connect()를 호출해서 입력스트림과 출력스트림을 연결한다. 입출력을 마친 후에는 어느 한쪽 스트림만 닫아도 나머지 스트림은 자동으로 닫힌다.

문자 기반 보조 스트림

BufferedReader와 BufferedWriter

BufferedReader와 BufferedWriter는 버퍼를 이용해서 입출력의 효율을 높일 수 있도록 해주는 역할을 한다. BufferedReader의 readLine()을 사용하면 데이터를 라인단위로 읽을 수 있고 BufferedWriter는 newLine()으로 줄바꿈을 해주는 메서드를 가지고 있다.

InputStreamReader와 OutputStreamWriter

바이트 기반 스트림을 문자 기반 스트림으로 연결시켜주는 역할을 한다. 그리고 바이트 기반 스트림의 데이터를 지정된 인코딩의 문자 데이터로 변환하는 작업을 수핸한다.

String getEncoding() InputStreamReader의 인코딩을 알려준다.

표준 입출력과 File

표준 입출력 - System.in, System.out, System.err

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

System.in 콘솔로부터 데이터를 입력받는데 사용
System.out 콘솔로부터 데이터를 출력하는데 사용
System.err 콘솔로부터 에러를 출력하는데 사용

표준 입출력 대상변경 - setOut(), setErr(), setIn()

초기에는 in,out,err의 입출력 대상이 콘솔화면이지만 in,out,err를 사용하면 입출력을 콘솔 이외에 다른 입출력 대상으로 변경하는 것이 가능하다. 하지만 JDK 1.5 이후로는 Scanner 클래스가 제공되면서 System.in으로부터 데이터를 입력받아 작업하는 것이 편리해졌다.

RandomAccessFile

자바에서는 입력과 출력이 각각 분리되어 별도로 작업을 하도록 설계되어 있는데, RandomAccessFile만은 하나의 클래스로 파일에 대한 입출력을 모두 할 수 있도록 되어 있다. DataInput 인터페이스와 DataOutput 인터페이스를 모두 구현했기 때문에 읽기와 쓰기가 모두 가능하다. 사실 DataInputStream은 DataInput 인터페이스를, DataOutputStream은 DataOutput 인터페이스를 구현했다. 기본 자료형을 읽고 쓰기 위한 메서드들은 모두 이 2개의 인터페이스에 의해 정의되어 있는 것이다. RadnomAccessFile 클래스도 기본자료형 단위로 데이터를 읽고 쓸 수 있고 가장 큰 장점은 파일의 어느 위치에나 읽기/쓰기가 동시에 가능하다는 것이다. 다른 입출력 클래스들은 입출력 소스에 순차적으로 읽기/쓰기를 하기 때문에 제한적인데 반해 RandomAccessFile 클래스는 제한이 없다.

File

파일은 가장 기본적이면서 많이 사용되는 입출력 대상이기 때문에 중요하다.
File(String fileName) fileName은 주로 경로를 포함해서 지정해주지만, 파일 이름만 사용해도 된다. 이 경우 프로그램이 실행되는 이치가 경로로 간주된다.
File(String pathName, String fileName)
File(File pathName, String fileName) 두 메서드 모두 파일의 경로와 이름을 따로 분리해서 지정할 수 있도록한 생성자이다.
File(URI uri) uri로 파일 생성
String getName() 파일이름 반환
String getPath() 파일경로 반환
String getAbsolutePath() 파일 절대경로 String 반환
File getAbsoluteFile() 파일 절대경로 File 반환
String getParent() 조상 디렉터리 String 반환
File getParent() 조상 디렉터리 File 반환
String getCanonicalPath() 정규경로를 String 반환
File getCanonicalPath() 정규경로를 File 반환

static String pathSeparator OS에서 사용하는 경로 구분자. 윈도우, 유닉스:
static char pathSeparatorChar 위 동일한 char 형태
static String separator OS에서 사용되는 이름 구분자. 윈도우\ 유닉스 /
static char separator 위 동일한 char 형태

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글