KyungHwan's etc.

자바(JAVA) 파일 입출력(I/O) 본문

Java

자바(JAVA) 파일 입출력(I/O)

KyungHwan_0 2018. 6. 22. 10:06

파일 입출력

File

java.io 패키지에서 제공하는 File 클래스는 파일 크기, 파일 속성, 파일 이름등의 정보를 얻어내는 기능과 파일 생성 및 삭제 기능을 제공한다.
File file = new File("C:/Temp/file.txt"); OR File file = new File("C:\\Temp\\file.txt");


File 객체를 생성했다고 해서 파일이나 디렉토리가 생기지 않는다. 해당 경로에 실제로 파일이나 디렉토리가 있는지 확인하려면 exists() 메소드를 호출할 수 있다.
boolean isExist = file.exists();

\그래서 이 메소드의 리턴 값이 false일 때, 파일이나 디렉토리를 생성할 수 있다.

FileInputStream

FileInputStream 클래스는 파일로부터 바이트 단위로 읽어들일 때 사용하는 바이트 기반 입력 스트림이다.

  1. FileInputStream fis = new FileInputStream("C:/Temp/image.gif"); -> 파일 객체가 이미 생성되어 있을 때라서 그냥 FileInputStream만 만들면 됨.

  2. File file = new File("C:/temp/image.gif"); FileInputStream fis = new FileInputStream(file); -> 파일 객체가 없을 때


FileInputStream은 InputStream의 하위 클래스이기 때문에 사용 방법이 InputStream과 동일하다.

FileOutputStream

FileOutputStream 클래스는 바이트 단위로 데이터를 파일에 저장할 때 사용하는 바이트 기반 출력 스트림이다.

  1. FileOutputStream fis = new FileOutputStream("C:/Temp/image.gif"); -> 파일 객체가 이미 생성되어 있을 때.

  2. File file = new File("C:/temp/image.gif"); FileOutputStream fis = new FileOutputStream(file); -> 파일 객체가 없을 때

주의할 점은 파일이 이미 존재할 경우, 기존 파일의 내용은 사라지고 파일을 덮어쓰게 된다.기존 파일의 내용에 데이터를 추가할 경우에는 생성자의 두번 째 매개 값으로 true를 주면 된다.

FileOutputStream은 OutputStream의 하위 클래스이기 때문에 사용 방법이 OutputStream과 동일하다.

FileReader

FileReader 클래스는 텍스트 파일을 프로그램으로 읽어들일 때 사용하는 문자 기반 스트림이다.

  1. FileReader fr = new FileReader("C:/Temp/file.txt");

  2. File file = new File("C:/Temp/file.txt"); FileReader fr = new FileReader(file);

FileReader는 Reader의 하위 클래스이기 때문에 사용 방법이 Reader와 동일하다.

FileWriter

FileWriter 클래스는 텍스트 데이터를 파일에 저장할 때 사용하는 문자 기반 스트림이다.

  1. FileWriter fw = new FileWriter("C:/Temp/file.txt"); -> 파일 객체가 이미 생성되어 있을 때.

  2. File file = new File("C:/temp/file.txt"); FileWriter fis = new FileWriter(file); -> 파일 객체가 없을 때

주의할 점은 파일이 이미 존재할 경우, 기존 파일의 내용은 사라지고 파일을 덮어쓰게 된다. 기존 파일의 내용에 데이터를 추가할 경우에는 생성자의 두번 째 매개 값으로 true를 주면된다.

FileWriter은 Writer의 하위 클래스이기 때문에 사용 방법이 Writer와 동일하다.

보조스트림

보조 스트림이란 다른 스트림과 연결되어 여러 가지 편리한 기능을 제공해주는 스트림을 말한다.

보조 스트림을 필터 스트림이라고도 하는데 이는 보조 스트림의 일부가 FilterInputStream, FilterOutputStream의 일부이기 때문이다. 하지만 다른 보조스트림은 이 클래스들을 상속받지 않는다.

보조스트림은 문자 변환, 입출력 성능 향상, 기본 데이터 타입 입출력, 객체 입출력 등의 기능을 제공한다. 보조 스트림을 생성할 때는 자신이 연결될 스트림을 다음과 같이 생성자의 매개값으로 받는다.

InputStream is = System.in;
InputStreamReader reader = new InputStreamReader(is);
BufferedReader br = new BufferedReader(reader)

InputStreamReader, OutputStreamReader

소스 스트림이 바이트 기반 스트림(OutputStream,FileInputStream 등등)이면서 입출력 데이터가 문자라면 Reader와 Writer로 변환해서 사용하는 것을 고려해야 한다. 왜냐하면 Reader와 Writer는 문자 단위로 입출력하기 때문에 바이트 기반 스트림보다는 편리하고, 문자셋의 종류를 지정할 수 있기 때문이다.

InputStream is = System.in;
Reader reader = new InputStreamReader(is);

FileInputStream fis = new FileInputStream("C:/Temp/file.txt");
Reader reader = new InputStreamReader(fis);
//OutputStream또 마찬가지.

BufferedInputStream과 BufferedReader

프로그램의 실행 성능은 입출력이 가장 늦은 장치를 따라간다. CPU가 아무리 좋아도 하드디스크의 입출력이 늦어지면 프로그램의 실행 성능은 하드디스크의 처리 속도에 맞춰진다. 그나마 실행 성능을 향상시킬 수 있는 것이 buffer이다. 데이터가 쌓이길 기다렸다가 꽉 차면 데이터를 한 꺼번에 하드디스크로 보내서 출력 횟수를 줄여준다.

BufferedInputStream은 바이트 입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림이고, BufferedReader는 문자 입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림이다. 입력 소스로부터 자신의 내부 버퍼 크기만큼 데이터를 미리 읽고 버퍼에 저장해둔다

BufferedInputStream bis = new BufferedInputStream(바이트입력스트림);
BufferedReader br = new BufferedReader(문자 입력 스트림);

BufferedInputStream과 BufferedReader 보조 스트림은 다음과 같이 생성자의 매개값으로 준 입력 스트림과 연결되어 8192 내부 버퍼 사이즈를 가지게 된다. 최대, 8192바이트, 8192 문자가 저장될 수 있다. 데이터를 읽어들이는 방법이 InputStream이나 Reader와 같다.

BufferedOutputStream과 BufferedWriter

원리가 BufferedInputStream과 BufferedReader와 같다. 다만 버퍼가 가득 찼을 때만 출력을 하기 때문에 마지막 자투리 데이터는 flush()를 통해서 버퍼에 잔류하고 있는 데이터를 모두 보내야 한다.

기본 타입 입출력 보조 스트림

바이트 스트림은 바이트 단위로 입출력하기 때문에 기본 데이터 타입인 boolean, char, int 등등의 단위로 입출력할 수 없다. 그러나 DataInputStream과 DataOutputStream 보조 스트림을 연결하면 기본 데이터 타입으로 입출력이 가능하다.

DataInputStream dis = new DataInputStream(바이트입력스트림)
DataOutputStream dis = new DataOutputStream(바이트입력스트림)

readBoolean(), readChar(), readInt() 등등이다. 이 메소드들로 입출력할 때 주의할 점이 있는데 데이터 타입의 크기가 모두 다르므로 DataOutputStream으로 출력한 데이터를 다시 DataInputStream으로 읽어올 때는 출력한 순서와 동일한 순서로 읽어야 한다. (int -> boolean -> double이면 읽을 때도 int -> boolean -> double)

객체 입출력 보조 스트림

자바는 메모리에 생성된 객체를 파일 또는 네트워크로 출력할 수 있다. 객체는 문자가 아니기 때문에 바이트 기반 스트림으로 출력해야 한다. 객체를 출력하기 위해서는 객체의 데이터(필드값)를 일렬로 늘어선 연속적인 바이트로 변경해야 하는데 이것을 객체 직렬화(Serialization)이라고 한다. 반대로 파일에 저장되어 있거나 네트워크에서 전송된 객체를 읽을 때 연속적인 바이트를 객체로 복원하는데 이것을 역직렬화(Deserialization)라고 한다.

ObjectInputStream, ObjectOutputStream

ObjectInputStream은 바이트 출력 스트림과 연결되어 객체를 직렬화하는 역할을 하고, ObjectOutputStream은 바이트 입력 스트림과 연결되어 객체로 역직렬화하는 역할을 한다.

ObjectInputStream ois = new ObjectInputStream(바이트입력스트림);
ObjectOutputStream oos = new ObjectOutputStream(바이트출력스트림);

ObjectOutputStream으로 객체를 직렬화하기 위해서는 writeObject() 메소드를 사용한다.
oos.writeObject(객체);

반대로 ObjectInputStream의 readObject() 메소드는 입력 스트림에서 읽은 바이트를 역직렬화해서 객체로 생성한다. readObject() 메소드의 리턴 타입은 Object 타입이기 때문에 객체 원래의 타입으로 변환해야 한다.
객체타입 변수 = (객체타입) ois.readObject();

직렬화가 가능한 클래스(Serializable)

자바는 Serializable 인터페이스를 구현한 클래스만 직렬화할 수 있도록 제한하고 있다. Serializable 인터페이스는 필드나 메소드가 없는 빈 인터페이스지만, 객체를 직렬화할 때 priavate 필드를 포함한 모든 필드를 바이트로 변환해도 좋다는 표시 역할을 한다.

객체를 직렬화하면 바이트로 변환되는 것은 필드들이고, 생성자 및 메소드는 직렬화에 포함되지 않다. 따라서 역직렬화할 때는 필드의 값만 복원된다. 그리고 필드에 transient가 붙어있을 경우에는 직렬화가 되지 않는다.

serialVersionUID 필드

직렬화된 객체를 역직렬화할 때는 직렬화했을 때와 같은 클래스를 사용해야 한다.

클래스 이름이 같더라도 클래스의 내용이 변경되면, 역직렬화는 실패한다.

serialVersionUID는 같은 클래스임을 알려주는 식별자 역할을 한다. Serializable 인터페이스를 구현한 클래스를 컴파일하면 자동적으로 serialVersionUID 정적 필드가 추가된다.

writeObject()와 readObject() 메소드

두 클래스가 상속관계에 있다고 가정해보자.

부모 클래스가 Serializable 인터페이스를 구현하고 있으면 자식 클래스는 Serializable 인터페이스를 구현하지 않아도 자식 객체를 직렬화하면 부모 필드 및 자식 필드가 모두 직렬화된다.

반대로 부모 클래스가 Serializable 인터페이스를 구현하고 있지 않으면 자식 클래스만 Serializable을 구현하고 있다면 자식 객체를 직렬화할 때 부모의 필드는 직렬화에서 제외된다. 이 경우, 부모 클래스의 필드를 직렬화하고 싶다면 두 가지 선택이 있다.

  1. 부모 클래스가 Serializable 인터페이스를 구현하도록 한다.

  2. 자식 클래스에서 writeObject(), readObject() 메소드를 선언해서 부모 객체의 필드를 직접 출력시켜준다.

첫 번째 방법이 좋지만, 부모 클래스의 소스를 수정할 수 없는 경우에는 두 번째 방법을 사용해야 한다. writeObject() 메소드는 직렬화될 때 자동으로 호출되고, readObject() 메소드는 역직렬화될 때 자동적으로 호출된다.

private void writeObject(ObjectOutputStream out) throws IOException {
out.writeXXX(부모필드); //부모객체의 필드값을 출력함
...
out.defaultWriteObject();// 자식 객체의 필드값을 직렬화
}

private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException {
부모필드 = in.readXXX(); //부모객체의 필드값을 읽어옴
...
in.defaultReadObject();// 자식 객체의 필드값을 역직렬화
}

두 메소드를 작성할 때 주의할 점은 접근 제한자가 private이 아니면 자동호출되지 않기 때문에 반드시 private을 붙여줘야 한다.

전체예제

Parent.java

//Serializable을 구현하지 않은 부모 클래스.
public class Parent {
 public String field1;     //부모 클래스에서 간단한 필드 1개
}

Child.java

//Serializable을 구현한 자식 클래스
public class Child extends Parent implements Serializable{
  //자식 필드
 public String field2;    
 int field3;
 transient char field4;     //transient를 썼으므로 직렬화에서 제외.
 
 //writeObject()와 readObject()로 부모 객체의 필드를 직접 출력시킨다. 무조건 private을 써줘야 한다.
 private void writeObject(ObjectOutputStream out) throws IOException{
   out.writeUTF(field1);   //부모 객체의 필드 값을 출력
   out.defaultWriteObject();   //자식 객체의 필드값을 직렬화
}
 
 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
   field1 = in.readUTF();  //부모 객체의 필드 값을 읽어옴
   in.defaultReadObject(); //자식 객체의 필드값을 역직렬화
}
}

Fileexample.java

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

public class FileExample {

 public static void main(String[] args) throws IOException, ClassNotFoundException {
   
   //시간을 재기 위한 간단한 변수
   long start = 0;
   long end = 0;
   
   //디렉토리를 만들고, file을 만들기 위함.
   File dir = new File("/home/user_name/Desktop/filehello");
   File file1 = new File("/home/user_name/Desktop/filehello/hello1.txt");
   
   //디렉토리나 file이 존재하지 않으면 새로 만든다.
   if(dir.exists() == false) { dir.mkdirs(); }
   if(file1.exists() == false) { file1.createNewFile(); }
   
   //바이트 기반 파일 출력 스트림.
   FileOutputStream fos = new FileOutputStream("/home/user_name/Desktop/filehello/hello1.txt");
   //더 빠르게 하기 위해서 Buffered 스트림으로 해줌.
   BufferedOutputStream bos = new BufferedOutputStream(fos);
   //바이트 출력 스트림을 문자 출력 스트림(Writer)으로 변환.
   Writer writer = new OutputStreamWriter(bos);
   
   String data = "hello world1\r\nhello world2";
   
   //그냥 시간 측정
   start = System.currentTimeMillis();
   writer.write(data);
   end = System.currentTimeMillis();
   System.out.println((end - start) + "ms");
   
   writer.flush();     //버퍼를 썼으니 한 번 비워주기.
   writer.close();
   fos.close();
   bos.close();
   
   //문자 기반 파일 입력 스트림으로 읽어오기.
   FileReader fr = new FileReader("/home/user_name/Desktop/filehello/hello1.txt");
   //빠르게 해주기 위해서 buffered로.
   BufferedReader br = new BufferedReader(fr);

   
   start = System.currentTimeMillis();
   int readCharNo;
   char[] cbuf = new char[100];
   //계속 한 문자씩 받아오다가 맨 끝인 -1이 오면 종료한다. readLine은 \n이 나올 때 끝나서 개행이 있을 때 전체를 출력하지 못 함.
   while ((readCharNo = br.read(cbuf)) != -1) {    
     String str = new String(cbuf, 0, readCharNo);
     System.out.print(str);
  }
   System.out.println();
   end = System.currentTimeMillis();
   System.out.println((end - start) + "ms");
   
   fr.close();
   br.close();
   
   //객체이므로 바이트 기반 출력 스트림
   FileOutputStream foso = new FileOutputStream("/home/user_name/Desktop/filehello/Object.dat");
   //객체 출력 스트림
   ObjectOutputStream oos = new ObjectOutputStream(foso);
   
   //Child 클래스는 Serializable이 구현되어있으므로 직렬화 가능.
   Child child = new Child();
   child.field1 = "hello1";
   child.field2 = "hello2";
   child.field3 = 3;
   child.field4 = 'a';
   //직렬화를 해준다.그러면 바이트 데이터들이 일렬로 되어있다.
   oos.writeObject(child);
   oos.flush();
   oos.close();
   foso.close();
   
   //바이트 기반 입력 스트림
   FileInputStream fiso = new FileInputStream("/home/user_name/Desktop/filehello/Object.dat");
   ObjectInputStream ois = new ObjectInputStream(fiso);
   //readObject() 반환 값은 Object 이므로 Child로 형변환. 역 직렬화를 해준다.
   Child kk = (Child) ois.readObject();
   System.out.println(kk.field1);
   System.out.println(kk.field2);
   System.out.println(kk.field3);
   System.out.println(kk.field4);
}
}

Comments