Web/Java

[Java] I/O

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

 

 

스트림(Stream)

스트림은 바이트 단위로 데이터를 운반하는데 사용되는 연결통로 입니다.

 

스트림은 데이터의 흐름을 한쪽 방향으로 흐르는 물에 비유해서 붙여진 이름입니다.

 

즉, 스트림은 단방향 통신만 가능하기 때문에 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없습니다.

 

그래서 입력과 출력을 동시에 수행하려면 입력스트림, 출력스트림 모두 2개의 스트림의 필요합니다.

 

 

스트림은 먼저 보낸 데이터를 먼저 받게 되어 있는 큐(queue), FIFO 구조라고 보면 쉽습니다.

 

 

 

 

InputStream과 OutputStream에 정의된 읽기와 쓰기를 수행하는 메서드

InputStream OutputStream
abstract int read () abstract 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)

 

 

read()와 write(int b) (-> 추상메서드들)는 입출력 대상에 따라 알맞게 구현하라는 의미에서 추상메서드로 정의되어 있습니다.

또한, 추상메서드가 아닌 나머지 메서드들은 추상메서드를 이용해 구현이 되어있기 때문에 read()와 write(int b) (-> 추상메서드들)이 꼭 구현이 되어 있어야 합니다.

 

 

 

 

입출력 대상에 따른 입력스트림, 출력스트림의 종류

입력스트림 출력스트림 입출력 대상의 종류
FileInputStream FileOutputStream 파일
ByteArrayInputStream ByteArrayOutputStream 메모리(byte 배열)
PipedInputStream PipedOutputStream 프로세스(프로세스간의 통신)
AudioInputStream AudioOutputStream 오디오장치

 

 

 

 

 

바이트 기반 스트림

앞서 얘기한 InputStream, OutputStream은 모든 바이트 기반 스트림의 조상이며 다음과 같은 메서드가 선언되어 있습니다.

 

InputStream

메서드명 설명
int available() 스트림으로부터 읽어 올 수 있는 데이터의 크기를 반환한다.
void close() 스트림을 닫음으로써 사용하고 있던 자원을 반환한다.
void mark(int readlimit) 현재위치를 표시해 놓는다. 후에 reset()에 의해서 표시해 놓은 위치로 다시 돌아갈 수 있다. (readlimit은 되돌아갈 수 있는 byte의 수이다.)
boolean markSupported() mark()와 reset()을 지원하는지를 알려 준다. mark()와 reset()기능을 지원하는 것은 선택적이므로, mark()와 reset()을 사용하기 전에 markSupported()를 호출해서 지원여부를 확인해야 한다.
abstract int read() 1 byte를 읽어 온다(0~255사이의 값). 더 이상 읽어 올 데이터가 없으면 -1을 반환한다. abstract 메서드이기 때문에 InputStream의 자손들은 상황에 맞게 구현 해야한다.
int read(byte[] b) 배열 b의 크기만큼 읽어서 배열을 채우고 읽어 온 데이터의 수를 반환한다. 반환하는 값은 항상 배열의 크기보다 작거나 같다.
int read(byte[] b, int off, int len) 최대 len개의 byte를 읽어서, 배열 b의 지정된 위치(off)부터 저장한다. 실제로 읽어 올 수 있는 데이터가 len개보다 적을 수 있다.
void reset() 스트림에서의 위치를 마지막으로 mark()이 호출되었던 위치로 되돌린다.
long skip(long n) 스트림에서 주어진 길이(n)만큼을 건너뛴다.

 

OutputStream

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

 

 

 

ByteArrayInputStream, ByteArrayOutputStream

스트림의 종류가 달라도 읽고 쓰는 방법은 동일하므로 ByteArrayInput/OutputStream을 예제로 해보겠습니다.

 

 

 

IOEx1

바이트 배열은 메모리만 사용하므로 가비지 컬렉터에 의해 자동으로 자원이 반환됩니다.

그래서 close()를 이용해 스트림을 닫지 않아도 됩니다.

 

그리고 read()와 write(int b)사용하여 한번에 1byte씩만 읽고 쓰므로 자원 효율이 떨어집니다.

package javastudy.ch13;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;

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

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

        int data = 0;

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

        outSrc = output.toByteArray(); // 스트림의 내용을 byte 배열로 반환한다.

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

    }
}

 

 

 

 

IOEx2

byte 배열 temp를 이용해서 IOEx1과 달리 한번에 배열의 크기만큼 옮겨서 더 효율적입니다.

package javastudy.ch13;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;

public class IOEx2 {
    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 = new ByteArrayInputStream(inSrc);
        ByteArrayOutputStream output = new ByteArrayOutputStream();

        input.read(temp, 0, temp.length);
        output.write(temp, 5, 5);

        outSrc = output.toByteArray(); // 스트림의 내용을 byte 배열로 반환한다.

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

    }
}

 

 

 

IOEx3

  • available()을 이용해 blocking없이 읽어 올 수 있는 바이트의 개수를 체크해 반복합니다.
  • read()나 write()에서는 IOException이 발생할 수 있으므로 try catch문으로 묶습니다.
  • read()한 데이터의 길이만큼만 write()를 해주어서 다른 값이 write()되지 않게 막습니다.

 

package javastudy.ch13;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class IOEx3 {
    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 = new ByteArrayInputStream(inSrc);
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        
        try {
            while (input.available() > 0) {
                int len = input.read(temp);
                output.write(temp, 0, len);
            }
        } catch (IOException e) {

        }

        outSrc = output.toByteArray(); // 스트림의 내용을 byte 배열로 반환한다.

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

    }
}

 

 

read()에 byte 배열만 넣을 경우 IOException을 던지는 것을 볼 수 있습니다.

 

 

 

 

 

 

문자 스트림

문자스트림은 문자데이터를 다루는데 사용되는 스트림입니다.

 

바이트 스트림과는 사용방법은 거의 동일하지만 조상이 다릅니다.

 

조상

  • 바이트 스트림 : InputStream / OutputStream
  • 문자 스트림 : Reader / Writer

 

아래는 Reader / Writer의 메서드인데, byte배열 대신 char배열을 사용하는 것 외에는 InputStream/OutputStream과 거의 다르지 않습니다.

 

Reader

메서드명 설명
abstract void close() 입력스트림을 닫음으로써 사용하고 있던 자원을 반환한다.
void mark(int readlimit) 현재위치를 표시해 놓는다. 후에 reset()에 의해서 표시해 놓은 위치로 다시 돌아갈 수 있다. (readlimit은 되돌아갈 수 있는 byte의 수이다.)
boolean markSupported() mark()와 reset()을 지원하는지를 알려 준다. 
int read() 입력소스로부터 하나의 문자를 읽어 온다. char의 범위인 0~65535범위의 정수를 반환하며,  더 이상 읽어 올 데이터가 없으면 -1을 반환한다.
int read(char[] b) 배열 c의 크기만큼 읽어서 배열을 채우고 읽어 온 데이터의 수 또는 -1을 반환한다.
abstract int read(char[] b, int off, int len) 최대 len개의 문자를 읽어서, 배열 c의 지정된 위치(off)부터 저장한다. 읽어온 데이터의 개수 또는 -1을 반환한다.
int read(CharBuffer target) 입력소스로부터 읽어서 문자버퍼 (target)에 저장한다.
boolean ready() 입력소스로부터 데이터를 읽을 준비가 되어있는지 알려준다.
void reset() 입력소스에서의 위치를 마지막으로 mark()이 호출되었던 위치로 되돌린다.
long skip(long n) 현재 위치에서 주어진 길이(n)만큼을 건너뛴다.

 

 

 

Writer

메서드명 설명
abstract void close() 출력스트림을 닫음으로써 사용하고 있던 자원을 반환한다.
abstract void flush() 스트림의 버퍼에 있는 모든 내용을 출력소스에 쓴다. (버퍼가 있는 스트림에만 해당됨)
Writer append(char b) 지정된 문자를 출력소스에 출력한다.
Writer append(CharSequence c) 지정된 문자열(CharSequence)을 출력소스에 출력한다.
Writer append(CharSequence c, int start, int end) 지정된 문자열(CharSequence)의 일부를 출력소스에 출력 (CharBuffer, String,StringBuffer가 CharSequence를 구현)
void write(int b) 주어진 값을 출력소스에 쓴다.
void write(char[] c) 주어진 배열 c에 저장된 모든 내용을 출력소스에 쓴다.
abstract void write(char[] c, int off, int len) 주어진 배열 c에 저장된 내용 중에서 off번째부터 len길이 만큼만 출력소스에 쓴다.
void write(String str) 주어진 문자열(str)을 출력소스에 쓴다.
void write(String str, int off, int len) 주어진 문자열(str)의 일부를 출력소스에 쓴다. (off번째 문자부터 len개 만큼의 문자열)

 

 

 

문자기반 스트림은 단순히 2byte 스트림을 처리하는 것만이 아니라 인코딩 정보를 다룬다는 것이 중요합니다.

문자기반 스트림은 여러 종류의 인코딩과 자바에서 사용하는 유니코드(UTF-16)간의 변환을 자동으로 처리해줍니다.

 

test.txt

Hello, 안녕하세요!

 

package javastudy.ch13;

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

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

            int data = 0;

            while ((data = fis.read()) != -1) {
                System.out.print((char) data);
            }
            System.out.println();
            fis.close();

            while ((data = fr.read()) != -1) {
                System.out.print((char) data);
            }
            System.out.println();
            fr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

 

결과

바이트 기반 스트림인 FileInputStream과 문자 기반 스트림인 FileReader의 출력결과가 다른 것을 볼 수 있습니다.

 

문자기반 스트림은 자바에서 사용하는 UTF-16으로 인코딩 까지 해주기 때문에 한글이 깨지지 않는 것을 볼 수 있습니다.

 

 

 

 

 

 

 

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

 

표준입출력은 콘솔을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미합니다.

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

 

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

 

 

실제 System 클래스

  • in, out, err이 static 변수로 선언
  • 선언부분에선 타입이 InputStream이지만 실제로는 BufferedInputStream 인스턴스를 사용
  • 선언부분에선 타입이 PrintStream이지만 실제로는 BufferedOutputStream 인스턴스를 사용

 

 

System.in, System.out 예시코드

package javastudy.ch13;

import java.io.IOException;

public class StandardIOEx1 {
    public static void main(String[] args) {
        try {
            int input = 0;

            while ((input = System.in.read()) != -1) {
                System.out.println("input : " + input + ", (char)input : " + (char) input);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

실행화면

  1. Enter를 누르기 전까지 Blocking상태로 입력을 기다림
  2. Buffer를 사용하므로 Backspace키를 이용해 편집 가능
  3. 마지막 ^d (mac의 EOF)를 입력시 종료됨

 

 

 

 

 

 

File (파일 읽고 쓰기)

자바에서는 File클래스를 통해 파일과 디렉토리를 다룰 수 있도록 합니다.

 

File 클래스를 이용하여 해당 프로젝트의 패키지중 javastudy부터 디렉토리와 파일들을 출력하는 코드입니다.

main의 매개변수로 "/Users/dongho/Documents/java-study/src/main/java/javastudy"를 주었습니다.

$ java FileEx /Users/dongho/Documents/java-study/src/main/java/javastudy
package javastudy.ch13;

import java.io.File;
import java.util.ArrayList;

public class FileEx {
    static int totalFiles = 0;
    static int totalDirs = 0;

    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println("USAGE : java FileEx DIRECTORY");
            System.exit(0);

        }

        File dir = new File(args[0]);

        if (!dir.exists() || !dir.isDirectory()) {
            System.out.println("유효하지 않은 디렉토리입니다.");
            System.exit(0);
        }

        printFileList(dir);

        System.out.println();
        System.out.println("총 " + totalFiles + "개의 파일");
        System.out.println("총 " + totalDirs + "개의 디렉토리");


    }

    public static void printFileList(File dir) {
        System.out.println(dir.getAbsoluteFile() + " 디렉토리");
        File[] files = dir.listFiles();

        ArrayList subDir = new ArrayList();

        for (int i = 0; i < files.length; i++) {
            String filename = files[i].getName();
            if (files[i].isDirectory()) {
                filename = "[" + filename + "]";
                subDir.add(i+"");
            }
            System.out.println(filename);
        }

        int dirNum = subDir.size();
        int fileNum = files.length - dirNum;

        totalFiles += fileNum;
        totalDirs += dirNum;

        System.out.println(fileNum + "개의 파일, " + dirNum + "개의 디렉토리");
        System.out.println();

        for (int i = 0; i < subDir.size(); i++) {
            int index = Integer.parseInt((String) subDir.get(i));
            printFileList(files[index]);
        }
    }
}
/Users/dongho/Documents/java-study/src/main/java/javastudy 디렉토리
[ch9]
[ch7]
[ch6]
[ch8]
.DS_Store
[ch12]
[ch13]
[ch3]
[ch4]
[ch5]
[ch2]
[ch11]
[ch10]
1개의 파일, 12개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch9 디렉토리
FinallyTest.java
ExceptionEx1.java
ExceptionEx2.java
ExceptionEx3.java
ExceptionEx4.java
5개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch7 디렉토리
Singleton.java
Time.java
2개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch6 디렉토리
UseSuper.java
PointEx.java
DynamicMethodDispatch.java
EqualsAndHashCode.java
AbstractPlayer.java
InterfaceEx.java
6개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch8 디렉토리
Phone.java
MyInterface.java
InterfaceStaticTest.java
InterfaceExtends.java
PhoneApp.java
Galaxy.java
6개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch12 디렉토리
AnnotationEx1.java
LombokTest.java
TestInfo.java
NewClass.java
TestType.java
5개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch13 디렉토리
IOEx3.java
IOEx2.java
FileEx.java
StandardIOEx1.java
FileReaderEx1.java
FileInputOutputEx.java
IOEx1.java
7개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch3 디렉토리
AssignOperator.java
[instanceofTest]
ConditionOperator.java
ArrowOperator.java
3개의 파일, 1개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch3/instanceofTest 디렉토리
InstanceofTest.java
1개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch4 디렉토리
Case.java
[assignment1]
EnhancedFor.java
While.java
IfElseIf.java
[assignment2]
IfElse.java
5개의 파일, 2개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch4/assignment1 디렉토리
MakeDashBoard.java
README.md
2개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch4/assignment2 디렉토리
ListNodeQueue.java
ArrayQueue.java
ListNode.java
ListNodeStack.java
Stack.java
5개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch5 디렉토리
[assignment]
Tv.java
1개의 파일, 1개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch5/assignment 디렉토리
Node.java
BinaryTree.java
2개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch2 디렉토리
VariableScopeExam.java
VariableInstance.java
2개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch11 디렉토리
EnumDemo.java
Card.java
EnumEx1.java
MyEnum.java
Direction.java
5개의 파일, 0개의 디렉토리

/Users/dongho/Documents/java-study/src/main/java/javastudy/ch10 디렉토리
ThreadEx2.java
ThreadEx3.java
ThreadEx4.java
ThreadEx5.java
DeadlockEx.java
ThreadEx1.java
6개의 파일, 0개의 디렉토리


총 64개의 파일
총 16개의 디렉토리

Process finished with exit code 0

 

 

 

File 클래스는 파일의 데이터 수정하는 것이 아니라 파일의 크기, 속성, 이름 등의 정보를 얻어내는 기능과 파일의 생성 및 삭제 기능을 제공합니다.

 

 

파일을 수정하려면 InputStream, OutputStream을 사용해야합니다.

File 클래스는 파일의 데이터를 읽고 쓰는 기능은 지원하지 않는다.
파일의 입출력은 스트림을 사용해야 한다.

 

 

 

  1. File 클래스를 이용해 새 파일(FileTest1.txt)을 생성하고
  2. 기존의 test.txt파일의 데이터를 읽어와 FileTest1.txt에 써보겠습니다.

대신, 한글 인코딩이 들어가야 하기 때문에 InputStream 대신 Reader를 사용하였습니다.

package javastudy.ch13;

import java.io.*;

public class FileInputOutputEx {
    public static void main(String[] args) {
        try {
            // FileTest1.txt 생성
            File FileTest1 = new File("FileTest1.txt");
            FileTest1.createNewFile();

            // test.txt 읽어옴
            FileReader fr = new FileReader("test.txt");

            // FileTest1.txt에 데이터를 쓰기위한 인스턴스 생성
            FileWriter fw = new FileWriter("FileTest1.txt");

            // FileTest1.txt에 데이터 쓰기
            int data = 0;
            while ((data = fr.read()) != -1) {
                fw.write(data);
            }
            System.out.println();

            fr.close();
            fw.close();

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

    }

}

 

 

 

 

 

 

버퍼(Buffer)

버퍼는 byte, char, int 등 기본 데이터 타입을 저장할 수 있는 저장소로서, 제한된 크기에 순서대로 데이터를 저장합니다.

 

채널(Channel)을 통해 데이터를 주고받을때 쓰입니다.

 

 

 

 

 

채널(Channel)

스트림 처럼 데이터가 통과하는 통로이지만, 채널은 쌍방향 통로입니다.

그리고 채널에서 데이터를 주고 받을 때 사용되는 것이 버퍼 입니다.

 

출처 : jhnyang.tistory.com

 

 

 

 

 

 

 

 

IO vs NIO(New I/O)

 

버퍼를 이용한 I/O가 이용하지 않는 I/O보다 효율성이 휠씬 높습니다.

 

버퍼를 거쳐가면 더 느릴 것 같지만 왜 빠를까요?

 

물을 마실때 정수기로부터 한모금씩 여러번 먹는 것 보다, 물컵에 먹을 만큼 한번에 따라 놓고 먹는 것이 훨씬 시간이 덜 소요되는 것을 생각해보시면 이해가 빠릅니다.

 

 

 

New I/O는 자바 1.4버전에 새롭게 추가된 API 입니다.

자바 1.3 버전까지 사용해왔던 기존 IO API와는 비교가 안될 정도로 성능, 확장성 등에서 뛰어나게 설계되어 있습니다.

 

특히 *논 블로킹(Non-blocking) 입출력과 데이터 버퍼링 기능을 사용하여 기존 버전에서는 상상도 할 수 없을 정도의 뛰어난 성능을 지니고 있습니다.

 

 

*논 블로킹(Non-blocking) 입출력

I/O는 각각의 스트림에서 read()나 write()가 호출이 되면 쓰레드가 블로킹상태가 되어 작업이 끝날때까지 기다리게 됩니다. 하지만 논블로킹 상태에서는 Interrupt를 이용해여 빠져나올 수 있습니다.

 

 

구분 I/O New I/O
입출력 방식 스트림 채널
버퍼 이용 x o
비동기 방식 x o
Blocking/Non-Blocking Blocking Only 둘 다
사용 케이스 연결 클라이언트가 적고,
I/O가 큰 경우
연결 클라이언트가 많고,
I/O 처리가 작은 경우

 

 

 

 

'Web > Java' 카테고리의 다른 글

[Java] 람다식  (2) 2021.09.01
[Java] 제네릭  (1) 2021.08.30
[Java] Enum  (2) 2021.08.11
[Java] 애노테이션  (1) 2021.08.11
[Java] 멀티쓰레드 프로그래밍  (1) 2021.08.06