도서/자바의 신

[도서/자바의 신] #26 파일에 있는 것을 읽고 쓰려면 아이오를 알아야죠

yulee_to 2023. 1. 1. 04:07

자바의 신

✔️이 글은 [자바의 신 - 이상민 지음] 도서를 바탕으로 정리한 글입니다. 


I/O는...

I/O는 프로그램에 있는 어떤 내용을

  • 파일에 읽거나 저장할 일이 있을 때
  • 다른 서버나 디바이스로 보낼 일이 있을 때 사용한다.

JVM기준으로 I(Input)는 읽을 때, O(Output)는 파일로 쓰거나 외부로 전송할 때로 I/O는 입력과 출력을 통칭한다.

초기 단계의 자바에서는 I/O를 처리하기 위해 java.io 패키지의 바이트 기반 데이터 처리를 위한 스트림(Stream)클래스를 제공했다.

읽는 작업은 InputStream을 통해, 쓰는 작업은 OutputStream을 통해 작업하도록 되어있다.

바이트가 아닌, char 기반의 문자열로만 되어있는 파일은 Reader와 Writer라는 클래스로 처리한다. 

스트림은 연속적인 데이터를 의미한다.

 

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

Java 7에서는 NIO2가 추가돼 파일을 보다 효율적으로 처리해준다.

자바의 File과 Files 클래스

java.io 패키지에 있는 File 클래스는 파일과 파일의 경로(path)정보도 포함한다. 

File 클래스는 정체가 불분명하고, 심볼릭 링크(symbolic link)와 같은 유닉스 계열의 파일에서 사용하는 몇몇 기능을 제공하지 못한다.

데이터 처리를 위해선 File 클래스 객체를 생성해야 한다.

 

NIO2가 등장하면서 java.nio.file 패키지에 Files라는 클래스가 File 클래스에 있는 메소드들을 대체하여 제공한다. 

File과 달리 Files의 모든 메소드는 static이라 객체를 생성해줄 필요가 없다. 

 

File 클래스의 생성자

  • File(File parent, String child) : 이미 생성되어 있는 File 객체(parent)와 그 경로의 하위 경로 이름으로 새로운 File 객체 생성
  • File(String pathname) : 지정한 경로 이름으로 File 객체 생성
  • File(String parent, String child) : 상위 경로와 하위 경로로 File 객체를 생성
  • File(URI uri) : URI에 따른 File 객체 생성

child 값은 경로가 될 수도 있고, 파일이름이 될 수 있다.

전체 경로와 파일 이름이 pathname에 지정되어 있을 경우에는 파일을 가리키는 File 객체가 된다. 

URI는 Uniform Resource Identifier의 약자로, 어떠한 리소스를 가리키기 위한 경로를 의미한다.

File 클래스를 이용하여 파일의 경로와 상태를 확인해 보자

OS마다 디렉터리 구분 기호가 다르기 때문에 이런 모호함을 없애기 위해 File 클래스에서 separator라는 것이 static 변수로 존재한다. 

따라서 경로를 표시할 때 File.separator + "상위 디렉토리" + File.separator + "하위 디렉토리" ...식으로 String 변수에 넣어주면 된다. 

 

여러 메소드

  • exists() : 해당 경로가 존재하는지 확인, 존재하면 true
  • mkdir() : 디렉토리 하나 생성
  • mkdirs() : 여러 개의 하위 디렉토리 생성(C:\godofjava -> C:\godofjava\text1\text2)
  • isDirectory() :  File 객체가 경로를 나타내는지 확인
  • isFile() : File 객체가 파일을 나타내는지 확인
  • isHidden() : File 객체가 숨겨진 파일인지 확인
  • canRead(), canWrite(), canExecute() : 현재 수행하고 있는 자바 프로그램이 해당 File 객체에 읽거나, 쓰거나, 실행할 수 있는 권한이 있는지 확인 (Java 6부터 추가)
  • lastModified() : 파일이나 경로가 언제 생성되었는지를 확인, long 타입의 시간을 리턴->java.util.Date 클래스로 시간 확인
  • delete() : 파일 삭제, 성공적으로 삭제시 true 반환
  • length() : 파일의 길이(byte) 반환

File 클래스를 이용하여 파일을 처리하자

  • createNewFile() : 비어있는 새로운 파일 생성, 성공적으로 생성시 true 이미 있는 파일이거나 실패시 false, IOException 던지는 메소드로 try-catch 처리해줘야 함
  • getAbsolutePath() : 객체의 절대 경로를 String으로 반환
  • getAbsoluteFile() : 객체의 절대 경로를 File 객체로 반환
  • getCanonicalPath() : 객체의 상대 경로를 String으로 반환
  • getCanonicalFile() : 객체의 상대 경로를 File 객체로 반환
  • getPath() : 드라이브 이름을 제외한 경로와 파일이름 반환
  • getName() : 파일 이름 반환
  • getParent() : 해당 객체가 파일을 가리키고 있는 경우 파일이름을 제외한 경로만 출력

Absolute는 절대 경로를 의미하고, Canonical은 절대적이고, 유일한 경로를 의미한다. 

Absoulte 경로는 여러개 있을 수 있어 C:\godofjava\a, C:\godofjava\..\godofjava\a, C:\godofjava\.\.\.\a 모두 Abosolute 경로이다. 반면에 Canonical 경로는 단 하나만 존재할 수 있기 때문에 절대 경로가 어떻든지 상관없이 항상 C:\godofjava\a로 고정되어 있다. 

디렉터리에 있는 목록을 살펴보기 위한 list 메소드들

  • listRoots() : JVM이 수행되는 OS에서 사용중인 파일 시스템의 루트(root) 디렉토리 목록을 File 배열로 리턴, static 메소드
  • list() : 현재 디렉터리의 하위에 있는 목록을 String 배열로 리턴
  • list(FilenameFilter filter) : 현재 디렉터리의 하위에 있는 목록 중, 매개 변수로 넘어온 filter의 조건에 맞는 목록을 String 배열로 리턴
  • listFiles() : 현재 디렉터리의 하위에 있는 목록을 File 배열로 리턴
  • listFiles(FileFilter filter) : 현재 디렉터리의 하위에 있는 목록 중, 매개 변수로 넘어온 filter의 조건에 맞는 목록을 File 배열로 리턴
  • listFiles(FilenameFilter filter) : 현재 디렉터리의 하위에 있는 목록 중, 매개 변수로 넘어온 filter의 조건에 맞는 목록을 File 배열로 리턴, 매개변수가 FilenameFilter 객체임

FileFilter 인터페이스 메소드

  • accept(File pathname) : 매개변수로 넘어온 File 객체가 조건에 맞는지 확인

FilenameFilter 인터페이스 메소드

  • accept(File dir, String name) : dir에 있는 경로나 파일이름(name)이 조건에 맞는지 확인

둘다 listFiles()를 호출하면 매개변수에 맞는 인터페이스에 해당하는 accept() 메소드가 호출된다. 

FilenameFilter는 메소드 매개 변수로 fileName이 넘어오기 때문에 별도로 File 객체의 getName()을 호출할 필요가 없지만, FileFilter는 호출해서 확인해줘야 한다. FilenameFilter는 디렉터리와 파일을 구분하지 못해 만약 .jpg로 끝나는 디렉터리가 있으면 필터로 걸러낼수가 없다는 단점이 있다. 

InputStream과 OutputStream은 자바 스트림의 부모들이다

자바의 I/O는 기본적으로 InputStream과 OutputStream이라는 abstract 클래스를 통해 제공된다. 

 

InputStream 클래스 선언부

public abstract class InputStream 
extends Object
implements Closeable

Closeable 인터페이스는 close()라는 메소드만 선언되어 있는데, 어떤 리소스를 열었던 간에 이 인터페이스를 구현하면 해당 리소스는 close() 메소드를 이용해 닫으라는 것을 의미한다. java.io 패키지의 클래스를 사용할 땐 작업이 끝나면 close() 메소드로 항상 닫아줘야 한다. 

 

리소스는 파일이 될 수 있고, 네트워크 연결도 될 수 있다. 스트림을 통해 작업할 수 있는 모든 것을 리소스라고 보면 된다. 

 

InputStream 클래스 메소드들

  • availble() : 스트림에서 중단없이 읽을 수 있는 바이트 개수 리턴
  • mark(int readlimit) : 스트림의 현위치를 표시(mark)해둠. 매개변수값은 표시해둔 자리의 최대 유효 길이로 이 값을 넘어가면 표시해둔 자리는 의미가 없어짐
  • reset() : 현재 위치를 mark() 메소드가 호출되었떤 위치로 되돌림
  • markSupported() : mark()나 reset() 메소드가 수행 가능한지 확인
  • read() : 스트림에서 다음 바이트를 읽음. 유일한 abstract 메소드
  • read(byte[] b) : 매개변수로 넘어온 바이트 배열에 데이터를 담음. 담은 개수 리턴
  • read(byte[] b, int off, int len) : off부터 len길이만큼 읽어서 바이트 배열에 담음. 담은 개수 리턴
  • skip(long n ) : n만큼 데이터 건너 뜀
  • close() : 작업중인 대상 해제 

스트림을 다룰 때 다른 메소드를 호출하지 않아도 close() 메소드는 반드시 호출해야 한다. 

 

주로 사용하는 InputStream 스트림

  • FileInputStream : 파일을 읽는 데 사용. 텍스트 파일보다는 이미지처럼 바이트 코드로 된 데이터 읽을 때 사용
  • FilterInputStream : 다른 입력 스트림을 포괄하며, 단순히 InputStream 인터페이스를 상속함
  • ObjectInputStream : ObjectOutputStream으로 저장한 데이터 읽어오는 데 사용

FileInputStream와 ObjectInputStream은 객체를 생성해 데이터 처리, FilterInputStream의 생성자는 protected로 선언되어 있기 때문에 상속받은 클래스에서만 객체 생성이 가능하다.

 

OutputStream 클래스 선언부

public abstract class OutputStream 
extends Object
implements Closeable, Flushable

Flushable 인터페이스에는 하나의 메소드만 선언되어 있으며, 그 이름은 flush()다. 쓰기 작업을 요청할 때마다 저장하면 효율이 좋지 않아 버퍼에 모아서 저장하는데 flush() 메소드는 현재 버퍼에 있는 내용을 기다리지 말고 무조건 저장하라고 시키는 작업을 한다.

 

OutputStream의 메소드

  • write(byte[] b) : 매개 변수로 받은 바이트 배열(b)을 저장
  • write(byte[] b, int off, int len) : 바이트 배열의 특정 위치부터 지정한 길이 만큼 저장
  • write(int b) : 매개변수로 받은 바이트를 저장, 타입은 int지만 실제 저장되는건 바이트
  • flush() : 버퍼에 쓰려고 대기하는 데이터 강제로 쓰게 함
  • close() : 쓰기 위해 연 스트림 해제

Reader와 Writer

Reader와 Writer는 char 기반의 문자열 처리를 위한 클래스이다.

 

Reader 클래스 선언부

public abstract class Reader
extends Object
implements Readable, Closeable

Reader의 abstract 메소드는 close()와 read() 메소드이다. 

대부분의 메소드는 InputStream과 유사하고, close() 메소드는 모든 작업이 끝나면 꼭 호출해줘야 한다.

 

Writer 클래스 선언부

public abstract class Writer
extends Object
implments Appendable, Closeable, Flushable

Appenable 인터페이스는 Java 5부터 추가되었으며, 각종 문자열을 추가하기 위해 선언되었다.

대부분의 메소드는 OutputStream과 동일하지만, append()만 추가되었다.

  • append(char c) : 매개변수로 넘어온 char를 추가
  • append(CharSeqeunce csq) : 매개변수로 넘어온 CharSequence 추가

만들어진 문자열이 String 타입이면 그냥 write() 메소드를 사용해도 별 상관은 없겠지만, StringBuilder나 StringBuffer로 문자열을 만들면 append() 메소드를 사용하는 것이 훨씬 편하다.

텍스트 파일을 써보자

자바에서 char 기반의 내용을 파일로 쓰기 위해서는 FileWriter나 BufferedWriter를 사용한다. 두 클래스는 Writer를 확장한 클래스들이다.

FileWriter의 append(), write() 메소드는 호출할 때마다 데이터를 쓰기 때문에 비효율적이고 BufferedWriter가 버퍼를 이용해 버퍼가 차게 되면 데이터를 저장하도록 해서 보다 더 효율적이다. 다만 BufferedWriter의 생성자들은 모두 첫번째 매개변수로 Writer 객체를 넘겨줘야 한다. 

두 클래스의 객체를 생성할 때는 IOException이 발생할 수 있으므로 try-catch로 묶어줘야 한다. 

중간에 예외가 발생할 경우 close() 메소드가 수행되지 않을 수 있기 때문에 항상 finally에 close()를 수행해주는게 좋다. 

또 close()를 해줄때는 먼저 open한 객체부터 해줘야 정상적인 처리가 가능하다.

텍스트 파일을 읽어보자

직접 파일을 열어서 확인해보려면 FileReader와 BufferedReader를 사용하면 된다.

주로 파일을 하나 열어 while((data=bufferedReader.readLine()) != null) { 출력 } 식으로 while 문에서 readLine()과 그 값이 null인지 까지 한줄에 확인을 해준다.

 

read할 때 위 클래스들을 사용하면 코드가 길어지는데 java.util 패키지에 있는 Scanner 클래스를 사용하면 더 쉽게 파일을 읽을 수 있다.

Scanner 클래스는 텍스트 기반의 기본 자료형이나 문자열 데이터를 처리하기 위한 클래스로 정규 표현식(Regular Expression)을 사용해 데이터를 잘라 처리할 수도 있다. 

hasNextLine() 메소드로 다음줄이 있는지 확인하고, nextLine() 메소드로 다음 줄의 내용을 문자열로 한 줄 씩 리턴해 파일을 읽어온다. 

 

Java 7에서 제공하는 Files 클래스로는 다음 한 줄로 파일을 읽을 수도 있다.

String data = new String(Files.readAllBytes(Paths.get(filename));

정리해 봅시다

1. I/O는 각각 무엇의 약자인가요?

Input과 Output의 약자이다.

2. File 클래스는 파일만 지정할 수 있나요?

NO, 경로도 가능

3. OS마다 다른 경로 구분자를 처리하기 위해서는 File 클래스의 어떤 상수를 사용해야 하나요?

separator

4. File 클래스에서 디렉터리를 만드는 mkdir()과 mkdirs() 메소드의 차이는?

mkdir()은 하나만 만들어주고, mkdirs()는 여러개의 하위 디렉터리를 만들어준다.

5. File 클래스의 list() 메소드와 listFiles() 메소드의 차이는?

list는 디렉터리의 하위 목록을 String 배열로 리턴하지만, listFiles()는 하위 목록을 File 배열로 리턴

6. FileFilter와 FilenameFilter의 차이는?

FileFilter는 매개변수로 넘어온 File 객체가 조건에 맞는지 확인하고, FilenameFilter는 매개변수로 넘어온 디렉터리에 있는 경로나 파일 이름이 조건에 맞는지 확인한다.

7. InputStream이라는 abstract 클래스는 어떤 작업을 하기 위해 만들어졌나요?

스트림에서 바이트를 읽기 위해서이다.

8. OutputStream이라는 abstract 클래스는 어떤 작업을 하기 위해 만들어졌나요?

스트림에서 바이트를 쓰기 위해서이다.

9. Reader라는 abstract 클래스는 어떤 작업을 하기 위해 만들어졌나요?

char 기반의 문자열 읽기 위해서이다. 

10. Writer라는 abstract 클래스는 어떤 작업을 하기 위해 만들어졌나요?

char 기반의 문자열을 쓰기 위해서이다. 

11. BufferedReader나 BufferedWriter를 사용하는 이유는?

버퍼를 사용해 효율적으로 쓰거나 읽어주기 위해서이다. 

12. 파일을 읽고, 문자열을 처리하기 위해서 필요한 Scanner 클래스가 속해있는 패키지는?

java.util

728x90