직렬화(Serialization)

우리는 파일에 텍스트를 기록하고, 이진 데이터를 기록하는 방법은 많이들 알고 계시겠습니다. 그런데 만약 이런 종류의 데이터들이 아니라 객체를 파일로 저장하거나 읽어올 수 있을까요? 네, 물론 있습니다. 여러분이 직렬화만 배운다면 할 수 있습니다. 

직렬화에 대한 아주 직관적인 그림은 아래에 나타내어져있는데요. Account라는 클래스는 email, 이름, 주소, 폰 번호, 등록 일자를 멤버로 갖는 클래스입니다. 이것을 객체화하여 파일이나 네트워크로 write할때는 직렬화를 거쳐서 전달됩니다. 반대로 읽어올때는 역직렬화(Deserialization)를 거쳐서 가져오게 되지요. 

 

직렬화에 메소드는 포함하지 않습니다. 메소드는 각 클래스가 같은 동작을 수행하기 때문에 직렬화해서 저장할 필요는 없습니다. 단지 멤버들마다 다른 값을 가지고 있는 필드들이 직렬화가 됩니다. 

이제 본격적으로 직렬화는 어떻게 수행해야할까요? 우리는 두가지만 알고 있으면 됩니다. 

1. 직렬화가 가능한 클래스를 구현하기

2. 직렬화가 된 클래스의 객체를 쓰고 읽는 Stream 준비하기

 

1. 직렬화가 가능한 클래스 구현하기

이건 간단합니다. 직렬화를 하려면 우선 Serializable을 인터페이스를 implements를 해야합니다. 그래서 우리가 Account클래스를 정의하려면 아래와 같이 정의가 되어야합니다.

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();
		
	}
}

 

Serializable을 implements하고 있는 것을 볼 수 있네요. 

직렬화 제외하기 - transient

하지만 우리가 직렬화로 파일에다가 기록할때 민감한 데이터이기 때문에 직렬화에 제외하는 방법은 transient라는 키워드를 사용하면 됩니다. 그러면 직렬화에 빠지게 되어 파일에 저장되지 않습니다. 예를 들면 아래와 같이 사용할 수 있겠습니다.

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";
	}
	//...생략...
}

 

2. 객체 쓰기 읽기

2-1. 객체 쓰기(ObjectOutputStream)

객체를 쓰려면 Stream을 열어야합니다. 우리는 당장 파일에 그 객체를 쓸 것이기 때문에 FileOutputStream을 사용할 것이고 이 파일 스트림에 객체를 쓸 것이기 때문에 ObjectOutputStream을 사용할 겁니다. 사용법은 아주 간단한데요. 아래의 차례대로 진행하면 됩니다. 우선 FileOutputStream으로 파일의 Stream을 열고 이것을 ObjectOutputStream으로 전달해주면 됩니다.

FileOutputStream fos=new FileOutputStream("user.acc");
ObjectOutputStream oos=new ObjectOutputStream(fos);

 

여기서 user.acc는 우리가 객체를 저장할 파일 이름입니다. 파일을 열때 FileNotFoundException이라는 IOException이 발생할 수 있습니다. 이것을 직접 처리하기가 귀찮으니 우리는 메인 함수에서 throws로 처리하도록 하겠습니다.

public static void main(String[] ar) throws IOException

 

그리고 파일에 써줄 객체를 정의해주고 ObjectOutputStream의 객체 쓰기 메소드인 writeObject에 전달해주면 됩니다. 그리하여 객체를 쓰는 전체코드는 아래와 같습니다. 

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로 선언한 멤버 필드) 역시 이곳에 기록되어 있지 않습니다.

 

2-2. 객체 읽기(ObjectInputStream)

이제는 이 파일에 쓰여진 객체를 읽어볼까요? 객체를 읽는데는 파일 읽기 스트림(FileInputStream)을 열고 ObjectInputStream을 통해서 읽어오면 됩니다. 이것 역시 간단하게 ObjectInputStream에 FileInputStream을 전달하고 readObject를 이용해서 객체를 얻어오면 됩니다. 이때 Object객체로 반환하기 때문에 적절히 형변환을 해야합니다. 그래서 객체를 읽는 과정은 아래와 같습니다.

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{

 

객체를 쓰고 읽는 전체 코드는 이렇습니다. 

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를 통해서 버전이 같은 클래스인지 아닌지 판단할 수 있습니다. 그래서 이 버전이 같은지 같지 않은지를 통해서 조치를 취해야합니다. 그래서 우리도 위의 serialVersionUID를 사용하여 클래스 버전을 관리할 수 있습니다.

class Account implements Serializable{
	static final long serialVersionUID=1919191919191919L;
    //...생략...
}

 

이처럼 객체를 저장하고 읽을때는 객체가 직렬화가 되어야하고 Object를 쓰는 ObjectOutputStream, Object를 읽는 ObjectInputStream을 사용하여 읽을 수 있습니다. 객체를 파일로 읽고 쓰는 경우는 어떤 경우에 유용할까요? 엑셀 파일을 저장하거나 한글 문서파일을 저장, 그리고 그림판의 파일을 저장하고 불러오는 등 객체를 쓰고 읽어야할 경우는 매우 많으니 알아두시는 게 매우 편합니다.

 

여기서 우리는 File에만 Object를 쓰고 읽었지만 네트워크에서도 가능합니다. 이상으로 객체 직렬화에 대한 포스팅을 마치도록 하겠습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,