I/O 입 출력
I/O 입출력
I/O 는 Input과 Output의 약자로 입력과 출력으로, 컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터를 주고받는 것을 말한다.
데이터를 입력 받는 것을 Input, 입력받은 데이터를 내보내는 것이 Output이다.
Stream / Buffer / Channel 기반의 I/O
스트림
스트림이란 데이터를 운반하는데 사용되는 연결통로로, 물이 위에서 아래로 한쪽 방향으로만 흐르듯, 스트림 역시 단방향통신만 가능하다.
(입력과 출력을 동시에 처리 할 수 없음)
먼저 보낸 데이터를 먼저 받게 되는 FIFO(First In First Out) 구조로 되어 있다.
데이터는 bit
, char
, byte
단위로 전달된다.
버퍼
컴퓨팅에서 버퍼는 데이터를 한 곳에서 다른 한 곳으로 전송하는 동안 일시적으로 그 데이터를 보관하는 메모리 영역
-wikipedia
채널
I/O channel은 입출력이 일어나는 동안 프로세서가 다른 일을 하지 못하는 문제를 극복하기 위해 개발된 것으로, 시스템의 프로세서와는 독립적으로 입출력만을 제어하기 위한 시스템 구성요소
cpu처럼 직접 메모리에 접근 가능하고, 자체적으로 데이터를 처리 할 수 있으며 오류 수정도 가능하다.
표준스트림
자바는 콘솔로부터 데이터를 입력받을 때 System.in 을 사용하고, 콘솔에 데이터를 출력할 때 System.out
을 사용하고, 에러를 출력할 때 System.err
을 사용한다.
- System.out : standard ouput stream
- System.in : standard input stream
- 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
클래스를 상속 받은 하위 클래스 사용
- 8비트의 바이트를 읽고 쓰기 위한 스트림
-
문자 스트림
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 파일이 이미 존재하면 파일의 내용을 복사해서 덮어씀
- ATOMIC_MOVE
- 사용
long copy(Path source, Path dest.CopyOption)
- source 파일을 dest 경로로 복사
- 동일 파일 있으면 예외 발생
- 옵션 지정하여 덮어쓰기 등 가능
- 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 사용법 정리
- 채널(Channel) 생성
- 정적(static)메소드 이용
open(Path p, Option)
- java.io 클래스에서 제공하는 인스턴스 메소드 사용
getChannel()
1-1. StandardOpenOption
- 채널 생성 옵션을 가진 기본 라이브러리 Enum 클래스
open()
메소드를 이용한 채널 인스턴스 생성 시 옵션은 중복으로 여러개 넣어 줄 수 있음
- 정적(static)메소드 이용
- 버퍼(Buffer) 생성
- 정적(static)메소드 이용
- 커널 버퍼
- ByteBuffer 클래스의
allocateDirect(int capacity)
에서만 가능
- ByteBuffer 클래스의
- 일반 버퍼
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 의 범위
- 정적(static)메소드 이용
- 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)
: 버퍼의 내용을 디코딩해서 문자열로 변환