직렬화란? 객체를 데이터 스트림으로 만드는 것을 뜻하는 용어이다.
객체에 저장된 데이터를 스트림에 쓰기(write)위해 연속적인(serial) 데이터로 변환하는 것을 의미한다. 반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(deserialization)라고한다.
객체를 저장한다는 것은 객체의 모든 인스턴스변수의 값을 저장한다는 것이다. 객체의 메서드는 모든 객체마다 동일하기에 메서드를 제외한 모든 인스턴스변수의 값을 저장하기만 하면 된다. 그리고 저장했던 객체를 다시 생성하려면, 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스변수에 저장하면 된다.
직렬화(스트림에 객체를 출력)에는 ObjectOutputStream을 사용하고 역직렬화(스트림으로부터 객체를 입력)에는 ObjectInputStream을 사용한다.
ObjectInputStream/ObjectOutputStream은 각각 InputStream과 OutputStream을 상속받지만 기반스트림을 필요로 하는 보조스트림들이다. 그래서 객체를 생성할 때 입출력(직렬화/역직렬화)할 스트림을 지정해 주어야 한다.
ObjectInputStream(InputStream in)
ObjectOutputStream(OutputStream out)
만약 파일에 객체를 저장(직렬화)하고 싶다면 다음과 같이 한다.
FileOutputStream fos = new FileOutputStream("objectfile.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(new UserInfo());
위 예제코드는 objectfile.ser이라는 파일에 UserInfo객체를 직렬화하여 저장한다.
출력할 스트림인 FileOutputStream을 생성하여 이를 기반스트림으로 하는 ObjectOutputStream을 생성하고 writeObject(Object obj)메서드를 사용해 객체를 출력하면, 객체가 파일에 직렬화되어 저장된다.
역직렬화 방법 역시 간단한데, 직렬화할 떄와는 달리 입력스트림을 사용하고 readObject()를 사용하여 저장된 데이터를 읽기만 하면 객체로 역직렬화된다.
다만 readObject()메서드의 반환타입이 Ojbect이기에 원래 객체의 타입으로 형변환 해줘야 한다.
FileInputStream fis = new FileInputStream("objectfile.ser");
ObjectInputStream in = new ObjectInputStream(fis);
UserInfo info = (UserInfo)in.readObject();
ObjectInputStream과 ObjectOutputStream에는 아래와 같은 여러 가지 타입값을 위한 입출력 메서드를 제공한다.
위의 메서드들은 직/역직렬화를 직접 구현할 때 주로 사용되며, defaultReadObject()와 defaultWriteObject()는 자동 직렬화를 수행한다.
객체의 직/역직렬화 작업은 객체의 모든 인스턴스변수가 참조하고 있는 모든 객체에 대한 것이기 때문에 상당히 복잡할 수 있고, 시간도 오래 걸린다. readObject()와 writeObject()를 사용한 자동 직렬화가 편리하기는 하지만 직렬화작업시간을 단축시키기 위해서는 객체의 클래스에 추가적으로 다음과 같은 2개의 메서드를 직접 구현해주어야 한다.
private void writeObject(ObjectOutputStream out) throws IOException {
// Write메서드를 사용해서 직렬화를 수행한다.
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// read메서드를 사용해서 역직렬화를 수행한다.
}
직렬화가 가능한 클래스를 만드는 방법은 간단한데 직렬화하고자 하는 클래스가 Serializable인터페이스를 구현하도록 하면 된다.
앞서 배운것 처럼 Serializable은 아무 내용이 없는 인터페이스이지만, 직렬화를 고려하여 작성한 클래스인지 판단한다.
public interface Serializable { }
public class User implements java.io.Serializable {
String name;
String password;
int age;
}
아래의 클래스는 직렬화시에 NotSerializableException이 발생한다. 그 이유는 직렬화 할 수 없는 클래스의 객체를 인스턴스 변수가 참조하고 있기 때문이다.
그런데 똑같이 Object타입의 인스턴스 변수더라도 실제로 어떤 인스턴스인지에 따라 직렬화 가능 여부가 나뉜다 아래 코드에서 str은 Object타입이지만 실제로는 String클래스(Serializable을 구현함)이기 때문에 직렬화가 가능하다.
public class User implements Serializable {
String name;
String password;
Object obj = new Object(); // 직렬화 불가!
Object str = new String("str"); // 직렬화 가능!
}
직렬화 하고자 하는 객체의 클래스에 직렬화가 안되는 객체에대한 참조를 하고 있다거나 직렬화 하면 안되는 변수가 있다면 제어자 transient를 붙여서 직렬화 대상에서 제외시킬 수 있다.
public class UserInfo implements Serializable {
String name;
transient String password; // 제외
int age;
transient Object obj = new Object(); // 제외
}
아래 UserInfo라는 객체를 여러개 만들어 UserInfo.ser라는 파일에 직렬화하여 저장한 후, 다시 역직렬화를 통해 불러오는 작업을 해 볼것이다.
UserInfo ↓
public class UserInfo implements Serializable {
String name;
String password;
int age;
public UserInfo() {
this("Unknown", "1111", 0);
}
public UserInfo(String name, String password, int age) {
this.name = name;
this.password = password;
this.age = age;
}
public String toString() {
return "(" + name + "," + password + "," + age + ")";
}
}
별거 없다. Serializable을 implements하여 직렬화를 구상한 클래스임을 밝히기만 했을 뿐이다.
이제 이 클래스를 이용해 객체를 만들고 해당 객체를 저장하거나 List에 담아서 저장 해 보자.
직렬화 저장 ↓
public class Ex15_20 {
public static void main(String[] args) {
try {
String fileName = "UserInfo.ser";
FileOutputStream fos = new FileOutputStream(fileName);
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream out = new ObjectOutputStream(bos);
UserInfo u1 = new UserInfo("JavaMan", "1234", 30);
UserInfo u2 = new UserInfo("JavaWoman", "4321", 26);
ArrayList<UserInfo> list = new ArrayList<UserInfo>();
list.add(u1);
list.add(u2);
// 직렬화
out.writeObject(u1);
out.writeObject(u2);
out.writeObject(list);
out.close();
System.out.println("직렬화 끝");
} catch (IOException e) {}
}
}
u1, u2라는 UserInfo인스턴스를 만들고 out.writeObject()를 통해서 u1,u2를 저장했다.
또 ArrayList도 만들어서 해당 List에 u1, u2를 넣고 out.writeObject()를 통해 List를 직렬화하여 저장했다.
이렇게 되면 기본경로에 UserInfo.ser라는 파일이 만들어 졌을 것이고 해당 파일을 열어보면 직렬화 후 저장된 객체들을 볼 수 있다.
이제 이 ser파일을 역직렬화 하여 u1, u2, ArrayList list에 담아보자.
여기서 중요한 점은, ser파일에 직렬화하여 저장한 순서 그대로 역직렬화 해야 한다.
직렬화 순서가 u1, u2, list였기 때문에 역직렬화 할때 readObject()메서드가 순서대로 가져온다. u1, u2는 똑같은 UserInfo객체라 섞여도 일단 저장은 되겠지만 list와 u1,u2가 섞이면 오류가 날 것이다.
역직렬화 ↓
public class Ex15_21 {
public static void main(String[] args) {
try {
String fileName = "UserInfo.ser";
FileInputStream fis = new FileInputStream(fileName);
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream in = new ObjectInputStream(bis);
UserInfo u1 = (UserInfo) in.readObject();
UserInfo u2 = (UserInfo) in.readObject();
ArrayList list = (ArrayList) in.readObject();
System.out.println(u1);
System.out.println(u2);
System.out.println(list);
in.close();
} catch(Exception e) {
e.printStackTrace();
}
}
}
순서를 제대로 하지 않았을 때 ↓
java.lang.ClassCastException: class 직렬화.UserInfo cannot be cast to class java.util.ArrayList (직렬화.UserInfo is in unnamed module of loader 'app'; java.util.ArrayList is in module java.base of loader 'bootstrap')
at 직렬화.Ex15_21.main(Ex15_21.java:19)
이렇게 직렬화할 객체가 많을 때는 각 객체를 개별적으로 직렬화하기 보다는 ArrayList와 같은 컬렉션에 저장해서 직렬화하는 것이 좋다. 역직렬화 시 ArrayList하나만 역직렬화 하면 되므로 객체의 순서를 고려하지 않아도 되기 때문이다.