오늘의 나보다 성장한 내일의 나를 위해…
Serialization
Serialization
- 자바 시스템 내부에서 사용되는 Object 또는 Data를 외부의 자바 시스템에서도 사용할 수 있도록 byte 형태로 데이터를 변환하는 기술이다.
- JVM의 메모리에 상주(힙 또는 스택)되어 있는 객체 데이터를 바이트 형태로 변환하는 기술
파일에 텍스트를 기록하고, 이진 데이터를 기록하는 방법은 많이들 알고 있다.
그런데 만약 이런 종류의 데이터들이 아니라 객체를 파일로 저장하거나 읽어올 수 있을까?
있다!!!
직렬화가 그것을 가능하게 해준다.
이제부터 직렬화에 관한 간단한 예제를 살펴보자 아래 그림은 Account라는 클래스의 멤버 변수를 나타낸다. 이것을 객체화하여 파일이나 네트워크로 write할 때는 직렬화를 거쳐서 전달된다.
반대로 읽어올때는 역직렬화(Deserialization)를 거쳐서 가져오게 된다.
직렬화에 메서드는 포함하지 않는다.
메서드는 각 클래스가 같은 동작을 수행하기 때문에 직렬화해서 저장할 필요는 없다.
단지 멤버들마다 다른 값을 가지고 있는 필드들이 직렬화가 된다.
직렬화는 어떻게 수행하는가?
- 직렬화가 가능한 클래스 구현하기
- 직렬화가 된 클래스의 객체를 쓰고 읽는 Stream 준비하기
직렬화가 가능한 클래스 구현하기
직렬화를 하려면 우선 Serializable 인터페이스를 implements 해야 한다. 그래서 우리가 Account 클래스를 정의하려면 아래와 같이 정의가 되어야 한다.
Account.java
class Account implements Serializable{
private String email;
private String name;
private String address;
private String phone;
private Date reg_date;
public Account(String email,String name,String address,String phone) {
this.email=email;
this.name=name;
this.address=address;
this.phone=phone;
reg_date=new Date();
}
public String getEmail() {
return email;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
public String getPhone() {
return phone;
}
public String getRegDate() {
SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd");
return format.format(reg_date);
}
@Override
public String toString() {
return "Information:"+getEmail()+"/"+getName()+"/"+getAddress()+"/"+getPhone()+"/"+getRegDate();
}
}
직렬화 제외하기 - transient
우리가 직렬화로 파일에다가 기록할 때 민감한 데이터이기 때문에 직렬화에 제외하는 방법은 transient라는 키워드를 사용하면 된다.
그러면 직렬화에 빠지게 되어 파일에 저장되지 않는다.
Example
class Account implements Serializable{
//... 생략 ...
private Date reg_date;
private transient String password;
public Account(String email,String name,String address,String phone) {
//...생략...
reg_date=new Date();
password="12341234";
}
//...생략...
}
객체 쓰기 (ObjectOutputStream)
객체를 쓰려면 stream을 열어야 한다. 우리는 당장 파일에 그 객체를 쓸 것이기 때문에 FileOutputStream을 사용할 것이고 이 파일 스트림에 객체를 쓸 것이기 때문에 ObjectOutputStream을 사용할 것이다.
사용법은 간단하다.
아래의 차례대로 진행하면 된다.
우선 FileOutputStream으로 파일의 Stream을 열고 이것을 ObjectOutputStream으로 전달해주면 된다.
FileOutputStream fos=new FileOutputStream("user.acc");
ObjectOutputStream oos=new ObjectOutputStream(fos);
여기서 user.acc는 우리가 객체를 저장할 파일 이름이다.
파일을 열때 FileNotFoundException이라는 IOException이 발생할 수 있다.
이것이 주목적은 아니기 때문에 메인 함수에서 throw로 처리한다.
public static void main(String[] ar) throws IOException
그리고 파일에 써줄 객체를 정의해주고 ObjectOutputStream의 객체 쓰기 메서드인 writeObject에 전달해주면 된다.
그리하여 객체를 쓰는 전체코드는 아래와 같다.
Main
public static void main(String[] ar) throws IOException, ClassNotFoundException{
Account wuser=new Account("reakwon@aaa.ccc","reakwon","seoul","010-1234-1010");
FileOutputStream fos=new FileOutputStream("user.acc");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(wuser);
oos.close();
}
위 코드를 실행하면 파일은 working directory에 있다.
그리고 메모장으로 그 내용을 까보면 아래와 같다.
물론 여기에는 직렬화하지 않길 원하는 멤버 필드(transient로 선언한 멤버 필드)가 기록되어 있지 않다.
객체 읽기 (ObjectInputStream)
이제는 이 파일에 쓰여진 객체를 읽어보자.
파일 읽기 스트림(FileInputStream)을 열고 ObjectInputStream을 통해서 읽어오면 된다.
이것 역시 간단하게 ObjectInputStream에 FileInputStream을 전달하고 readObject를 이용해서 객체를 얻어오면 된다.
이때 Object 객체로 반환하기 때문에 적절한 형변환을 해줘야 한다.
Example
Account ruser=null;
FileInputStream fis=new FileInputStream("user.acc");
ObjectInputStream ois=new ObjectInputStream(fis);
ruser=(Account)ois.readObject();
ois.close();
혹시나 객체의 클래스를 찾을 수가 없는 예외가 발생할 수 있다. 그때 발생할 수 있는 예외가 ClassNotFoundException이다.
이것도 메인 메서드에서 던져준다.
public static void main(String[] ar) throws IOException, ClassNotFoundException{
객체를 쓰고 읽는 전체 코드는 아래와 같다.
Main
public class Main {
public static void main(String[] ar) throws IOException, ClassNotFoundException{
Account wuser=new Account("reakwon@aaa.ccc","reakwon","seoul","010-1234-1010");
Account ruser=null;
FileOutputStream fos=new FileOutputStream("user.acc");
ObjectOutputStream oos=new ObjectOutputStream(fos);
oos.writeObject(wuser);
FileInputStream fis=new FileInputStream("user.acc");
ObjectInputStream ois=new ObjectInputStream(fis);
ruser=(Account)ois.readObject();
System.out.println(ruser);
oos.close();
ois.close();
}
}
그래서 읽어보면 제대로 읽어오는 것을 확인할 수가 있다.
Information:reakwon@aaa.ccc/reakwon/seoul/010-1234-1010/2021-04-07
만약 transient로 직렬화에 포함되지 않은 데이터를 읽을 때는 null로 읽힌다.
주의
객체의 직렬화나 역직렬화에서 클래스는 완전한 동일한 클래스를 통해서 쓰고 읽혀야 한다.
그렇지 않은 경우 아래와 같은 에러가 발생한다.
여기서 쓸때는 기존의 Account 클래스, 그리고 읽을 때는 멤버 필드를 추가해 변경된 Account 클래스로 읽어보았다.
Exception in thread "main" java.io.InvalidClassException: aa.Account; local class incompatible: stream classdesc serialVersionUID = 2399026023152107267, local class serialVersionUID = 9178730303496146785
at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:722)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2022)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1872)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2179)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1689)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:495)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:453)
at aa.Main.main(Main.java:15)
이렇게 InvalidClassException 이라는 예외가 발생함으로 우리는 직렬화 클래스의 버전을 관리해줘야 한다.
자바는 serialVersionUID를 통해서 버전이 같은 클래스인지 아닌지 판단할 수 있다.
그래서 이 버전이 같은지 같지 않은지를 통해서 조치를 취해야 한다.
class Account implements Serializable{
static final long serialVersionUID=1919191919191919L;
//...생략...
}