I/O 입출력

I/O 는 Input과 Output의 약자로 입력과 출력으로, 컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터를 주고받는 것을 말한다.

데이터를 입력 받는 것을 Input, 입력받은 데이터를 내보내는 것이 Output이다.

Stream / Buffer / Channel 기반의 I/O

스트림

https://www.greenwich.com/blog/how-bilateral-streams-us-treasuries-really-work

스트림이란 데이터를 운반하는데 사용되는 연결통로로, 물이 위에서 아래로 한쪽 방향으로만 흐르듯, 스트림 역시 단방향통신만 가능하다.
(입력과 출력을 동시에 처리 할 수 없음)

먼저 보낸 데이터를 먼저 받게 되는 FIFO(First In First Out) 구조로 되어 있다. 데이터는 bit, char, byte 단위로 전달된다.

버퍼

컴퓨팅에서 버퍼는 데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리 영역
-wikipedia

채널

I/O channel은 입출력이 일어나는 동안 프로세서가 다른 일을 하지 못하는 문제를 극복하기 위해 개발된 것으로, 시스템의 프로세서와는 독립적으로 입출력만을 제어하기 위한 시스템 구성요소
cpu처럼 직접 메모리에 접근 가능하고, 자체적으로 데이터를 처리 할 수 있으며 오류 수정도 가능하다.

표준스트림

자바는 콘솔로부터 데이터를 입력받을 때 System.in 을 사용하고, 콘솔에 데이터를 출력할 때 System.out 을 사용하고, 에러를 출력할 때 System.err 을 사용한다.

  1. System.out : standard ouput stream
  2. System.in : standard input stream
  3. System.err : standard error stream

InputStream / OutputStream

데이터를 입력 받을 때 InputStream 사용하고 데이터를 출력 할 때 OutputStream을 사용한다.

InputStream

바이트 기반 입력 스트림의 최상위클래스이고, 추상 클래스이다.

모든 ByteStream은 이 클래스를 상속받아 만들어진다.

  • InputStream 기본 메소드
메소드 설명
int available() 현재 읽을 수 있는 바이트 수를 반환
void close() 현재 열려있는 InputStream을 닫음
void mark(int readlimit) InputStream에서 현재 위치를 표시 해줌
boolean markSupported() 해당 InputStream에서 mark()로 지정된 지점이 있는지에 대한 여부
abstract int read() InputStream에서 한 바이트를 읽어서 int값으로 반환
int read(byte[] b) byte[] b만큼의 데이터를 읽어서 b에 저장하고 읽은 바이트 수를 반환
int read(byte[] b, int off, int len) len 만큼 읽어서 byte[] b의 off 위치에 저장하고 읽은 바이트 수를 반환
void reset() mark()를 마지막으로 호출한 위치로 이동
long skip(long n) InputStream에서 n바이트만큼 데이터를 스킵하고 바이트 수를 반환

OutputStream

바이트기반 출력 스트림의 최상위클래스이고, 추상클래스이다.

모든 ByteStream은 이 클래스를 상속받아 만들어진다.

  • OutputStream 기본 메소드
메소드 설명
void close() OutputStream을 닫음
void flush() 버퍼에 남아있는 출력 스트림을 출력
void write(byte[] b) 버퍼의 내용 출력
void write(bute[] b, int off, int len) b배열 안에 있는 시작 off부터 len만큼 출력
abstract void write(int b) InputStream에서 한 바이트를 읽어서 int값으로 반환
int read(byte[] b) 정수 b의 하위 1바이트를 출력

IO / NIO

  • 즉, NIO는 read/write를 하나의 통로로 사용할 수 있다.
  • NIO는 버퍼를 사용하여 속도가 빠르다.
    • IO는 필터 스트림을 통해 버퍼처럼 사용 가능
  • NIO는 비동기 / non-blocking 방식 지원
  • IO는 스트림이라는 단방향 통로를 생성하여 외부 데이터와 통신한다면, NIO는 채널 이라는 양방향 통로를 생성해서 외부 데이터와 통신한다.

java.io

  • 간단한 입출력일 경우 NIO 보다 더 효율적일 수 있음
  • java.nio와 함께 병행해서 사용되고 있음

  • 바이트 스트림

    https://mainpower4309.tistory.com/18

    • 8비트의 바이트를 읽고 쓰기 위한 스트림
      • 영문 : 1byte 그외: 2byte
    • 영상이나 음악을 처리할 때 사용
    • InputStream, OutputStream 클래스를 상속 받은 하위 클래스 사용
  • 문자 스트림

    https://mainpower4309.tistory.com/18

    • 16비트 문자나 문자열을 읽고 쓰기 위한 스트림
    • Reader, Writer 클래스를 상속받은 하위 클래스들을 사용
  • Byte Streams vs Character Stream

    • Byte Stream (8bit)
      try(FileInputStream in = new FileInputStream("input.txt");
          FileOutputStream out = new FileOutputStream("output.txt")) {
          int c;
          while ((c = in.read()) != -1) {
              out.write(c);
          }
      } catch (IOException e) {
          e.printStackTrace();
      }
    
    • Character Stream (16bit)
      try(FileReader in = new FileReader("input.txt");
          FileWriter out = new FileWriter("output.txt")) {
          int c;
          while ((c = in.read()) != -1) {
              out.write(c);
          }
      } catch (IOException e) {
      	e.printStackTrace();
      }
    

    FileReader 는 한 번에 2바이트를 읽고 FileWriter는 한 번에 2바이트를 쓴다

java.nio

  • 양방향 Channel 방식을 사용해 통로가 하나만 있으면 외부 데이터와 입출력 연동이 가능하다.
  • 기본적으로 버퍼(Buffer)를 사용해 속도를 높였다
    • 커널 버퍼를 직접 사용하여 입출력 속도 향상도 가능하다
  • 비동기 지원
    • 메소드 호출 시점과 결과 출력 시점이 다르다
  • Non-Blocking 지원
    • I/O 를 수행하는 동안 스레드가 block 당하지 않도록 함

👉🏼 Java.nio.Path / Java.nio.Files 클래스

java.io 에서는 File 클래스에서 경로와 파일을 다루는 기능이 모두 포함되어 있었는데 nio 부터 분리되었다. 또한, java.io.File 클래스와도 연동하여 사용할 수 있다.

  • java.nio.file.Path 주요 메소드
    • 생성자
      import java.nio.file.Path;
      import java.nio.file.Paths;
        
      public class Test {
        
          public static void main(String[] args) {
                  Path dir1 = Paths.get("/home/yesol/temp/java/test.txt");
                  Path dir2 = Paths.get("/home", "yesol", "temp", "java", "test.txt");
                  System.out.println("dir1 = " + dir1);
                  System.out.println("dir2 = " + dir2);
          }
      }
    

    java.nio.file.Paths 클래스의 get() static 메소드를 통해 생성하고, 폴더 구조는 한번에 주나 나눠서 주나 동일하다

    • String toString() : 전체 경로 반환 (생략 가능)
    • Path getRoot() : Root 주소를 가진 Path 객체 생성
    • Path getParent() : 부모 주소를 가진 Path 객체 생성
    • Path getName(int index) : 인덱스 번호에 해당하는 주소를 가진 Path 객체 생성 (루트 다음부터 인덱스 0)
    • int getNameCount() : 루트 주소 다음부터 몇 개의 계층으로 이루어져 있는지 반환
    • Path normalize() : 정규화된 경로를 가진 Path 객체 생성

        public class App {
        	public static void main(String[] args) {
                Path dir1 = Paths.get("/home/yesol/temp/java/test.txt");
                System.out.println("전체 경로 : " + dir1);
              
                Path root = dir1.getRoot();
                System.out.println("root = " + root);
              
                Path parent = dir1.getParent();
                System.out.println("parent = " + parent);
              
                System.out.println("dir1.getNameCount() = " + dir1.getNameCount());
              
                Path name = dir1.getName(0);
                System.out.println("name = " + name);
              
                Path name2 = dir1.getName(1);
                System.out.println("name2 = " + name2);
              
                Path normal = dir1.normalize();
                System.out.println("normal = " + normal);
        	}
        }
      

    • Path resorve(String other) : 매개변수로 받은 문자열을 가진 Path 객체 생성
    • default File toFile() : java.io.File 타입으로 변환 후 반환
    • URI toUri() : Path의 경로를 URI 객체로 변환 후 반환
      public class App {
      	public static void main(String[] args) {
              Path dir = Paths.get("/home/yesol/temp/java/test.txt");
              Path dir2 = dir.resolve("/home/");
        
              System.out.println("dir2 = " + dir2);
      	}
      }
    

  • java.nio.file.Files 주요 메소드

    모두 static 메소드로 이루어져있어 별도의 인스턴스 생성이 필요 없고, 파일 또는 폴더의 주소 정보를 가진 Path 클래스의 인스턴스를 매개변수로 메소드를 수행한다.

    • boolean isDirectory(Path p) : 폴더인지 아닌지 검사
    • boolean exists(Path p) : 파일이 실제 존재하는지 검사
    • Path createDirectory(Path p) : 디렉토리 생성
    • Path createFile(Path p) : 파일 생성 (해당 파일이 이미 존재하다면 예외 발생)
      import java.io.File;
      import java.io.IOException;
      import java.nio.file.Files;
      import java.nio.file.Path;
      import java.nio.file.Paths;
        
      public class App {
      	public static void main(String[] args) {
              File file = new File("/home/yesol/temp/java/test.txt");
              // 주소 객체
              Path dir = Paths.get("/home/yesol/temp");
              // 파일명 객체
              Path file2 = Paths.get("/home/yesol/temp/java/test.txt");
              // File -> Path 변환  
              Path file3 = file.toPath();
              // Path -> File 변환							
              File file4 = file2.toFile();								
    
              try {
                  // 해당 디렉토리 없으면 생성
                  if (!Files.isDirectory(dir)) {
                      Files.createDirectories(dir);
                  }
    
                  // 해당 파일 없으면 생성
                  if (!Files.exists(file2)) {
                      Files.createFile(file2);
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              }
      	}
      }
    
    • Enum StandardCopyOption
      • copy, move 와 같은 메소드 사용시 옵션을 사용할 수 있는 기본 라이브러리의 Enum 클래스
      • 옵션은 여러개를 동시에 지정 가능
        • ATOMIC_MOVE
          • move 전용
          • 파일 이동 중 어떠한 방해가 생기더라도 이동 작업을 끝까지 보장
          • 어떤 프로세스가 중단(interrupt)를 내려도 이를 무시하고 이동을 완료한 뒤 대응
        • COPY_ATTRIBUTES
          • 모든 파일 속성(File Attributes) 복사
        • REPLACE_EXISTING
          • Dest 파일이 이미 존재하면 파일의 내용을 복사해서 덮어씀
      • 사용
        • long copy(Path source, Path dest.CopyOption)
          • source 파일을 dest 경로로 복사
            • 동일 파일 있으면 예외 발생
            • 옵션 지정하여 덮어쓰기 등 가능
        import java.io.IOException;
        import java.nio.file.Files;
        import java.nio.file.Path;
        import java.nio.file.Paths;
        import java.nio.file.StandardCopyOption;
              
        public class App {
        	public static void main(String[] args) {
                Path file = Paths.get("/home/yesol/temp/java/test.txt");
                Path file2 = Paths.get("/home/yesol/temp/java/sample.txt");
              
                try {
                    if (!Files.exists(file2)) {
                        // file2가 없으면 file을 복사해서 file2를 새로 생성
                        Files.copy(file, file2);        
                    }
              
                    // 없으면 복사해서 만들고, 이미 있으면 내용을 덮어씀
                    Files.copy(file, file2, StandardCopyOption.REPLACE_EXISTING);
              
                } catch (IOException e) {
                    e.printStackTrace();
                }
        	}
        }
      
      • Path move(Path source, Path dest.CopyOption)
        • source 파일을 dest 경로로 이동
        • source 파일이 없거나 dest 파일이 이미 존재하면 예외 발생
        • 두 경로가 동일하다면 source 파일의 이름을 dest로 변경
        • dest 파일이 있더라도 덮어쓰기하는등 CopyOption 사용 가능
          public static void main(String[] args) {
              Path file = Paths.get("/home/yesol/temp/java/test.txt");
              Path file2 = Paths.get("/home/yesol/temp/java/sample.txt");
                
              try {
                  /*
                      file(source)가 있고 file2와 경로 같으면 -> file2로 이름 변경
                      file(source)가 있고 file2와 경로 다르면 -> file2 경로로 이동
                      file(source)가 없거나 file2(dest)가 이미 있으면 -> 예외 발생
                  */
                  if (!Files.exists(file2)) {
                      Files.move(file, file2);
                  }
                
                  // 이미 존재하면 지우고 file(source)의 이름 변경
                  Files.move(file, file2, StandardCopyOption.REPLACE_EXISTING);
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
                    
        

👉🏼 채널 생성 (Channel)

  • java.io의 단방향 스트림과 달리 양방향 통로
    • 하나의 채널만 열어두면 입출력을 동시에 진행할 수 있음
  • 하드웨어, 장비, 파일, 네트워크 소켓 등과 입출력 작업을 수행할 수 있는 통로
  • 기존 스트림 방식 대비 속도가 빠름
  • non-blocking 방식을 지원하여 자원 사용의 효율성 상승
    • non-blocking : 스레드가 입출력 작업을 할 때 쓰레드를 멈추지 않고 여러 입출력 작업을 동시에 할 수 있도록 하는 것
Path input = Paths.get("/home/yesol/temp/java/test.txt");
try (FileChannel in = FileChannel.open(input, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
    // 입출력작업 수행
} catch (IOException e) {
    e.printStackTrace();
}

👉🏼java.nio.Buffer 인터페이스 상속받는 Buffer 클래스

커널 버퍼란 운영체제가 관리하는 메모리 영역에 생성되는 버퍼 공간으로, 자바는 외부데이터를 가져올때 OS의 메모리 버퍼에 먼저 담았다가 JVM 내의 버퍼에 한번 더 옮겨줘야 하기 때문에 시스템 메모리를 직접 다루는 C언어에 비해 입출력이 느리다. 이러한 단점을 개선하기 위해 나온 ByteBuffer 클래스의 allocateDirect() 메소드를 사용하면 커널 버퍼를 사용할 수 있다. 그 외로 만들어지는 버퍼는 모두 JVM 내에 생성되는 버퍼이다.

이 메소드는 내부적으로는 C언어를 호출해 시스템 메모리 영역을 사용하는 것이라 입출력 속도 자체는 빠르지만 내부적인 과정이 복잡해 버퍼 공간을 생성하고 해제하는 속도가 느려진다. 그러므로, 커널 버퍼 사용은 한번 만들어서 오래 사용해야 할 때 사용하는 것이 좋다.

public class App {
    public static void main(String[] args) {
        String str = "";

        Path input = Paths.get("C:\\Users\\jjang\\Desktop\\study\\java.txt");
        try (FileChannel in = FileChannel.open(
                input, StandardOpenOption.READ, StandardOpenOption.WRITE)){

            // 커널 버퍼 생성
            ByteBuffer buffer = ByteBuffer.allocateDirect(100);
            Charset charset = Charset.defaultCharset();

            int count = 0;

            while (count >= 0) {
                count = in.read(buffer);
                buffer.flip();
                str += charset.decode(buffer).toString();
                buffer.clear();
            }

            String str2 = "\n 파일쓰기도 동시에 가능!!";
            buffer = charset.encode(str2);
            count = in.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(str);
    }
}

✏️ java.nio 사용법 정리

  1. 채널(Channel) 생성
    • 정적(static)메소드 이용
      • open(Path p, Option)
    • java.io 클래스에서 제공하는 인스턴스 메소드 사용
      • getChannel()

    1-1. StandardOpenOption

    • 채널 생성 옵션을 가진 기본 라이브러리 Enum 클래스
    • open() 메소드를 이용한 채널 인스턴스 생성 시 옵션은 중복으로 여러개 넣어 줄 수 있음

  2. 버퍼(Buffer) 생성
    • 정적(static)메소드 이용
      • 커널 버퍼
        • ByteBuffer 클래스의 allocateDirect(int capacity)에서만 가능
      • 일반 버퍼
        • allocate(int capacity)
    • 자주 만들었다 지웠다 하는 버퍼 → 일반 버퍼 생성, 반대는 커널 버퍼 생성하는 것이 효율적
    • 채널은 파일 입출력을 버퍼에 하기 때문에, 실제 채널 메소드로 입출력을 해주는 메소드는 버퍼에다가 출력하고 버퍼에서 가져오는 작업이다

    2-1. Capacity, Position, Limit, mark

    • Capacity : 버퍼의 전체 크기 즉, 버퍼의 최대 데이터 개수(메모리 크기)를 나타낸다. 인덱스 값이 아니라 수량임
    • Position: 현재 버퍼를 쓰거나 읽을 위치, 인덱스 값이기 때문에 0부터 시작하며 lilmit보다 큰 값을 가질 수 없다. (만약 position과 limit값이 같아지면 더 이상 데이터를 쓰거나 읽을 수 없다는 의미)
    • Limit: 버퍼에서 읽거나 쓸 수 있는 위치의 한계를 나타낸다. 이 값은 capacity보다 작거나 같은 값을 가진다. 최초에 버퍼를 만들었을 때는 capacity와 같은 값을 가진다.
    • Mark : reset() 메소드를 실행했을 때 돌아오는 위치를 지정하는 인덱스를 mark() 메소드로 지정할 수 있다. 반드시 position 이하의 값으로 지정해주어야 한다. position이나 limit의 값이 mark 값보다 작은 경우 mark 값은 자동 제거된다. mark가 없는 상태에서 reset() 을 호출하면 InvalidMarkException 발생
    • 0 <= mark <= position <= limit <= capacity
    • 실제 버퍼를 읽고 쓰는 범위는 전체(Capacity) 중 Position - Limit 의 범위

  3. java.nio 파일 출력 및 Charset 클래스
    • 1 번 2번을 완료하면 (파일과 채널을 생성하고, 읽고 쓸 수 있는 버퍼 생성 완료) 파일을 읽고 쓸 수 있는 상태가 됨.
    • 그러나, 외부의 문자 데이터를 주고받을 때는 서로 다른 인코딩 타입을 사용할 수 있는 문제가 있음.
    • 예를들어,
      • 자바는 문자 인코딩 타입으로 유니코드 사용하는데 윈도우 메모장은 에 있는 파일을 읽어오는 경우 (윈도우 메모장-ANSI 코드)
      • 2byte 이상으로 이루어진 한글같은 경우
    • FileInputStream(바이트스트림)은 한글과 같은 다중 바이트 문자는 깨져서 나오고, 문자 스트림으로 파일을 읽어오면 내부로직에서 ANSI 코드를 유니코드로 변환하여 가져오기 때문에 문자를 제대로 읽을 수 있다. - nio 에서 문자 스트림 역할을 해 주는 클래스가 java.nio.Charset 클래스이다. - 사용법
    • Charset 클래스의 인스턴스 생성
      • Charset.forName("타입") : 유니코드↔ 직접입력한 타입간 변환을 해 주는 객체 생성
      • Charset.defaultCharset() : 유니코드 ↔ OS 인코딩 타입 간 변환을 해 주는 객체 생성
    • buffer = charset.encode(str); : 위에서 생성한 객체의 타입으로 인코딩해서 버퍼에 넣어줌

※ 파일 포인터의 위치를 자유 자재로 움직일 수 있으려면 RandomAccessFile 클래스로 파일을 열어줘야 한다.

동일 채널을 사용해 출력한 파일에서 다시 읽어오는 작업을 수행하려면 java.io.RandomAccessFile 클래스로 파일을 연 뒤, 채널을 생성해주면 된다.

  • getChannel() : java.io에서 연 파일에 채널 통로를 생성
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;

public class App {
    public static void main(String[] args) {

        File f = new File("C:\\Users\\jjang\\Desktop\\study\\java.txt");
        RandomAccessFile file = null;

        // 채널 열기
        try {
            /* 파일 쓰기 */
            file = new RandomAccessFile(f, "rw");
            FileChannel channel = file.getChannel();

            // Capacity가 10인 버퍼 생성
            ByteBuffer buffer = ByteBuffer.allocate(100);

            // 인코딩 타입 변환을 위한 Charset 객체 생성
            String str = "nio test!";
            Charset charset = Charset.defaultCharset();
            // 문자열 ANSI로 인코딩해서 버퍼에 넣어줌
            buffer = charset.encode(str);

            // 버퍼 내용 파일에다 쓰기
            channel.write(buffer);

            /* 파일 읽기 */
            String inputStr = "";
            file.seek(0);       // 파일 포인터를 처음으로 옮김
            file.write((byte) 'N');  // 소문자 n을 대문자로 변경
            file.seek(8);       // 마지막 글자 !위치로 이동
            file.write((byte) '$'); // !를 $로 변경
            file.seek(0);
            buffer.clear();         // 버퍼 초기화

            channel.read(buffer);   // 파일 내용 읽어서 버퍼에 저장
            buffer.flip();          // 버퍼의 Position과 Limit을 내용 범위로 변경
            inputStr = charset.decode(buffer).toString();
            System.out.println(inputStr);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                file.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

  • RandomAccessFile인스턴스.seek(long index) : 파일 포인터를 index로 옮김
  • RandomAccessFile인스턴스.write(byte b) : 현재 파일 포인터 위치에 내용을 덮어씀
    • 파일 쓰기를 하면 파일 포인터의 위치가 자동으로 이동하므로 주의해야 한다.
  • Buffer인스턴스.clear() : Position, Limit의 위치를 초기화 (Position = 0, Limit=Capacity)
  • Channel 인스턴스.read(buffer) : 현재 Position - Limit 의 크기만큼 파일을 읽어서 버퍼에 저장
  • Buffer인스턴스.flip() : 버퍼의 Limit을 현재 Position 위치로 이동시키고 Position 위치를 0으로 이동시킴
    • Position - Limit의 범위는 내용이 있는 범위만 가지게 됨
    • clear()를 사용하면 버퍼의 남는 공간만큼 공백으로 출력됨
  • Charset인스턴스.decode(buffer) : 버퍼의 내용을 디코딩해서 문자열로 변환