✔ 파일 시스템
NIO란?
- 자바 4에서 java.io 패키지의 성능을 보강하기 위한 New IO라는 NIO API가 java.nio 패키지로 묶여서 추가됨
- 그리고 그 뒤를 이어서 자바 7에서 NIO.2라고 불리는 API가 java.nio.file 패키지로 묶여서 추가됨
기본적인 파일 시스템
- 윈도우의 파일 경로는 \ 방식으로 나타내지만 리눅스의 파일 경로는 / 방식으로 나타냄
- 윈도우는 여러 개의 루트 디렉토리(최상위 디렉토리)를 가지지만, 리눅스는 하나의 루트 티렉토리만 가짐
- 경로에는 파일 또는 디렉토리의 위치를 루트 디렉토리를 기준으로 위치를 표현하는 절대 경로와
파일 또는 디렉토리의 위치를 현재 디렉토리를 기준으로 위치를 표현하는 상대 경로가 존재
Paths와 Path 클래스
- 결함이 있던 java.nio.file.Path 클래스를 대체하기 위해 정의된 Paths 인터페이스
- 경로를 표현하기 위한 인터페이스로 Paths.get 메소드는 경로 정보를 담은 인스턴스를 반환
Path pt1 = Paths.get("C:\\JavaStudy\\PathDemo.java"); // 절대 경로
Path pt2 = pt1.getRoot();
Path pt3 = pt1.getParent();
Path pt4 = pt1.getFileName();
Path cur = Paths.get(""); // 상대 경로
String cdir;
if(cur.isAbsolute()) // 정보가 절대 경로인지 판단
cdir = cur.toString();
else
cdir = cur.toAbsolutePath().toString(); // 절대 경로 정보로 수정된 인스턴스 생성 및 반환
파일 및 디렉토리의 생성과 소멸
- 파일 또는 디렉토리 정보를 담은 인스턴스를 생성했다면, 해당 파일 또는 디렉토리의 생성을 명령할 수 있음
- 이때 이들 각각에 대해 속성과 권한을 부여할 수 있음
public static Path createFile(Path path, FileAttribute<?>...attrs) throws IOException
public static Path createDirectory(Path dir, FileAttribute<?>...attrs) throws IOException
public static Path createDirectories(Path dir, FileAttribute<?>...attrs) throws IOException
Path fp = Paths.get("C:\\JavaStudy\\empty.txt"); // 파일 생성 (empty.txt)
fp = Files.createFile(fp);
Path dp1 = Paths.get("C:\\JavaStudy\\Empty"); // 디렉토리 생성 (Empty)
dp1 = Files.createDirectory(dp1);
Path dp2 = Paths.get("C:\\JavaStudy2\\Empty"); // 경로의 모든 디렉토리 생성 (JavaStudy2, Empty)
dp2 = Files.createDirectories(dp2);
파일을 대상으로 하는 간단한 입력 및 출력
- 빈 파일을 생성한 후 입출력할 데이터의 양이 적고 성능이 문제 되지 않는 경우 파일에 데이터를 읽고 쓸 수 있음
- 물론 I/O 스트림을 기반으로 입출력을 진행하여도 됨
- 문장을 전달할 때의 StandardOpenOption 옵션들이 존재하며
하나도 전달하지 않으면 CREATE, TRUNCATE_EXISTING이 기본으로 지정됨
- APPEND : 파일의 끝에 데이터를 추가
- CREATE : 파일이 존재하지 않으면 생성
- CREATE_NEW : 새 파일을 생성, 이미 파일이 존재하면 예외 발생
- TRUNCATE_EXISTING : 쓰기 위해 파일을 여는데 파일이 존재하면 파일의 내용을 덮어씀
public static byte[] readAllBytes(Path path) throws IOException // 바이트 데이터 입력
public static Path write(Path path, byte[] bytes, OpenOption...options) throws IOException // 바이트 데이터 출력
public static List<String> readAllLines(Path path) throws IOException // 문자 데이터 입력
public static Path write(Path path, Iterable<? extends CharSequence> lines, OpenOption...options) throws IOException // 문자 데이터 출력
Path fp = Paths.get("C:\\JavaStudy\\simple.bin");
fp = Files.createFile(fp);
byte buf1[] = {0x13, 0x14, 0x15};
Files.write(fp, buf1, StandardOpenOption.APPEND); // 파일에 바이트 데이터 쓰기
byte buf2[] = Files.readAllBytes(fp); // 파일로부터 바이트 데이터 읽기
Path fp = Paths.get("C:\\JavaStudy\\simple.txt");
String st1 = ("One Simple String");
String st2 = ("Two Simple String");
List<String> lst1 = Arrays.asList(st1, st2);
Files.write(fp, lst1); // 파일에 문자 데이터 쓰기 (옵션이 전달되지 않았으므로 CREATE, TRUNCATE_EXISTING)
List<String> lst2 = Files.readAllLines(fp); // 파일로부터 문자 데이터 읽기
파일 및 디렉토리의 복사와 이동
- 파일 및 디렉토리의 복사와 이동을 위한 메소드가 존재
- 복사를 할 때의 StandardCopyOption 옵션들이 존재
- REPLACE_EXISTING : 이미 파일이 존재한다면 해당 파일을 대체
- COPY_ATTRIBUTES : 파일의 속성까지 복사
public static Path copy(Path source, Path target, CopyOption...options) throws IOException // 복사
public static Path move(Path source, Path target, CopyOption...options) throws IOException // 이동
Path src = Paths.get("C:\\JavaStudy\\CopyFileFremFiles.java");
Path dst = Paths.get("C:\\JavaStudy\\CopyFileFremFiles2.java");
Files.copy(src, dst, StandardCopyOptions.REPLACE_EXISTING);
✔ NIO.2 기반의 I/O 스트림 생성
바이트 스트림의 생성
- NIO.2의 Files 클래스에서는 스트림의 생성을 위한 메소드를 별도로 제공하고 있어 더욱 간단하게 바이트 스트림을 생성
public static void main(String[] args) {
Path fp = Paths.get("data.dat")
// 이전 I/O 스트림 : try(DataOutputStream out = new DataOutputStream(new FileOutputStream("data.dat")))
try(DataOutputStream out = new DataOutputStream(Files.newOutputStream(fp))) {
out.writeInt(370);
out.writeDouble(3.14);
}
catch(IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Path fp = Paths.get("data.dat")
// 이전 I/O 스트림 : try(DataInputStream in = new DataInputStream(new FileInputStream("data.dat")))
try(DataInputStream in = new DataInputStream(Files.newInputStream(fp))) {
int num1 = in.readInt();
double num2 = int.readDouble();
}
catch(IOException e) {
e.printStackTrace();
}
}
문자 스트림의 생성
- NIO.2의 Files 클래스에서는 스트림의 생성을 위한 메소드를 별도로 제공하고 있어 더욱 간단하게 문자 스트림을 생성
public static void main(String[] args) {
String ks = "공부를 한다.");
Path fp = Paths.get("data.txt");
// 이전 I/O 스트림 : try(BufferedWriter bw = new BufferedWriter(new FilterWriter("data.txt")))
try(BufferedWriter bw = Files.newBufferedWriter(fp)) {
bw.write(ks, 0, ks.length());
bw.newLine(); // 줄 바꿈
bw.write(ks, 0, ks.length());
}
catch(IOException) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Path fp = Paths.get("data.txt");
// 이전 I/O 스트림 : try(BufferedReader br = new BufferedReader(new FilterReader("data.txt")))
try(BufferedReader br = Files.newBufferedReader(fp)) {
String str;
while(true) {
str = br.readLine(); // 한 문장 읽기
if(str == null)
break;
System.out.println(str);
}
}
catch(IOException) {
e.printStackTrace();
}
}
✔ NIO 기반의 입출력
NIO의 채널과 버퍼
- NIO는 입출력의 성능을 개선하기 위해 스트림을 대신해서 채널이라는 것을 생성함
- 채널도 스트림처럼 데이터의 입력 및 출력을 위한 통로가 됨
- 스트림은 한 방향으로만 데이터가 이동하지만 채널은 양방향으로 데이터 이동이 가능함
- 그러므로 스트림은 입력 스트림과 출력 스트림이 구분되므로 쓰면서 동시에 읽는 것도 가능한 스트림을 생성할 수 없지만
채널은 하나의 채널을 대상으로 읽고 쓰는 것이 가능함
- 반면 채널은 반드시 버퍼에 연결해서 사용해야 한다는 제약사항이 존재함
public static void main(String[] args) {
// 파일 복사를 위한 두 개의 파일
Path src = Paths.get("data.txt");
Path dst = Paths.get("newData.txt");
// 하나의 버퍼 생성
ByteBuffer buf = ByteBuffer.allocate(1024);
// 두 개의 채널 생성
try(FileChannel ifc = FileChannel.open(src, StandardOpenOpton.READ);
FileChannel ofc = FileChannel.open(dst, StandardOpenOption.WRITE, StandardOpenOption CREATE)) {
int num;
while(true) {
num = ifc.read(buf); // 파일 -> 채널 -> 버퍼 -> 데이터
if(num == -1)
break;
buf.flip(); // 모드 변환
ofc.write(buf); // 데이터 -> 버퍼 -> 채널 -> 파일
buf.clear(); // 버퍼 비우기 또는 버퍼에 저장된 내용 중에서 읽는 데이터만 지우는 compact() 사용
}
}
catch(IOException e) {
e.printStackTrace();
}
}
성능 향상 포인트
- 기존 IO 모델을 기반으로 파일 복사 프로그램을 작성하려면
입력 스트림과 출력 스트림 각각에 버퍼 스트림을 연결해야 하므로 두 개의 버퍼가 필요함
또한 입력 버퍼에 저장된 데이터를 출력 버퍼로 이동하는 버퍼 사이의 데이터 이동 과정을 반드시 거쳐야 함
- 그러나 위와 같은 작업을 NIO 모델에서는 생략할 수 있으며
Non-direct 버퍼 생성 대신 Direct 버퍼를 생성하면 더욱 성능 향상을 기대할 수 있음
- 하지만 Direct 버퍼의 할당과 해제에 드는 시간적 비용이 Non-direct 버퍼에 비해 다소 높기 때문에
입출력할 파일의 크기가 크지 않거나 버퍼를 빈번하게 할당하고 해제해야할 상황이라면 오히려 Non-direct 버퍼를 이용
ByteBuffer buf = ByteBuffer.allocate(1024); // 파일 -> 운영체제 버퍼 -> 가상머신 버퍼 -> 자바 프로그램
ByteBuffer buf = ByteBuffer.allocateDirect(1024); // 파일 -> 운영체제 버퍼 -> 자바 프로그램
- 그리고 다양한 데이터의 입출력을 위해 ByteBuffer 외에도 CharBuffer, IntBuffer, DoubleBuffer가 존재
파일 랜덤 접근
- 파일 랜덤 접근이란 파일에 데이터를 쓰거나 읽을 때 원하는 위치에 쓰거나 읽는 것을 의미
- NIO의 파일 랜덤 접근은 버퍼를 기반으로 하므로 버퍼에 파일의 데이터를 옮겨 놓은 다음에,
버퍼에 저장된 데이터를 순서에 상관없이 원하는 순으로 읽어 들일 수 있음
public static void main(String[] args) {
Path fp = Paths.get("data.txt");
// 버퍼 생성
ByteBuffer wb = ByteBuffer.allocate(1024); // 데이터 -> 버퍼1 -> 채널 -> 파일
// 버퍼에 데이터 저장
wb.putInt(120); // 포지션 0에 저장, 메소드 호출 후 포지션 4으로 이동
wb.putInt(240); // 포지션 4에 저장, 메소드 호출 후 포지션 8으로 이동
wb.putDouble(0.94); // 포지션 8에 저장, 메소드 호출 후 포지션 16으로 이동
wb.putDouble(0.75); // 포지션 16에 저장, 메소드 호출 후 포지션 24으로 이동
// 하나의 채널 생성
try(FileChannel fc = FileChannel.open(fp, StandardOpenOpton.CREATE, StandardOpenOpton.READ, StandardOpenOpton.WRITE)) {
// 파일에 쓰기
wb.flip(); // 메소드 호출 후 포지션 0
fc.write(wb);
// 파일로부터 읽기
ByteBuffer rb = ByteBuffer.allocate(1024); // 파일 -> 채널 -> 버퍼2 -> 출력
fc.position(0); // 채널의 포지션을 맨 앞으로 이동 (0)
fc.read(rb);
rb.flip(); // 메소드 호출 후 포지션 0
System.out.println(rb.getInt()); // 120 (0)
rb.position(Integer.BYTES * 2); // 버퍼의 포지션 이동 (0 + 8 = 8)
System.out.println(rb.getDouble()); // 0.94 (8)
System.out.println(rb.getDouble()); // 0.75 (8 + 8 = 16)
rb.position(Integer.BYTES); // 버퍼의 포지션 이동 (24 + 4)
System.out.println(rb.getInt()); // 240 (4)
}
catch(IOException e) {
e.printStackTrace();
}
}