[Java 기초] 입출력 I/O

Updated:

이번 포스트에서는 자바에서의 입출력에 대해서 알아보도록 하겠다. 👨‍🏫

📋 목차

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

Stream, Buffer, Channel 기반의 I/O

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

Stream

스트림 : 데이터를 운반하는데 사용되는 연결통로.

  • 단반향통신만 가능하다. 입출력을 동시에 처리할 수 없다.
  • 입출력 동시 수행을 위해선 입력,출력 스트림이 필요하다.
  • 먼저보낸 데이터를 먼저받는 FIFO 구조
  • 작업을 마친 스트림은 close()로 닫아줘야 한다. $($표준 입출력, 메모리 사용 스트림의 경우 제외)

Buffer

버퍼를 사용한다는 것은 1 바이트씩 바로바로 보내는것이 아니라 버퍼공간에 데이터를 모아뒀다가 한번에 보내는 방법이다.

이로인해 입출력 횟수가 줄어들고 이는 곧 OS 레벨에 있는 시스템 콜의 횟수가 줄어드는 것을 의미하기 때문에 성능이 향상된다.

Channel

NIO$($New Input/Output) 기존 I/O의 단점을 개선시켜서 나온 java.nio 패키지이다.

NIO는 기존의 IO와 다음과 같은 차이점이 존재한다.

Channel 기반이다.

Channel은 Stream과 달리 양방향 입출력이 가능하다.

즉, 입력과 출력을 위한 각각의 채널을 만들 필요가 없다.

Buffer를 사용한다.

출력시 버퍼에 여러개의 바이트를 한번에 모았다가 한번에 출력하는 것이 기존 IO에서 1바이트를 쓰면 1바이트를 읽는 방식보다 효율적이다.

IO 또한 보조 스트림인 BufferedInputStream / BufferedOutputStream을 사용해서 버퍼를 제공하는 것이 가능하다.

NIO는 기본적으로 버퍼를 사용해서 입출력을 수행한다.

Blocking + non-blocking

NIO는 블로킹과 넌블로킹의 특징을 모두 가지고있다.

기존의 IO는 입력 스트림의 read() 혹은 출력 스트림의 write()메서드를 호출하면 데이터가 입력/출력되기 전까지 쓰레드가 블로킹$($대기상태) 되었다.

스트림을 닫아서 블로킹을 빠져나와야했다.

반면 NIO의 블로킹은 쓰레드를 interrupt해서 블로킹 상태에서 빠져나올 수 있다.

NIO의 넌블로킹은 지금 바로 읽고 쓸수 있는 상태의 채널을 선택해서 작업 쓰레드가 처리하게 하기 때문에 블로킹이 되지 않는다.

이때 채널의 선택은 Selector가 제공하는 방법을 통해 선택하게 된다.

Byte와 Character 스트림

Byte 기반 스트림 : 입출력 단위가 1 byte.

InputStream - 입력
OutputStream - 출력

Character 기반 스트림 :

Reader - 입력
Writer - 출력

Java에서 char형은 2byte이기 때문에 바이트기반 스트림으로 문자를 처리하는데는 어려움이 있다.

문자데이터를 입출력할 때는 문자기반 스트림을 사용한다.

Byte 스트림 - InputStream과 OutputStream

InputStream의 메서드

메서드명 설명
abstract int read() 1 byte를 읽어 온다. 읽어올 데이터가 없으면 -1을 반환. 추상메서드 이므로 InputStream의 자손들이 상황에 맞게 구현해야 한다.
int read(byte[ ] b) 배열 b의 크기만큼 읽어 배열을 채운다. 읽어온 데이터의 수를 반환.
int read (byte[ ] b, int off, int len) len개의 byte를 읽어 배열 b의 지정된 off위치에 저장한다.
int available() 읽어 올 수 있는 데이터 크기를 반환.
void close() 스트림을 닫고 자원을 반납.
void mark(int readlimit) 후에 reset()에 의해 돌아갈 수 있도록 현재위치를 표시.
void reset() mark()가 호출되었던 위치로 되돌린다.
long skip(long n) 스트림에서 n만큼을 건너뛴다.

OutputStream의 메서드

메서드명 설명
abstract void write(int b) 주어진 값을 출력소스에 쓴다.
void write(byte[ ] b) 배열 b에 저장된 내용을 출력소스에 쓴다.
void write(byte[ ] b, int off, int len) 배열 b에 저장된 내용 중 off번째부터 len개 만큼 읽어서 출력소스에 쓴다.
void close() 입력소스를 닫고 자원 반환.
void flush() 스트림 버퍼의 모든 내용을 출력소스에 쓴다.

예제

바구니배열$($temp)을 이용한 입출력 작업은 효율을 증가시킨다.

바이트배열$($메모리)에 데이터를 입출력하는데 쓰이는 ByteArrayInputStream을 통해 스트림을 이용한 입출력을 보여준다.

import java.io.*;
import java.util.Arrays;

class Example {
    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[10];

        ByteArrayInputStream input = null;
        ByteArrayOutputStream output = null;

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

        // input으로 부터 최대 temp.length 바이트를 읽어서 temp배열의 0번째 위치부터 저장
        input.read(temp, 0, temp.length);
        output.write(temp, 5, 5); // temp[5]부터 5개의 데이터를 쓴다.

        outSrc = output.toByteArray();

        System.out.println("Input Source    :" + Arrays.toString(inSrc));
        System.out.println("temp            :" + Arrays.toString(temp));
        System.out.println("Output Source   :" + Arrays.toString(outSrc));
    }
}

출력

Input Source    :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
temp            :[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Output Source   :[5, 6, 7, 8, 9]

FileInput과 FileOutputStream

파일에 입출력을 하기위한 스트림 생성자이다.

FileInput 생성자

생성자 설명
FileInputStream(String name) 파일이름$($name)을 가진 실제 파일과 연결된 FileInputStream을 생성.
FileInputStream(File file) 위의 String name과 같지만 파일 이름을 File 인스턴스로 지정해줘야 한다.
FileInputStream(FileDescriptor fdObj) 파일 디스크립터로 FileInputStream 생성.

FileOutput 생성자

생성자 설명
FileOutputStream(String name) 파일이름$($name)을 가진 실제 파일과 연결된 FileOutputStream을 생성.
FileOutputStream(String name, boolean append) 파일이름$($name)을 가진 실제 파일과 연결된 FileOutputStream을 생성하고 appendtrue면 기존 파일 내용의 마지막에 덧붙이고 false면 기존 파일내용을 덮어쓴다.
FileOutputStream(File file) String name과 같지만 파일 이름을 File인스턴스로 지정해줘야 한다.
FileOutputStream(File file, boolean append) $($String name, boolean append)와 같지만 파일 이름을 File인스턴스로 지정해줘야 한다.
FileOutputStream(FileDescriptor fdObj) 파일 디스크립터로 FileOutputStream을 생성.
import java.io.*;

class Example {
    public static void main(String[] args) {
        try {
            FileOutputStream fos = new FileOutputStream("example.txt");
            BufferedOutputStream bos = new BufferedOutputStream(fos, 5); // BufferedOutputStream의 버퍼 크기를 5로한다.

            // example.txt 파일에 123456789 입력.
            for(int i = '1'; i <= '9'; i++) bos.write(i);

            bos.close(); // 버퍼에 남아있던 6789 입력됨
            // fos.close()인 경우 12345 출력
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

바이트기반 보조스트림

보조스트림 자체로는 입출력을 처리하지 못하며 스트림을 생성한 다음 이를 사용해서 보조스트림을 생성해야한다.

StreamExtend

FilterInputStream과 FilterOutputStream

protected FilterInputStream$($InputStream in)
public FilterOutputStream$($OutputStream out)

  • 모든 보조스트림의 조상이다.

  • 자체로는 아무런 일도 하지 못한다.

  • 상속을 통해 메서드를 오버라이딩하여 원하는 작업을 수행.

BufferedInputStream과 BufferedOutputStream

버퍼를 사용하여 스트림의 입출력 효율을 높이는 보조스트림이다.

버퍼를 사용하면 한번에 여러 바이트를 입출력하기 때문에 효율적이다.

BufferedInputStream의 생성자

생성자 설명
BufferedInputStream(InputStream in, int size) InputStream 인스턴스를 입력소스로 하여 byte단위의 버퍼를 갖는 BufferedInputStream인스턴스를 생성.
BufferedInputStream(InputStream in) InputStream인스턴스를 입력소스로 하고 8192byte 크기의 버퍼를 갖는다.

read 메서드로 데이터를 읽게되면 BufferedInputStream은 입력소스로 부터 버퍼 크기만큼 데이터를 읽어서 내부 버퍼에 저장한다.

이후 작업시, 내부의 버퍼로 부터 데이터를 읽어오고 버퍼에 저장된 모든 데이터를 다 읽고나서 read 메서드가 호출되면 다시 BufferedInputStream이 데이터를 읽어서 버퍼에 저장하는 과정이 반복적으로 이루어진다.

BufferedOutputStream의 생성자

생성자 설명
BufferedOutputStream(OutputStrream out, int size) OutputStream인스턴스를 출력소스로하며 byte단위의 버퍼를 갖는 BufferedOutputStream 인스턴스를 생성.
BufferedOutputStream(OutputStream out) OutputStream인스턴스를 출력소스로하며 8192 byte 크기의 버퍼를 갖는다.
flush() 버퍼의 모든 내용을 출력 후, 버퍼를 비운다.
close() flush()를 호출하고 BufferedOutputStream인스턴스의 자원을 모두 반환.

write메서드를 이용한 출력이 BufferedOutputStream에 저장되고 버퍼가 가득 차면 모든 내용을 출력소스에 출력한다. 그 다음 버퍼를 비우며 과정을 반복한다.

버퍼가 가득 찰때 출력소스에 출력하기 때문에 마지막 출력부분이 버퍼에 남을수 있다.

때문에 출력작업을 마치고 나면 close()flush()를 호출해서 남은 부분을 출력되도록 해야한다. $($주로 보조스트림의 경우 close$($)만 호출.)

import java.io.*;

class Example {
    public static void main(String[] args) {
        try {
            FileOutputStream fos = new FileOutputStream("example.txt");
            BufferedOutputStream bos = new BufferedOutputStream(fos, 5); // BufferedOutputStream의 버퍼 크기를 5로한다.

            for(int i = '1'; i <= '9'; i++) bos.write(i);

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

Character 스트림 - Reader와 Writer

문자기반 스트림은

  • 2 byte로 스트림을 처리한다.

  • 여러 종류의 인코딩과 유니코드간의 변환을 자동으로 처리해준다.

    • Reader가 특정 인코딩을 읽고 유니코드로 변환.
    • Writer가 유니코드를 특정 인코딩으로 변환하여 저장.

FileReader와 FileWriter

  • 파일로부터 텍스트데이터를 읽고 쓰는데 사용.
  • FileInputStream, FileOutputStream과 사용법이 같다.
import java.io.*;

class Example {
    public static void main(String[] args) {
        try {
            String fileName = "test.txt";
            FileInputStream fis = new FileInputStream(fileName);
            FileReader fr = new FileReader(fileName);

            int data = 0;

            // FileInputStream을 통한 파일내용 읽어 화면 출력
            while((data = fis.read()) != -1) {
                System.out.print((char)data); // 문자가 깨져서 출력됨.
            }
            System.out.println();
            fis.close();

            // FileReader를 통한 파일내용 읽어 화면 출력
            while((data = fr.read()) != -1) {
                System.out.print((char)data); // 문자가 정상적으로 출력됨.
            }
            System.out.println();
            fr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

문자기반의 보조스트림

CharacterExtend

BufferedReader readLine() 메서드를 통해 파일을 라인단위로 읽음.

InputStreamReader와 OutputStreamWriter

  • 바이트기반 스트림을 문자기반 스트림으로 연결시켜준다.

  • 바이트기반 스트림의 데이터를 인코딩 문자 데이터로 변환.

InputStreamReader 생성자와 메서드

생성자 / 메서드 설명
InputStreamReader(InputStream in) OS기반 기본 인코딩 문자로 변환하는 InputStreamReader를 생성.
InputStreamReader(InputStream in, String encoding) 지정된 인코딩을 사용하는 InputStreamReader를 생성.
String getEncoding() InputStreamReader의 인코딩을 알려준다.

OutputStreamWriter 생성자와 메서드

생성자 / 메서드 설명
OutputStreamWriter(OutputStream in) OS기반 기본 인코딩 문자로 변환하는 OutputStreamWriter를 생성.
OutputStreamWriter(OutputStream in, String encoding) 지정된 인코딩을 사용하는 OutputStreamWriter를 생성.
String getEncoding() OutStreamWriter의 인코딩을 알려준다.

예제

import java.io.*;

class Example {
    public static void main(String[] args) {
        String line = "";

        try {
            InputStreamReader isr = new InputStreamReader(System.in);
            BufferedReader br = new BufferedReader(isr);

            System.out.println("현재 OS의 인코딩 :" + isr.getEncoding());

            do {
                System.out.print("문장을 입력하세요. q to finish");
                line = br.readLine();
                System.out.println("입력한 문장 :" + line);
            } while(!line.equalsIgnoreCase("q"));

            br.close();
            System.out.println("프로그램 종료");
        } catch (IOException e) { }
    }
}

표준 스트림

  • Console을 통해 데이터를 입력하고 Console로 데이터를 출력하는 것

  • Java 어플리케이션 실행시 자동적으로 생성된다. $($별도의 스트림 생성 필요 없음)

System.in 콘솔로부터 데이터를 입력
System.out 콘솔로 데이터를 출력
System.err 콘솔로 데이터를 출력

public final class System {
    public final static InputStream in = nullInputStream();
    public final static PrintStream out = nullPrintStream();
    public final static PrintStream err = nullPrintStream();
}

in, out, err의 타입이 InputStream과 PrintStream이지만 실제로는 BufferedInputStream과 BufferedOutputStream의 인스턴스를 사용한다.

표준입출력 대상 변경

메서드 설명
static void setOut(PrintStream out) System.out의 출력을 지정된 PrintStream으로 변경
static void setErr(PrintStream err) System.err의 출력을 지정된 PrintStream으로 변경
static void setIn(InputStream in) System.in의 입력을 지정한 InputStream으로 변경
import java.io.*;

class Example {
    public static void main(String[] args) {
        PrintStream ps = null;
        FileOutputStream fos = null;

        try {
            fos = new FileOutputStream("test.txt");
            ps = new PrintStream(fos);
            System.setOut(ps);
        } catch (FileNotFoundException e) {
            System.err.println("File Not Found");
        }
        System.out.println("Hello by System.out");
        System.err.println("Hello by System.err");
    }
}

파일 읽고 쓰기

File 클래스를 통해 파일과 디렉토리를 다룰 수 있다.

File인스턴스 생성만으로 파일/디렉토리가 생성되는건 아니다.

File인스턴스 생성후 출력스트림 생성 혹은 createNefFile()을 호출해야 파일/디렉토리가 생성된다.

File의 생성자와 경로 관련 메서드

생성자 / 메서드 설명
File(String fileName) 문자열$($fileName)을 이름으로 갖는 파일/디렉토리를 위한 File인스턴스를 생성. 프로그램이 실행되는 위치가 path가 된다.
File(String pathName, String fileName)
File(File pathName, String fileName)
파일의 경로와 이름을 지정하여 File인스턴스 생성.
File(URI uri) 지정된 uri로 파일을 생성
String getName() 파일 이름을 반환
String getPath() 파일 경로를 반환
String getAbsolutePath()
File getAbsouluteFile()
파일의 절대경로를 반환
String getParent()
String getParentFile()
파일의 조상 디렉토리를 반환
String getCanonicalPath()
File getCanonicalFile()
파일의 정규경로를 반환

경로와 관련된 File의 멤버변수

멤버변수 설명
static String pathSeparator OS에서 사용하는 경로 구분자. 윈도우 - “,” , 유닉스 - “.”
static char pathSeparatorChar OS에서 사용하는 경로 구분자. 윈도우 - “,” , 유닉스 - “.”
static String separator OS에서 사용하는 이름 구분자. 윈도우 - “₩”, 유닉스 - “/”
`static char separatorChar OS에서 사용하는 이름 구분자. 윈도우 - “₩”, 유닉스 - “/”

구분자가 OS마다 다르기 떄문에 OS독립적으로 프로그램을 작성하기 위해서 위의 멤버변수를 사용한다.

File f = new File("/Users/Test/jdk1.8/work/test.java"); // 해당 파일이름을 갖는 파일 생성.

File f = new File("/Users/Test/jdk1.8/work", "test.java"); // 경로와 파일 이름 지정하여 파일 생성.

File dir = new File("/Users/Test/jdk1.8/work"); // 디렉토리 생성

File f = new File(dir, "test.java");

이미 존재하는 파일을 참조하는 경우

File f = new File("/Users/Test/jdk1.8/work", "test.java");

기존에 없는 파일을 새로 생성하는 경우

File f = new File("/Users/Test/jdk1.8/work", "test.java");
f.createNewFile(); // 새로운 파일을 생성

참조
Java의 정석 $($남궁 성)
whiteship 자바 라이브 스터디

Categories:

Updated:

Leave a comment