본문 바로가기
Java/Java-basic

[Java Study 13주차] I/O

by 어썸오184 2021. 2. 18.
728x90
반응형

본 포스팅은 백기선님이 진행하시는 자바 스터디 를 진행하며 혼자 공부하고 이해한 내용을 바탕으로 정리한 글입니다. 오류나 지적 사항이 있다면 댓글로 알려주시면 감사하겠습니다.

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

스트림(Stream)

I/O란 입력(Input)과 출력(Output)을 말한다. 데이터를 밖으로 발신하는 것이 출력, 밖으로부터 데이터를 수신하는 것이 입력이다. 이때 안과 밖의 기준은 JVM이다. 입출력을 위해 자바는 java.io 패키지를 제공한다.

출처: https://techvidvan.com/tutorials/java-file-handling/

입출력을 수행하려면 안과 밖으로 데이터가 이동할 통로가 필요한데, 이를 스트림(Stream)이라고 부른다. 스트림은 단방향 통신만 가능하기 때문에 입력과 출력을 수행하기 위해서는 두 개의 스트림이 필요하다. java.io 패키지에서는 여러 종류의 스트림 클래스를 제공한다. 바이트 기반의 데이터 처리에는 InputStream, OutputStream이라는 추상 클래스를 이용하고 char 기반의 데이터 처리에는 Reader와 Writer라는 추상 클래스를 이용한다.

채널(Channel) & 버퍼(Buffer)

JDK 1.4부터는 보다 빠른 입출력을 위해 NIO(New I/O)가 추가되었다. NIO는 스트림 기반이 아니라, 버퍼(Buffer)채널(Channel) 기반으로 데이터를 처리한다.

일반적으로 NIO의 모든 I/O는 채널로 시작한다. 채널데이터를 버퍼로 읽을 수 있고, 버퍼에서 채널로 데이터를 쓸 수 있다. 채널은 스트림과 달리 양방향이다. 즉 하나의 채널로 동시에 읽고 쓰는 것이 가능하다. 또 비동기적으로 읽고 쓸 수 있다.

NIO의 버퍼는 채널과 상호작용할 때 사용된다. 커널에 의해 관리되는 시스템 메모리를 직접 사용할 수 있는 채널에 의해 직접 read 되거나 write 될 수 있는 배열과 같은 객체이다.

IO vs NIO

java.io 패키지에는File 클래스가 있다. 이 클래스는 파일 뿐만 아니라 파일의 디렉터리(path) 정보도 포함한다.

JDK 7부터 NIO2 가 등장하면서 java.nio.file 패키지의Files 클래스에서 File 클래스에 있는 메소드들 중 파일에 대한 기능을 대체하여 제공한다. path에 대한 처리는 Path/Paths로 따로 제공한다. 또 위에서 언급했듯이 NIO는 채널과 버퍼를 기반으로 데이터를 처리하기 때문에 일반적으로 스트림보다 처리 속도가 빠르다. 또 하나의 큰 특징은 비동기/논블로킹 입출력을 지원한다는 것이다.

이처럼 io 패키지를 개선한 것이 nio이지만 그럼에도 불구하고 아직까지 구버전인 java.io가 많이 쓰이기 때문에 처음 공부하는 입장에서는 익혀두는 것이 좋다. 또 nio는 io에 비해 사용하기가 조금 까다롭기 때문에 간단한 처리는 io를 사용하는 것이 낫다.

File 클래스

File 클래스는 파일 및 디렉터리 정보를 통제하기 위한 클래스이다. File 클래스는 생성한 파일 객체가 가리키고 있는 것이

  • 존재하는지
  • 파일인지 디렉터리인지
  • 읽거나, 쓰거나 실행할 수 있는지
  • 언제 수정되었는지

를 확인하는 기능과 해당 파일의

  • 이름을 바꾸고
  • 삭제하고
  • 생성하고
  • 전체 디렉터리를 확인

하는 등의 기능을 제공한다.

public class IODemo {

    public static void main(String[] args) {
        String pathName = "/Users/awesomeo184/tmp";
        File f = new File(pathName);

        System.out.println(pathName + " exists? = " + f.exists());  // false

        f.mkdir();

        System.out.println(pathName + " exists? = " + f.exists());  // true
    }

}

디렉터리를 생성하고 존재 유무를 확인하는 예제이다. mkdir() 메서드를 통해 디렉터리를 생성하기 전에는 false를, 생성하고 나서는 true를 반환한다.

매개변수로 File 객체를 받았을 때, 이것이 디렉터리인지, 파일인지 알 수 없다. 이를 확인하기 위해 제공하는 메서드도 있다.

f.isDirectory();
f.isFile();
f.isHidden();  // 숨김 파일인지 아닌지 확인하는 메서드

이번에는 새로운 파일을 생성해보자.

public class IODemo {

    public static void main(String[] args) throws IOException {
        String pathName = "/Users/awesomeo184/tmp/test.txt";
        File f = new File(pathName);

        f.createNewFile();  // 예외 처리가 필요한 메서드이므로 예외를 던져주었다.

        System.out.println("file exists? = " + f.exists());  //true
    }

}

코드를 실행하고 해당 경로로 가보면 test.txt가 생성된 것을 확인할 수 있을 것이다.

 

File 클래스는 디렉터리에 있는 목록을 살펴볼 수 있는 각종 메서드를 제공하며 이 메서드들은 list로 시작한다.

리턴타입 메서드 이름 및 매개 변수 설명
static File[] listRoots() JVM이 수행되는 OS에서 사용중인 파일시스템의 루트 디렉터리 목록을 리턴한다.
String[] list() 현재 디렉터리의 하위에 있는 목록을 리턴한다. 
String[] list(FilenameFilter filter) filter의 조건에 맞는 목록을 리턴한다.
File[] listFiles() 현재 디렉터리의 하위에 있는 목록을 리턴한다. 
File[] listFiles(FileFilter filter) filter의 조건에 맞는 목록을 리턴한다.
File[] listFiles(FilenameFilter filter) filter의 조건에 맞는 목록을 리턴한다.

FileFilter와 FilenameFilter는 추상 메서드 accept() 하나만 정의된 함수형 인터페이스이다. 원하는 조건을 구현해서 인자로 넘겨주면 조건에 맞는 파일 목록만 가져올 수 있다.

예를 들어, tmp 디렉터리의 구조가 다음과 같이 되어있을 때,

awesomeo184
    + tmp
        + test.txt
        + test1.txt
        + test2.txt
        + exclude.txt

test로 시작하는 파일의 리스트만 얻고 싶다면 다음과 같이 작성하면 된다.

public class IODemo {

    public static void main(String[] args) throws IOException {
        String pathName = "/Users/awesomeo184/tmp";
        File f = new File(pathName);

        File[] files = f.listFiles(new TestFilter());

        System.out.println("files = " + Arrays.toString(files));

    }
}

class TestFilter implements FileFilter {
    @Override
    public boolean accept(File file) {
        if (file.isFile()) {
            String fileName = file.getName();
            if (fileName.startsWith("test")) {
                return true;
            }
        }
        return false;
    }
}

실행 결과

files = [/Users/awesomeo184/tmp/test1.txt, /Users/awesomeo184/tmp/test2.txt, /Users/awesomeo184/tmp/test.txt]

InputStream & OutputStream

스트림은 바이트 기반 스트림과 문자 기반 스트림으로 나뉜다.

바이트 기반 스트림은 이미지, 오디오 파일 등 바이트코드로 구성된 파일을 전송하거나 프로세스간의 통신에서 데이터를 주고 받을 때 사용한다. 이때 사용되는 스트림은 InputStream과 OutputStream이라는 추상클래스를 상속받았으며 입출력의 대상에 맞게 사용하면 된다. 입출력 스트림에는 다음과 같은 것들이 있다.

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

InputStream과 OutputStream에는 데이터를 읽고 쓰기위한 read() 메서드와 write()메서드가 추상메서드로 선언되어 있다. 이를 상속받는 스트림들은 각자의 입출력에 맞게 메서드를 구현하고 있다.

데이터를 읽고 쓰는 방법은 모든 스트림이 거의 다 비슷하기 때문에 문자 기반 스트림에서 예시를 통해 사용법을 알아보고자 한다.

Reader & Writer

바이트기반 스트림은 입출력의 단위가 1byte이다. 그러나 java에서는 문자를 의미하는 char형이 2byte이기 때문에 문자를 다룰때 어려움이 있다. 이를 위해 제공되는 것이 문자 기반 스트림이다. 문자 기반 스트림들은 Reader와 Writer라는 추상클래스를 상속하며 다음과 같은 것들이 있다.

입력 스트림 출력 스트림
FileReader FilerWriter
CharArrayReader CharArrayWriter
PipedReader PipedWriter
StringReader StringWriter

예시를 통해 사용법을 알아보자

자바에서 char 기반의 내용을 파일로 쓰기 위해서는 FileWriter 클래스를 사용한다.

생성자 설명
FileWriter(File file) File 객체를 매개 변수로 받아 객체를 생성한다.
FileWriter(File file, boolean append) File 객체를 매개 변수로 받아 객체를 생성한다. append 값을 통하여 해당 파일의 뒤에 붙일지(append = true), 아니면 덮어쓸지를 결정한다.
FileWriter(FileDescriptor fd) FileDescriptor 객체를 매개 변수로 받아 객체를 생성한다.
FileWriter(String fileName) 지정한 문자열의 디렉터리와 파일 이름에 해당하는 객체를 생성한다.
FileWriter(String fileName, boolean append) 지정한 문자열의 디렉터리와 파일 이름에 해당하는 객체를 생성한다. append를 통해 붙일지 덮어쓸지 결정한다.

Writer에 있는 write()나 append() 메서드를 사용해서 데이터를 쓰면 메서드를 호출할 때마다 파일에 쓰기 때문에 비효율적이다. 이때 BufferedWriter라는 보조 스트림을 이용할 수 있다. 이때 보조 스트림의 생성자로 Writer 객체를 넘겨준다.

public class IODemo {

    public static void main(String[] args) throws IOException {
        String pathName = "/Users/awesomeo184/tmp/test.txt";

        FileWriter fileWriter = new FileWriter(pathName);
        BufferedWriter br = new BufferedWriter(fileWriter);

        for (int i = 0; i < 100; i++) {
            br.write(Integer.toString(i));
            br.newLine();  // 개행
        }

        br.close();
        fileWriter.close();
    }
}

test.txt

0
1
2
3
4
5
6
...
99

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

표준 입출력은 콘솔을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미한다. 자바에서는 표준 입출력을 위해 3가지 스트림을 제공하는데 이를 표준 스트림이라고 한다. 이들은 자바 어플리케이션의 실행과 동시에 사용할 수 있게 자동적으로 생성되기 때문에 개발자가 별도로 스트림을 생성하는 코드를 작성하지 않아도 된다. 내부적으로 BufferedInputStream과 BufferedOutputStream을 사용한다.

System.in, System.out, System.err은 기본 입출력 대상이 콘솔이지만 setXXX() 메서드를 통해 입출력 대상을 변경할 수 있다.

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

 

public class IODemo {

    public static void main(String[] args) {
        String pathName = "/Users/awesomeo184/tmp/test.txt";

        PrintStream ps = null;
        FileOutputStream fos = null;

        try {
            fos = new FileOutputStream(pathName);
            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");
    }
}

위 코드를 실행해보면 콘솔 창에는 Hello by System.err 밖에 출력되지 않는다. 그 이유는 System.out의 출력 결과는 test.txt에 저장되기 때문이다. 실제 파일을 열어보면 Hello by System.out 이 저장되어 있는 것을 확인할 수 있다.

 

참고자료

  • 자바의 신
  • 자바의 정석
728x90
반응형

댓글