Java 재활 훈련 14일차 - 데이터 입출력

0

java

목록 보기
14/18

Data 입출력

입출력 스트림

java는 입력 스트림과 출력 스트림을 통해 데이터를 입출력한다. stream은 단방향으로 데이터가 흐르는 것을 말하는데, 다음과 같이 data는 출발지에서 나와 도착지로 흘러들어간다.

----출발지----      ----Program-----    ---도착지----
|1. 키보드   |      |              |    |1. 모니터  |
|2. 파일     | ---> | 도착지 출발지 |--> |2. 파일    |
|3. 프로그램 |      ----------------    |3. 프로그램 |
-------------                           ------------

프로그램을 기준으로 데이터가 들어오면 입력 스트림, 데이터가 나가면 출력 스트림이 된다. 프로그램이 다른 프로그램과 데이터를 교환하려면 양쪽 모두 입력 스트림과 출력 스트림이 필요하다.

조심해야할 것은 입출력 기준이 항상 프로그램 기준이라는 것이다.

어떤 데이터를 입출력하느냐에 따라 stream은 다음 두 종류로 구분할 수 있다.
1. byte stream: 그림, 멀티미디어, 문자 등 모든 종류의 데이터를 입출력할 때 사용
2. 문자 stream: 문자만 입출력할 때 사용

아래는 byte stream의 클래스이다.

구분byte 입력 스트림byte 출력 스트림
최상위 클래스InputStreamOutputStream
하위 클래스XXXInputStream(FileInputStream)XXXOutputStream(FileOutputStream)

아래는 문자 stream의 클래스이다.

구분문자 입력 스트림문자 출력 스트림
최상위 클래스ReaderWriter
하위 클래스XXXReader(FileReader)XXXWriter(FileWriter)

byte 출력 스트림

                        OutputStream
                            |
        ---------------------------------------------------------
        |               |                   |                   |
FileOutPutStream    PrintStream     BufferedOutputStream    DataOutputStream

OutputStream class에는 모든 바이트 출력 스트림이 기본적으로 가져야할 메서드가 정의되어 있다. 다음은 OutputStream class의 주요 메서드이다.

메서드설명
void write(int b)1byte를 출력
void write(byte[] b)매개값으로 주어진 배열 b의 모든 바이트를 출력
void write(byte[] b, int off, int len)매개값으로 주어진 배열 b[off]부터 len개의 바이트를 출력
void flush()출력 버퍼에 잔류하는 모든 바이트를 출력
void close()출력 스트림을 닫고 사용 메모리 해제

아래는 write를 사용하여 ./test1.txt 파일에 데이터를 쓰는 것을 볼 수 있다.

public class Main {
    public static void main(String[] args) {
        try {
            OutputStream os = new FileOutputStream("./test1.txt");
            byte[] arr = {10, 20, 30};

            os.write(arr);

            os.flush();
            os.close();
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

OutputStream에는 내부에 buffer가 있어서, buffer가 가득차면 그때야 출력을 한다. 즉, os.write를 실행해도 내부 buffer에 데이터를 넣고만 있다는 것이다. 따라서, buffer를 비워주어 buffer내부의 데이터를 정해진 file로 출력시켜주기 위해서는 flush를 사용해야한다. 또한, close를 실행하여 반드시 명시적으로 파일 디스크립터에 대한 자원을 반환해주어야 한다.

byte 입력 스트림

InputStream은 바이트 입력 스트림의 최상위 클래스로, 추상 클래스이다. 모든 바이트 입력 스트림은 InputStream class를 상속받아 만들어진다.

                    InputStream
                        |
    ---------------------------------------------
    |                   |                       |
FileInputStream     BufferedInputStream     DataInputStream

InputStream 클래스에는 바이트 입력 스트림이 기본적으로 가져야 할 메서드가 정의되어 있다. 다음은 InputStream 클래스의 주요 메서드이다.

메서드설명
int read()1byte를 읽은 후 바이트를 반환
int read(byte[] b)읽은 바이트를 매개값으로 주어진 배열에 저장 후 읽은 바이트 수를 리턴
void close()입력 스트림을 닫고 사용 메모리 해제

다음은 우리가 만든 test1.txt파일을 InputStream으로 읽어들이는 코드이다.

public class Main {
    public static void main(String[] args) {
        InputStream is = null;
        try {
            is = new FileInputStream("./test1.txt");
            byte[] data = new byte[100];
            while (true) {
                int num = is.read(data);
                if(num == -1) break;

                for(int i =0; i< num;i ++) {
                    System.out.println(data[i]);
                }
            }
            is.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

read는 입력받은 byte 배열의 크기만큼만 데이터를 읽어 들일 수 있다. 따라서, 100 바이트씩만 읽어들이기 때문에 while으로 반복해야하는 것이다. 이때 read의 반환값이 -1이라면 더이상 읽을 내용이 없다는 것이므로 순회를 종료한다.

문자 입출력 스트림

바이트 입출력인 InputStreamOutputStream에 대응하는 문자 입출력 스트림으로 ReaderWriter이 있다. 입출력되는 단위가 문자인 것을 제외하고는 사용 방법이 동일하다.

Writer는 출력 스트림의 최상위 클래스로 추상 클래스이다. 모든 문자 출력 스트림 클래스는 Writer 클래스를 상속받아서 만들어진다.

                            Writer
                              |
    ------------------------------------------------------
    |               |               |                    |
FileWriter  BufferedWriter      PrintWriter     OutputStreamWriter

Writer 클래스에는 모든 문자 출력 스트림이 기본적으로 가져야 할 메서드가 정의되어 있다. Writer 클래스의 주요 메서드는 다음과 같다.

메서드설명
void write(int c)매개값으로 주어진 한 문자를 출력
void write(char[] cbuf)매개값으로 주어진 배열의 모든 문자를 출력
void write(char[] cbuf, int off, int len)매개값을 주어진 배열에서 cbuf[off]부터 len개 까지의 문자를 출력
void write(String str)매개값으로 주어진 문자열을 출력
void write(String str, int off, int len)매개값으로 주어진 문자열에서 off 순번부터 len개 까지의 문자를 출력
void flush()버퍼에 잔류하는 모든 문자를 출력
void close()출력 스트림을 닫고 사용 메모리 해제

WriterOutputStream과 사용 방법은 동일하지만, 출력 단위가 문자이다. 그리고 문자열을 출력하는 메서드를 추가로 제공하는 것이다.

public class Main {
    public static void main(String[] args) {
        try {
            Writer writer = new FileWriter("./test2.txt");
            String data = "hello myworld!";
            writer.write(data);
            writer.flush();
            writer.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

test2.txt 파일이 생기고 다음의 결과를 갖게 된다.

hello myworld!

Reader는 문자 입력 스트림의 최상위 클래스로 추상 클래스이다. 모든 문자 입력 스트림 클래스는 Reader 클래스를 상속받아서 만들어진다.

                  Reader
                    |
    -----------------------------------------
    |                   |                   |
FileReader      BufferedReader      InputStreamReader

Reader 클래스에는 문자 입력 스트림이 기본적으로 가져야 할 메서드가 정의되어 있다. 다음은 Reader 클래스의 주요 메서드이다.

메서드설명
int read()1개의 문자를 읽고 반환
int read(char[] cbuf)읽은 문자들을 매개값으로 주어진 문자 배열에 저장하고 읽은 문자 수를 반환
void close()입력 스트림을 닫고, 사용 메모리 해제

ReaderInputStream과 사용 방법은 동일하지만, 출력 단위가 문자(char)이다.

public class Main {
    public static void main(String[] args) {
        try {
            Reader reader = new FileReader("./test2.txt");
            char[] buf = new char[3];
            while (true) {
                int len = reader.read(buf);
                if(len == -1) break;
                for(int i = 0; i< len; i++) {
                    System.out.print(buf[i]);
                }
            }
            reader.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

FileReader에 읽어 들일 file의 path를 적어주고 read 메서드에 char[]을 넣어주면 char[]의 크기만큼 읽어들인다. 위의 경우 buf의 사이즈가 3이므로 3개씩 읽어들이는 것이다.

보조 스트림

보조 스트림은 다른 스트림과 연결되어 여러 가지 편리한 기능을 제공해주는 스트림을 말한다. 보조 스트림은 자체적으로 입출력을 수행할 수 없기 때문에, 입출력 source로부터 직접 생성된 입출력 스트림에 연결해서 사용해야 한다.

-----------                                           ------------
|입력스트림| --보조 스트림---> Program --보조 스트림---> |츨력 스트림|
-----------                                           ------------

입출력 스트림에 보조 스트림을 연결하려면 보조 스트림을 생성할 때 생성자 매개값으로 입출력 스트림을 제공하면 된다.

보조스트림 변수 = new 보조스트림(입출력스트림);

가령, 바이트 입력 스트림인 FileInputStreamInputStreamReader 보조 스트림을 연결하는 코드는 다음과 같다.

InputStream is = new FileInputStream("...");
InputStreamReader reader = new InputStreamReader(is);

보조스트림은 또 다른 보조스트림과 연결되어 stream 체인으로 구성할 수도 있다. 가령 문자 변환 보조 스트림인 InputStreamReaderBufferedReader 보조 스트림을 연결하는 코드는 다음과 같다.

InputStream is = new FileInputStream("...");
InputStreamReader reader = new InputStreamReader(is);
BufferedReader br = new BufferedReader(reader);

자주 사용되는 보조 스트림은 다음과 같다.

보조 스트림기능
InputStreamReader바이트 스트림을 문자 스트림으로 변환
BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter입출력 성능 향상
DataInputStream, DataOutputStream기본 타입 데이터 입출력
PrintStream, PrintWriter줄바꿈 처리 및 형식화된 문자열 출력
ObjectInputStream, ObjectOutputStream객체 입출력

이제부터 보조 스트림에 대해서 하나씩 알아보자.

문자 변환 스트림

바이트 스트림(InputStream, OutputStream)에서 입출력할 때 데이터가 문자라면 문자 스트림(Reader, Writer)로 변환해서 사용하는 것이 좋다. 그 이유는 문자로 바로 입출력하는 편리함이 있고, 문자셋의 종류를 지정할 수 있기 때문이다.

InputStreamReader로 변환하는 것은 InputStreamReader 보조 스트림을 연결하면 된다.

                      -----InputStreamReader---
byte ---> InputStream | ------> Reader -----> | --> program(문자)
                      -------------------------

다음은 InputStreamReader로 변환하는 코드이다.

InputStream is = new FileInputStream("./test2.txt");
Reader reader = new InputStreamReader(is);

OutputStreamWriter로 변환하려면 OutputStreamWriter 보조 스트림을 연결하면 된다.

                  ---OutputStreamWriter---
program(문자) --> |     Writer  -------> | --> OutputStream --> byte 
                  ------------------------

다음은 OutputStreamWriter로 변환하는 코드를 보여준다.

OutputStream os = new FileOutputStream("./test.txt");
Writer writer = new OutputStreamWriter(os);

참고로 FileOutputStreamOutputStreamWriter를 연결하지 않고, FileWriter를 직접 생성할 수 있다. FileWriterOutputStreamWriter의 자식 클래스이다. 이것은 FileWriter가 내부적으로 FileOutputStreamOutputStreamWriter 보조 스트림을 연결한 것이라고 볼 수 있다.

아래는 source stream은 byte 기반 FileOutputStreamFileInputStream이지만, 문자 기반 스트림인 WriterReader로 변환해서 사용한다.

public class Main {
    public static void main(String[] args) {
        try {
            Main.write("bye world");
            String data = Main.read();
            System.out.println(data); // bye world
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void write(String str) throws Exception {
        OutputStream os = new FileOutputStream("./test3.txt");
        Writer writer = new OutputStreamWriter(os, "UTF-8");
        writer.write(str);
        writer.flush();
        writer.close();
    }

    public static String read() throws Exception {
        InputStream is = new FileInputStream("./test3.txt");
        Reader reader = new InputStreamReader(is, "UTF-8");

        char[] data = new char[100];
        int num =reader.read(data);
        reader.close();

        String str = new String(data, 0, num);
        return str;
    }
}

write는 byte stream인 FileOutputStream으로 파일을 열었지만, 데이터를 출력할 때 OutputStreamWriter를 사용하여 문자 stream인 Writer로 처리한다.

read는 byte stream인 FileIntputStream으로 파일을 열었지만, 데이터를 입력할 때 InputStreamReader를 사용하여 문자 stream인 Reader로 처리한다.

성능 향상 스트림

하드 디스크의 성능이 너무 안좋으면, CPU와 Memory 성능과는 상관없이 하드디스크에 대한 병목 현상이 생길 수 밖에 없다. 이러한 문제에 대해서 memory buffer를 만들어 완충제 역할을 하면 실행 성능을 어느정도 향상 시킬 수 있다.

출력 스트림의 경우 직접 하드 디스크에 데이터를 보내지 않고, 메모리 버퍼에 데이터를 보냄으로써 출력 속도를 향상시킬 수 있다. 버퍼는 데이터가 쌓이기를 기다렸다가 꽉 차게되면 데이터를 한꺼번에 하드디스크로 보냄으로써 출력 회수를 줄여준다.

---Program---             ---메모리 버퍼---
|   data    |--고속전송-->|     메모리    | --buffer의 데이터를 모두 전송---> 하드디스크
-------------             ----------------

입력 스트림에서도 buffer를 사용하면 읽기 성능이 좋아진다. 하드 디스크로부터 직접 읽는 것 보다는 메모리 버퍼로부터 읽는 것이 빠르다.

---Program---             ---메모리 버퍼---
|   data    |<--고속읽기--|     메모리    | --buffer에 데이터를 전송 <--- 하드디스크
-------------             ----------------

위와 같이 메모리 버퍼를 제공하여 program의 실행 성능을 향상시키는 보조 스트림이 있다. 바이트 스트림에는 BufferedInputStream, BufferedOutputStream이 있고, 문자 스트림에는 BufferedReader, BufferedWriter이 있다. 보조 스트림을 연결하는 방법은 다음과 같다.

BufferedInputStream bis = new BufferedInputStream(바이트 입력 스트림);
BufferedOutputStream bos = new BufferedOutputStream(바이트 출력 스트림);

BufferedReader br = new BufferedReader(문자 입력 스트림);
BufferedWriter bw = new BufferedWriter(문자 출력 스트림);

참고로 Buffered~ stream도 close를 해주어야 하는데, close를 해주면 wrapping하고 있던 바이트 입출력 스트림, 문자 입출력 스트림 모두 close된다.

아래의 예제는 BufferedInputStream, BufferedOutputStream의 사용 방법이다.

public class Main {
    public static void main(String[] args) {
        try {
            FileOutputStream fos = new FileOutputStream("./test4.txt");
            BufferedOutputStream bos = new BufferedOutputStream(fos);

            byte[] data = {'h', 'e' , 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
            bos.write(data);
            bos.flush();
            bos.close();

            FileInputStream fis = new FileInputStream("./test4.txt");
            BufferedInputStream bis = new BufferedInputStream(fis);

            byte[] buf = new byte[100];
            while(true) {
                int num = bis.read(buf);
                if(num == -1) break;
                for(int i = 0; i < num; i++) {
                    System.out.print((char) buf[i]); // hello world
                }
            }

            bis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

hello worldBufferedOutputStream으로 test4.txt에 쓰고, BufferedInputStream로 읽어내는 코드이다.

문자 스트림에도 성능을 위해 buffered를 사용할 수 있는데, 문자 입력 스트림 ReaderBufferedReader를 연결하면 성능 향상 뿐만 아니라, 행 단위로 문자열을 읽는 매우 편리한 readLine 메서드를 제공한다는 것이다. 다음은 문자 파일을 행 단위로 읽는 코드를 보여준다.

BufferedReader br = new BufferedReader(new FileReader("..."));
while(true) {
    String str = br.readLine(); // 파일에서 한 행씩 읽음
    if(str == null) break; // 더 이상 읽을 행이 없을 경우 while문 종료 
}

다음은 한 행씩읽으면서 행 단위 번호를 붙여주는 코드이다.

  • test4.txt
hello world
bye world
new world!
  • Main
public class Main {
    public static void main(String[] args) {
        try {
            BufferedReader br = new BufferedReader(new FileReader("./test4.txt"));
            int lineNo = 1;
            while(true) {
                String str = br.readLine();
                if (str == null) break;
                System.out.println(lineNo + "\t" + str);
                lineNo++;
            }
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

결과는 아래와 같다.

1	hello world
2	bye world
3	new world!

BufferedReader를 닫으면 FileReader도 닫힌다.

기본 타입 스트림

바이트 스트림에 DataInputStreamDataOutputStream 보조 스트림을 연결하면 기본 타입인 boolean, char, short, int, long, float, double 값을 입출력할 수 있다.

byte --->InputStream ---> DataInputStream ---> program(기본 데이터 타입, int, double...)  ---> DataOutputStream ---> OutputStream --> byte

다음은 DataInputStreamDataOutputStream 보조 스트림을 연결하는 코드이다.

DataInputStream dis = new DataInputStream("바이트 입력 스트림");
DataOutputStream dos = new DataOutputStream("바이트 출력 스트림");

DataInputStreamDataOutputStream에서 각 primitive data type에 대해서 메서드를 사용하면 된다. readBoolean, writeBoolean, readInt, writeInt 등이 있다.

DataInputStream, DataOutputStream을 사용할 때 한 가지 주의할 점이 있다. 데이터 타입의 크기가 모두 다르므로 DataOutputStream으로 출력한 데이터를 다시 DataInputStream으로 읽어 올 때에는 출력한 순서와 동일한 순서로 읽어야 한다는 것이다. 가령 출력할 때 순서가 int -> boolean -> double이라면 읽을 때의 순서도 int -> boolean -> double이어야 한다.

다음은 이름, 성적, 순위 순으로 파일에 출력하고 다시 파일로부터 읽는 방법을 보여준다.

  • Main
public class Main {
    public static void main(String[] args) throws Exception {
        FileOutputStream fos = new FileOutputStream("./test4.txt");
        DataOutputStream dos = new DataOutputStream(fos);

        dos.writeUTF("Hong");
        dos.writeDouble(95.5);
        dos.writeInt(1);

        dos.writeUTF("KIM");
        dos.writeDouble(90.3);
        dos.writeInt(2);

        dos.flush();
        dos.close();

        FileInputStream fis = new FileInputStream("./test4.txt");
        DataInputStream dis = new DataInputStream(fis);

        for(int i = 0; i< 2; i++) {
            String name = dis.readUTF();
            double score = dis.readDouble();
            int order = dis.readInt();

            System.out.println(name + ": "+ score +": " + order);
        }

        dis.close();
    }
}

출력 결과는 다음과 같다.

Hong: 95.5: 1
KIM: 90.3: 2

물론 이렇게 다양한 타입으로 쓰는 것을 추천하진 않는다. 그냥 writeUTF, readUTF만 하고 int와 같은 숫자타입들은 미리 문자로 바꿔놓고, 쓰도록 하자. 왜냐면 output도 그렇고, input도 그렇고 어떤 순서가 명확하지 않은 채로 읽는 것은 분명 실수가 발생할 수 밖에 없다.

객체 스트림

자바는 메모리에 생성된 객체를 파일 또는 네트워크로 출력할 수 있다. 객체를 출력하려면 field값을 일렬로 늘어선 바이트로 변경해야하는데, 이를 직렬화(Serializable)이라고 한다. 반대로 직렬화된 바이트를 객체의 field값으로 복원하는 것을 역직렬화(Deserializable)이라고 한다.

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

byte ---> InputStream ---> ObjectInputStream(역직렬화) ---> 객체 (program 내부) ---> ObjectOutputStream(직렬화) ---> OutputStream ---> byte

다음은 ObjectInputStreamObjectOutputStream 보조 스트림을 연결하는 코드이다.

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

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

oos.writeObject(객체);

반대로 ObjectInputStreamreadObject 메서드는 읽은 바이트를 역직렬화해서 객체로 생성한다.

객체 타입 변수 = (객체타입) ois.readObject();

단, 직렬화(Serialize), 역직렬화(Deserialize)를 객체에 적용하기 위해서는 해당 객체가 Serializable 인터페이스를 구현하고 있어야한다.

public class Something implements Serializable {

}

다음은 다양한 객체를 파일에 저장하고 다시 파일로부터 읽어 객체로 복원하는 예제이다. 복수의 객체를 저장할 경우, 출력된 객체 순서와 동일한 순서로 객체를 읽어야 한다.

  • Member
public class Member implements Serializable {
    private static final long serialVersionUID = -6221903048729L;
    public String name;
    public int age;

    public Member(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Member{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
  • Product
public class Product implements Serializable {
    private static final long serialVersionUID = -6221903048730L;
    public String name;
    public int price;

    public Product(String name, int price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String toString() {
        return "Product{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

MemberProduct 모두 Serializable 인터페이스를 implements하고 있는 것을 볼 수 있다. 위의 예제에서 볼 수 있듯이 Serializable 인터페이스에는 별다른 추상 메서드가 없다. 그저 직렬화-역직렬화가 가능하다는 것만 알려주는 것이다.

  • Main
public class Main {
    public static void main(String[] args) throws Exception {
        FileOutputStream fos = new FileOutputStream("./object.dat");
        ObjectOutputStream oos = new ObjectOutputStream(fos);

        Member kim = new Member("park",30);
        Product notebook = new Product("notebook", 1000);
        int[] arr1 = {1, 2,3};

        oos.writeObject(kim);
        oos.writeObject(notebook);
        oos.writeObject(arr1);

        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("./object.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);

        Member m2 = (Member) ois.readObject();
        Product p2 = (Product) ois.readObject();
        int[] arr2 = (int[]) ois.readObject();

        ois.close();

        System.out.println(m2); // Member{name='park', age=30}
        System.out.println(p2); // Product{name='notebook', price=1000}
        for(int num: arr2) {
            System.out.print(num); // 123
        }
    }
}

ObjectOutputStream을 사용하여 object.datMember, Product, int[] 객체를 직렬화하여 넣고, ObjectInputStream으로 역직렬화하여 code에 객체로 가져오는 것을 볼 수 있다. 이때 readObject의 반환 타입이 Object이므로 원하는 객체의 타입으로 명시적 변환시켜주어야 한다.

참고로 객체가 직렬화될 때 instance의 모든 field값들은 직렬화 대상이다. 즉 public 뿐만 아니라 private도 직렬화된다. 단, instance의 static field와 transient field에 대해서는 직렬화되지 않는다.

public class XXX implements Serializable {
    // 직렬화 대상
    public int field1;
    protected int field2;
    int field3;
    private int field4;

    // 직렬화 제외
    public static int field5; // 정적 필드는 직렬화 제외
    transient int field6; // transient로 선언된 필드는 직렬화 제외
}

위의 예제를 보면 MemberProductSerialVersionUID field를 사용한 것을 볼 수 있다. 직렬화할 때 사용된 클래스와 역직렬화할 때 사용된 클래스는 기본적으로 동일한 클래스여야 한다. 만약 클래스의 이름이 같더라도 클래스의 내용이 다르면 역직렬화에 실패한다.

다음 코드를 보자, 첫번째 Member 클래스로 생성한 객체를 직렬화하면, 두번째 Member 클래스로 역직렬화가 불가능하다. 그 이유는 두번째 Member 클래스에는 field3가 있기 때문이다.

public class Member implements Serializable {
    int field1;
    int field2;
}

...


public class Member implements Serializable {
    int field1;
    int field2;
    int field3;
}

class 내용이 다르다 할지라도 직렬화된 field를 공통으로 포함하고 있다면 역직렬화할 수 있는 방법이 있다. 두 클래스가 동일한 serialVersionUID 상수 값을 가지고 있으면 된다.

public class Member implements Serializable {
    private static final long serialVersionUID = -6221903048729L;
    int field1;
    int field2;
}

...


public class Member implements Serializable {
    private static final long serialVersionUID = -6221903048729L;
    int field1;
    int field2;
    int field3;
}

serialVersionUID의 값은 개발자가 임의로 줄 수 있다. 단, unique함을 보장해야한다. 대부분의 ide에서 자동 생성을 제공하므로 사용하도록 하자.

File과 Files 클래스

java는 패키지 파일과 디렉터리 정보를 가지고 있는 FileFiles 클래스를 제공한다. FilesFile을 개선한 클래스로 생각하면 된다.

File 클래스

File 클래스로부터 File 객체를 생성하는 방법은 다음과 같다.

File file = new File("...path");

경ㄹ 구분자는 운영체제마다 약간 다르다. 윈도우는 //, /이고, 리눅스, 맥은 /을 사용한다.

File file = new File(".../path/file.txt");

File 객체를 생성했다고 해서 파일이나 디렉토리가 생성되는 것은 아니다. 또한, 경로에 실제 파일이나 디렉토리가 없더라도 Exception이 발생하지 않는다. 파일이나 디렉토리가 실제 있는지 확인하고 싶다면 File 객체를 새성하고 나서 exists() 메서드를 호출해보면 된다.

boolean isExist = file.exists(); // file이나 directory가 존재한다면 true를 반환

exists() 메서드가 false를 반환할 경우, 다음 메서드로 file 또는 directory를 생성할 수 있다.

메서드설명
boolean createNewFile()새로운 파일 생성
boolean mkdir()새로운 디렉터리 생성
boolean mkdirs()경로 상에 없는 모든 디렉터리를 생성

exists() 메서드의 반환값이 true라면 다음의 메서드를 사용할 수 있다.

메서드설명
boolean delete()file 또는 directory 삭제
boolean canExecute()실행할 수 있는 file인지 여부
boolean canRead()읽을 수 있는 파일인지 여부
boolean canWrite()수정 및 저장할 수 있는 파일인지 여부
String getName()파일의 이름을 반환
String getParent()부모 디렉터리를 반환
File getParentFile()부모 디렉터리를 File 객체로 생성 후 반환
String getPath()전체 경로를 반환
boolean isDirectory()디렉터리인지 여부
boolean isFile()파일인지 여부
boolean isHidden()숨김 파일인지 여부
long lastModified()마지막 수정 날짜 및 시간을 반환
long length()파일의 크기 반환
String[] list()디렉터리에 포함된 파일 및 서브 디렉터리 목록 전부를 String 배열로 반환
String[] list(FilenameFilter filter)디렉터리에 포함된 파일 및 서브 디렉터리 목록 중에 FilenameFilter에 맞는 것만 String 배열로 반환
File[] listFiles()디렉터리에 포함된 파일 및 서브 디렉터리 목록 전부를 File 배열로 반환
FIle[] listFiles(FilenameFilter filter)디렉터리에 포함된 파일 및 서브 디렉터리 목록 중에 FilenameFilter에 맞는 것만 File 배열로 반환

다음은 temp directory에 images directory를 생성하고 file1.txt, file2.txt, file3.txt 파일을 생성하고, temp directory에 있는 내용을 출력하는 예제이다.

public class Main {
    public static void main(String[] args) throws Exception {
        File dir = new File("./temp/images");
        File file1 = new File("./temp/file1.txt");
        File file2 = new File("./temp/file2.txt");
        File file3 = new File("./temp/file3.txt");

        if(dir.exists() == false) {
            dir.mkdirs();
        }

        if(file1.exists() == false) {
            file1.createNewFile();
        }

        if(file2.exists() == false) {
            file2.createNewFile();
        }

        if(file3.exists() == false) {
            file3.createNewFile();
        }

        File temp = new File("./temp");
        File[] contents = temp.listFiles();

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd a HH:mm");
        for(File file: contents) {
            System.out.printf("%-25s", sdf.format(new Date(file.lastModified())));
            if(file.isDirectory()) {
                System.out.printf("%-10s%-20s", "<DIR>", file.getName());
            } else {
                System.out.printf("%-10s%-20s", file.length(), file.getName());
            }
            System.out.println();
        }
    }
}

결과는 아래와 같다.

2025-02-17 PM 17:17      <DIR>     images              
2025-02-17 PM 17:17      0         file1.txt           
2025-02-17 PM 17:17      0         file3.txt           
2025-02-17 PM 17:17      0         file2.txt

파일 또는 directory의 정보를 얻기 위해 File 객체를 단독으로 사용할 수 잇찌만, 파일 입출력 스트림을 생성할 때 경로 제공 목적으로 사용되기도 한다.

File file = new File(".../path/image.txt");
FileInputStream fis = new FileInputStream(file);

File객체를 넘겨주기만 해도 된다.

Files 클래스

Files 클래스는 정적 메서드로 구성되어 있기 때문에 File 클래스처럼 객체로 만들 필요가 없다. Files의 정적 메서드는 운영체제의 파일시스템에게 파일 작업을 수행하도록 위임한다.

Files의 모든 정적 메서드들은 매개값으로 Path 객체를 받는다. Path객체는 파일이나 디렉터리를 찾기 위한 경로 정보를 가지고 있는데, 정적 메서드인 get() 메서드로 다음과 같이 얻을 수 있다.

Path path = Paths.get(String first, String ... more);

get() 메서드의 매개값은 파일 경로인데, 전체 경로를 한꺼번에 지정해도 좋고, 상위 디렉터리와 하위 디렉터리를 나열해서 지정해도 좋다. 다음은 ./temp/dir/file.txt 경로를 이용해서 Path 객체를 얻는 방법을 보여준다.

Path path = Paths.get("./temp/dir/file.txt");
Path path = Paths.get("./temp/dir","file.txt");
Path path = Paths.get("./temp","dir","file.txt");

헷갈리니까 첫번째 방법만 기억하자.

아래 예제는 Files 클래스를 이용해서 ./temp directory에 user.txt 파일을 생성하고 읽는 방법을 보여준다.

public class Main {
    public static void main(String[] args) {
        try {
            String data = "" + "id: winter\n" + "email: winter@mycompany.com\n";

            Path path = Paths.get("./temp/user.txt");
            Files.writeString(path, data,Charset.forName("UTF-8"));

            System.out.println("file type: " + Files.probeContentType(path));
            System.out.println("file size: " + Files.size(path) + "bytes");

            String contents = Files.readString(path, Charset.forName("UTF-8"));
            System.out.println(contents);
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Files의 모든 정적 메서드에서는 맨 앞에 Path 객체가 들어가는 것을 볼 수 있다.

출력 결과는 아래와 같다.

file type: text/plain
file size: 39bytes
id: winter
email: winter@mycompany.com

또한, project의 temp/user.txt에 가면 user.txt 파일이 만들어진 것을 볼 수 있다.

  • temp/user.txt
id: winter
email: winter@mycompany.com

0개의 댓글