교재에 무조건 있는 내용인데 내가 확실하게 잘 이해를 못한 것 같아서 다시 정리하려고 한다. 파일 입출력 관련해서 예제 코드를 만드다가 io 따로 있고 nio가 따로 있던데 이것도 무슨 차이인지 정리해보려고 한다.
정리는 예전에 학원에서 배웠던 교재와 대학교에서 배웠던 교재를 참고하였다.
참고
https://www.booksr.co.kr/product/%EB%AA%85%ED%92%88-java-programming%EA%B0%9C%EC%A0%954%ED%8C%90/
명품 JAVA Programming (개정4판) | 생능출판사
명품 자바를 사랑해주시는 많은 교수님들과 독자들께 감사드립니다. 2017년 7월에 개정3판이 나오고, 두 달도 지나지 않아 Java 9가 출시되었습니다. 그리고 급기야 올해 3월에는 Java 10이 출시되었
www.booksr.co.kr
https://www.yes24.com/Product/Goods/23117533
어서와 JAVA는 처음이지! - 예스24
더 이상 이론에 지쳐 JAVA를 포기하지 말자프로그램을 배운다는 것은 나를 표현하는 또 하나의 언어를 습득하는 일이다. 내 뜻대로 알고리즘을 만들어내고, 생각했던 대로 결과물이 산출되는 것
www.yes24.com
파일의 필요성
프로그램이 종료되면 메모리(휘발성 메모리인 RAM)에서 지워지고 그동안 작업하였던 데이터는 모두 사라진다. 따라서 프로그램을 실행하는 도중에 어떤 데이터를 저장하고자 한다면 우리는 하드 디스크에 파일 형태로 저장하여야 한다. 예를 들자면, 게임에서는 사용자의 점수를 score.txt 파일 안에 저장하거나 사용되는 아이템, 색상, 폰트와 같은 사용자의 선택 사항을 파일에 저장할 수도 있다.
데이터를 디스크에 저장하는 방법과 디스크에서 데이터를 읽어오는 방법을 알아보자.
자바에서는 파일에 연결된 스트림(Stream)을 열어서 데이터를 기록하거나 읽을 수 있다.
단어 자체의 뜻은 '흐르는 시냇물', '물이 흐르는 개울' 을 의미하며 컴퓨터 공학에서는 연속적인 데이터의 흐름 혹은 데이터를 전송하는 소프트웨어 모듈을 뜻한다.
"바이트들의 연속적인 흐름" 정도로 이해하면 될 듯 하다. (개울에 바이트들이 연속해서 떠다니는 모습을 상상해보자~)
이 흐름들의 양끝에는 프로그램(Java 응용프로그램)이 있고 그 반대에는 입출력 장치(키보드, 디스크, 프린터 등)나 네트워크 등이 있다.
그림을 넣을지 말지 고민하였는데 그래도 있는게 이해가 더 편할 것 같아서 첨부하였다.
스트림에 대한 특징을 간략하게 정리해보자.
ㆍ스트림의 양끝에는 입출력 장치와 자바 응용프로그램이 연결된다.
자바 응용프로그램은 입력 스트림과 출력 스트림만을 연결하고, 입출력 스트림이 입출력 장치를 제어하고 실질적인 입출력을 담당한다.
어떤 장치에 데이터를 쓰려면 장치와 연결된 스트림을 생성(생성자로 객체 생성)한 후 스트림에 데이터를 쓰면 된다. (해당 기능 끝나면 close 해주기)
ㆍ스트림은 단방향이다.
입력 스트림은 입력 장치에서 응용프로그램으로 데이터를 전송하며, 출력 스트림은 응용프로그램으로 부터 받은 데이터를 출력 장치로 전송한다.
입력과 출력을 하려면 입력 스트림, 출력 스트림이 각각 필요하다. 예를 들자면 System.in, System.out은 각각 키보드와 모니터를 나타내는 스트림으로, 표준 입력 스트림과 표준 출력 스트림으로 불린다. (System은 현재 사용하고 있는 컴퓨터 시스템을 나타냄)
ㆍ스트림은 선입선출, FIFO 구조
입력 스트림에 먼저 들어온 데이터가 응용프로그램에 먼저 전달되고, 출력 스트림은 응용프로그램이 출력한 순서대로 출력 장치에 보낸다.
(처리하는 순서는 입출력이 OS에서 보통 최우선적으로 처리하도록 설정 되어 있다)
스트림의 종류
java.io 패키지는 안에 스트림을 지원하는 클래스들이 있는데, 이들 클래스를 몇 가지의 기준에 따라서 분류할 수 있다. 먼저 취급하는 데이터의 종류에 따라 크게 바이트 스트림과 문자 스트림으로 분류할 수 있다.
● 바이트 스트림
이진 데이터를 읽고 쓰기 위하여 사용하고 바이트 단위로 입출력하는 클래스.
모든 바이트 스트림 클래스들은 추상 클래스인 InputStream와 OutputStream에서 파생된다.
● 문자 스트림
유니코드로 된 문자 데이터를 읽고 쓰기 위하여 사용하고 문자 단위로 입출력하는 클래스.
모든 문자 스트림 클래스들은 추상 클래스인 Reader와 Writer에서 파생된다.
(개인적으로 byte 단위인 바이트 스트림이 편해서 문자 스트림은 강의 들을 때 말곤 써본적이 없다)
바이트 스트림은 8비트의 바이트 단위로 입출력을 수행한다. 파일 입출력에 특화된 FileInputStream 클래스와 FileOutputStream 클래스가 있다. 각각 파일에서 바이트를 읽고 파일에 바이트를 쓴다.
import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
* 바이트 스트림 기본 클래스 이해하기
*/
public class FileIO_01 {
public static void main(String[] args) throws Exception {
int i; // 데이터 읽고 쓰기 위한 int 타입 변수
// 파일을 읽고 쓸 스트림 생성
try (
FileInputStream fis = new FileInputStream("C:\\Users\\NC627\\Desktop\\2563.txt");
FileOutputStream fos = new FileOutputStream("C:\\Users\\NC627\\Desktop\\코테어려워..ㅠㅠ.txt");
) {
// 편하게 -1로 끝을 알기 위해 int 타입 반환하는 read 활용
while ((i = fis.read()) != -1) {
fos.write(i);
}
}
System.out.println("끝! 바탕 화면 보기~");
}
}
이클립스에서 작성하고 복붙하는데 줄이 이상하게 나옴...
이런식으로 쓰는구나 정도로 느낌만 보고 가면 좋을듯 하다.
finally 처리 하기 귀찮아서 try-with-resource 사용하여 파일 입출력 작업을 한 후 close 될 수 있게 하였고, while에서 -1 이 반환되면 해당 파일을 쓴다.
문자 스트림은 (유니코드)문자 단위로 입출력을 수행한다. 문자 단위를 사용하다보니 인코딩 하는 방식도 생각을 해야 한다. 현재 시스템의 기본 문자 집합의 경우 JVM의 기본 설정에 의존한다. 바이트 스트림과 마찬가지로 파일 입출력에 특화된 FileReader 클래스와 FileWriter 클래스가 있다.
import java.io.File;
import java.net.URI;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
* 파일 생성자 클래스 및 메소드
*/
public class FileIO_03 {
public static void main(String[] args) throws Exception {
/**
* File 클래스 생성자
*
* pathName의 절대 경로가 나타내는 File 객체 생성 File(String pathName)
*
* 부모 디렉토리 File 객체와 자식 파일명을 사용하여 File 객체 생성 File(File parent, String childName)
*
* 부모 디렉토리와 자식 파일명을 사용하여 File 객체 생성 File(String parent, String childName)
*
* file:URI를 사용하여 File 객체 생성 File(URI uri)
*
*/
// File(String pathName)
File file1 = new File("C:\\Users\\NC627\\Desktop\\pathTest\\2563.txt");
String file1Path = file1.getPath(); // 파일 이름
String file1Name = file1.getName(); // 파일 경로
String file1ParentDictory = file1.getParent(); // 파일의 상위 디렉토리
long file1Length = file1.length(); // 파일 크기 (존재하지 않으면 운영체제에 따라 0 리턴)
boolean file1Exist = file1.exists(); // 파일 또는 디렉토리 존재하면 true
boolean file1isFile = file1.isFile(); // 일반 파일이면 true
boolean file1isDirectory = file1.isDirectory(); // 디렉토리이면 true
long file1LastModified = file1.lastModified(); // 최종 파일 수정시간
File[] currentDictoryList = new File(file1.getParent()).listFiles(); // 디렉토리 파일 리스트
// long 타입의 값을 Instant으로 변환
Instant instant = Instant.ofEpochMilli(file1LastModified);
// Instant을 LocalDateTime으로 변환
LocalDateTime lastModifiedDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
// 포맷팅하여 출력
String formattedDateTime = lastModifiedDateTime
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println("==================================File Method========================================");
System.out.println("name : " + file1Name);
System.out.println("path : " + file1Path);
System.out.println("parent directory : " + file1ParentDictory);
System.out.println("fileExist : " + file1Exist);
System.out.println("length : " + file1Length);
System.out.println("isFile : " + file1isFile);
System.out.println("isDirectory : " + file1isDirectory);
System.out.println("lastModifiedOrigin : " + file1.lastModified());
System.out.println("lastModified : " + formattedDateTime);
for (File tmp : currentDictoryList) {
System.out.println("currentDictoryList : " + tmp.getName());
}
// File(File parent, String childName)
File parentFile = new File("C:\\example");
String childFileName1 = "C:\\Users\\NC627\\Desktop\\자식 파일 클라쓰1.txt";
File file2 = new File(parentFile, childFileName1);
// File(String parent, String childName)
String parentDirectory = "C:\\example";
String childFileName2 = "C:\\Users\\NC627\\Desktop\\자식 파일 클라쓰2.txt";
File file3 = new File(parentDirectory, childFileName2);
// File(URI uri)
URI uri = new URI(
"file:///C:/Users/NC627/Downloads/comeon_java/Test%20Bank%20%EC%97%B0%EC%8A%B5%EB%AC%B8%EC%A0%9C(%ED%95%99%EC%83%9D%EC%9A%A9)/%EC%96%B4%EC%84%9C%EC%99%80%EC%9E%90%EB%B0%94_17%EC%9E%A5%EC%97%B0%EC%8A%B5%EB%AC%B8%EC%A0%9C.pdf");
File file4 = new File(uri);
// URI 정보 출력
System.out
.println("\n==================================URI type Method========================================");
System.out.println("URI: " + uri);
System.out.println("Scheme: " + uri.getScheme());
System.out.println("Host: " + uri.getHost());
System.out.println("Path: " + uri.getPath());
}
}
바이트 스트림과 마찬가지로 정상적으로 실행되어 파일이 생성 되었고 해당 내용도 똑같이 잘 들어가있다. (사진에는 따로 넣지 않았다) file.encoding의 경우 JVM의 값이 없으면 현재 OS의 값을 참조하여 사용한다.
(처음에 값을 물고 올라가서 중간에 값을 변경하여도 재실행 하지 않으면 변경이 안된다)
유니코드와 문자 변환 방식에 대해서는 나중에 따로 포스트를 작성하여 설명하려 한다.
스트림은 바이트들의 연속적인 흐름이라고 앞에서 설명하였다. 이러한 바이트(데이터)들을 빠르게 가공 처리할 수 있는 역할을 담당하는 버퍼 스트림(Buffered Stream)을 설명한다.
버퍼 스트림(Buffered Stream)
지금까지의 스트림은 버퍼를 사용하지 않는 입출력(unbuffered I/O)이었다. 이것은 매우 비효율적 방법이다. 입출력 요청은 디스크 접근이나 네트워크 접근과 같은 매우 시간이 많이 걸리는 동작을 요구하기 때문이다.
(이것에 대해 궁금하면 OS에서 디스크와 메모리, 캐시에 관련된 개념 부분을 찾아보면 좋다)
이러한 오버헤드를 줄이기 위하여 자바에서는 버퍼링된 스트림(buffered I/O)을 제공한다. 버퍼란 데이터를 일시적으로 저장하기 위한 메모리이다. 이러한 버퍼를 가진 스트림을 버퍼 스트림이라 한다. 입출력 데이터를 일시적으로 저장하는 버퍼를 이용하여 입출력 효율을 개선한다.
자바에서는 버퍼가 없는 스트림에 버퍼를 추가하기 위하여 4개의 버퍼 스트림이 제공된다.
BufferedInputStream, BufferedOutputStream / BufferedReader, BufferedWriter
버퍼 스트림은 버퍼를 가지고 있기 때문에 버퍼가 꽉 찼을 때만 출력되는 특징이 있다. 버퍼가 다 차지 않는 상태에서 버퍼에 있는 데이터를 강제로 출력 장치로 보내려면 flush() 메소드를 호출해서 버퍼를 비워줘야 한다. 버퍼 스트림의 사용이 끝났으면 close() 메소드를 호출하여 닫아주자.
InputStreamReader와 OutputStreamWriter
바이트 스트림과 문자 스트림을 연결하는 두 개의 범용 브릿지 스트림인 InputStreamReader와 OutputStreamWriter가 있다. InputStreamReader는 바이트 스트림을 문자 스트림으로 변환한다. 바이트를 읽어서 지정된 문자 집합을 사용하여 문자로 변환한 후 출력한다. 문자집합은 이름으로 지정될 수 있고 또는 플랫폼의 디폴트 문자집합이 사용될 수 있다. 효율성을 위하여 InputStreamReader를 BufferedReader로 감싸는 것이 좋다. OutputStreamWriter는 문자 스트림을 바이트 스트림으로 변환한다. 문자를 받아서 지정된 문자집합을 사용하여 바이트로 인코딩한다. 이것도 효율성을 위하여 BufferedWriter로 감싸는 것이 좋다.
package com.example;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
/**
* Buffered 관련 클래스, InputStreamReader, OutputStreamWriter
*/
public class FileIO_03 {
public static void main(String[] args) throws Exception {
File fi = new File("C:\\Users\\NC627\\Desktop\\02. study\\쉘 관련.txt");
/*
* new FileInputStream(fi)
* → 해당 파일 바이트 형식으로 읽음
*
* new InputStreamReader(new FileInputStream(fi),"UTF-8")
* → InputStream 타입과 charset을 파라미터로 받아서 바이트 형식을 문자 인코딩 방식에 맞춰 문자 형식으로 읽음
*
* 해당 결과물을 BufferedReader로 빠르게(버퍼를 안 썼을때 보다 상대적으로) 처리 한다.
*
*/
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(fi),"UTF-8"));
String str;
// 읽어온 파일을 한줄씩 출력한다.
while ((str = br.readLine()) != null) {
System.out.println(str);
}
}
}
몰랐는데 과제할 때 엑셀에서 내용 읽어올 때 사용했던 코드가 저 BufferedReader로 읽어오는 코드 그대로 사용했었다.
File 클래스
File 클래스는 파일이나 디렉토리에 대해 경로명, 크기, 타입, 수정 날짜 등의 속성 정보를 제공하고 파일 삭제, 디렉토리 생성, 파일 이름 변경, 디렉토리 내의 파일 리스트 제공 등 다양한 파일 관리 작업을 지원한다.
이름과는 달리 File 클래스에는 파일 입출력 기능은 없다. 파일을 읽고 쓰는 것은 앞에 있는 파일 입출력 클래스를 이용해야 한다.
import java.io.File;
import java.net.URI;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
* 파일 생성자 클래스 및 메소드
*/
public class FileIO_04 {
public static void main(String[] args) throws Exception {
/**
* File 클래스 생성자
*
* pathName의 절대 경로가 나타내는 File 객체 생성 File(String pathName)
*
* 부모 디렉토리 File 객체와 자식 파일명을 사용하여 File 객체 생성 File(File parent, String childName)
*
* 부모 디렉토리와 자식 파일명을 사용하여 File 객체 생성 File(String parent, String childName)
*
* file:URI를 사용하여 File 객체 생성 File(URI uri)
*
*/
// File(String pathName)
File file1 = new File("C:\\Users\\NC627\\Desktop\\pathTest\\2563.txt");
String file1Path = file1.getPath(); // 파일 이름
String file1Name = file1.getName(); // 파일 경로
String file1ParentDictory = file1.getParent(); // 파일의 상위 디렉토리
long file1Length = file1.length(); // 파일 크기 (존재하지 않으면 운영체제에 따라 0 리턴)
boolean file1Exist = file1.exists(); // 파일 또는 디렉토리 존재하면 true
boolean file1isFile = file1.isFile(); // 일반 파일이면 true
boolean file1isDirectory = file1.isDirectory(); // 디렉토리이면 true
long file1LastModified = file1.lastModified(); // 최종 파일 수정시간
File[] currentDictoryList = new File(file1.getParent()).listFiles(); // 디렉토리 파일 리스트
// long 타입의 값을 Instant으로 변환
Instant instant = Instant.ofEpochMilli(file1LastModified);
// Instant을 LocalDateTime으로 변환
LocalDateTime lastModifiedDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
// 포맷팅하여 출력
String formattedDateTime = lastModifiedDateTime.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println("==================================File Method========================================");
System.out.println("name : " + file1Name);
System.out.println("path : " + file1Path);
System.out.println("parent directory : " + file1ParentDictory);
System.out.println("fileExist : " + file1Exist);
System.out.println("length : " + file1Length);
System.out.println("isFile : " + file1isFile);
System.out.println("isDirectory : " + file1isDirectory);
System.out.println("lastModifiedOrigin : " + file1.lastModified());
System.out.println("lastModified : " + formattedDateTime);
for (File tmp : currentDictoryList) {
System.out.println("currentDictoryList : " + tmp.getName());
}
// File(File parent, String childName)
File parentFile = new File("C:\\example");
String childFileName1 = "C:\\Users\\NC627\\Desktop\\자식 파일 클라쓰1.txt";
File file2 = new File(parentFile, childFileName1);
// File(String parent, String childName)
String parentDirectory = "C:\\example";
String childFileName2 = "C:\\Users\\NC627\\Desktop\\자식 파일 클라쓰2.txt";
File file3 = new File(parentDirectory, childFileName2);
// File(URI uri)
URI uri = new URI("file:///C:/Users/NC627/Downloads/comeon_java/Test%20Bank%20%EC%97%B0%EC%8A%B5%EB%AC%B8%EC%A0%9C(%ED%95%99%EC%83%9D%EC%9A%A9)/%EC%96%B4%EC%84%9C%EC%99%80%EC%9E%90%EB%B0%94_17%EC%9E%A5%EC%97%B0%EC%8A%B5%EB%AC%B8%EC%A0%9C.pdf");
File file4 = new File(uri);
// URI 정보 출력
System.out.println("\n==================================URI type Method========================================");
System.out.println("URI: " + uri);
System.out.println("Scheme: " + uri.getScheme());
System.out.println("Host: " + uri.getHost());
System.out.println("Path: " + uri.getPath());
}
}
file.separator 값을 받아와서 생성한 path를 파라미터로 줬던 첫번째 생성자 빼고는 다 생소하다.
관련 메소드의 경우는 전부 있는건 아니지만 사용할 것 같은건 작성을 해봤다. 의문인건 getPath() 값을 가져오는 경우 현재 디렉토리의 상위 디렉토리가 아닌 앞에 파일이름만 뺀 path값이 나온다. OS마다 다른건지.. 찾아봐도 별 얘기가 없는것 같아서(해당 내용의 경우 좀 더 알아보고 내용 추가) 유념해두고 사용해야겠다. 날짜의 경우 타입이 long으로 나오기 때문에 포메팅 해줬다. (교제에는 그냥 printf로 출력형식만 맞춰줌) 파일리스트의 경우 File 타입의 배열로 값을 받는다.
'01. Java > 01. 기본개념' 카테고리의 다른 글
Escape Character (이스케이프 문자, 제어 문자) (0) | 2023.11.30 |
---|---|
자바 개발 도구 (0) | 2023.11.30 |