Java의 직렬화와 역직렬화에 대해서 알아봅시다.
정의
Java에서 말하는 직렬화(Serialization)란 과연 뭘까?
말그대로 객체를 직렬화하여 전송 가능한 형태로 만드는 것을 의미한다. 객체들의 데이터를 연속적인 데이터로 변형하여 Stream을 통해 데이터를 읽도록 해준다.
이것은 주로 객체들을 통째로 파일로 저장하거나 전송하고 싶을 때 주로 사용된다.
그럼 역직렬화(Deserialization)는?
직렬화된 파일 등을 역으로 직렬화하여 다시 객체의 형태로 만드는 것을 의미한다. 저장된 파일을 읽거나 전송된 스트림 데이터를 읽어 원래 객체의 형태로 복원한다.
직렬화 가능한 클래스
자. 그럼 직렬화를 해보자!
직렬화를 위한 전제조건이 있다. 바로 직렬화가 가능한 클래스를 먼저 만드는 것이다.
이는 Serializable을 통해 정할 수 있다.
public class A {
...
}
이런 형태라면
public class A implements Serializable {
...
}
위와 같이 직렬화가 가능한 클래스 A에 Serializable 인터페이스를 implements하면 된다.
하지만 단순히 인터페이스를 구현한다고 해서 다 가능한 대상이 되는 것일까? 객체에는 다양한 변수와 메서드, 그리고 상속구조가 존재하는데??
여러가지 상황에 따라 직렬 가능한 클래스와 데이터가 결정된다. 그리고 내가 결정할 수도 있다.
1. Serializable 인터페이스를 implements
위의 예시와 같이 Serializable 인터페이스를 구현하는 클래스로 만든다.
2. Serializable 없는 경우
보통의 경우는 직렬화가 불가능하다. 하지만 클래스들간의 관계를 고려해보면 가능한 경우도 있다. 바로 Serializable을 implement한 클래스를 상속받은 경우이다.
이러한 경우 클래스 B는 직렬화 가능한 클래스가 된다.
3. transient를 이용하여 직렬화 대상에서 제외하기
보통 클래스의 멤버변수 전부 직렬화 대상에 해당된다. 하지만 보안 상의 문제나 기타 이유로 멤버변수의 일부를 제외하고 싶다면 transient를 통해 지정할 수 있다.
유저들의 정보를 담는 User 클래스가 있다고 해보자.
public class User implements Serializable {
private String id;
private String password;
private String email;
//....
}
아이디, 비밀번호, 이메일 정보 중 비밀번호를 제외하고 싶다면 앞에 transient를 붙이면 끝.
public class User implements Serializable {
private String id;
private transient String password;
private String email;
//....
}
4. 다른 객체를 멤버변수로 가지고 있는 경우
int, long, String 등 기본 자료형 뿐만아니라 다른 객체를 멤버변수로 사용하는 경우가 굉장히 많다. 이러한 경우 직렬화를 할 수 있을까?
User 클래스에 타 객체 변수를 추가하였다.
public class User implements Serializable {
private String id;
private transient String password;
private String email;
ItemInfo itemInfo;
Calendar regDate;
//....
}
아이템정보를 가지고 있는 ItemInfo 클래스와 가입일자를 나타내는 Calendar 클래스의 멤버변수를 가지고 있다. 이 때, ItemInfo, Calendar 클래스들 중 Serializable 인터페이스를 구현한 클래스가 하나라도 없다면 직렬화할 수 없다. Calendar는 java.util 에서 제공하는 기본 클래스로 자체적으로 Serializable을 구현한 클래스이므로 User클래스는 직렬화 가능하다. 이처럼 참조하고있는 객체의 직렬화 가능 상태를 확인해야한다.
실습
직렬화 준비가 되었다면 이제 구현해보자.
ObjectInputStream / ObjectOutputStream 을 주로 활용한다.
유저 정보를 담을 User 클래스를 이용하여 직렬화, 역직렬화 하는 방법을 소개한다.
* User 클래스
public class User implements Serializable{
private String name;
private transient String password;
private String email;
public int age;
public User(String name, String password, int age) {
this.name = name;
this.password = password;
this.age = age;
}
public String toString() {
return "(" + name + ", " + password + ", " + email + ", " + age + ")";
}
}
* 직렬화 & 역직렬화
public class MainClass {
private static final String USERINFO_SER = "user.ser";
public static void main(String[] args) {
// TODO Auto-generated method stub
conductSerializing();
conductDeserializing();
}
public static void conductSerializing() {
try {
FileOutputStream fos = new FileOutputStream(USERINFO_SER);
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream out = new ObjectOutputStream(bos);
User u1 = new User("이방원", "1234", "lby@abc.com", 30);
User u2 = new User("무휼", "8877", "mh1398@abc.com", 25);
ArrayList list = new ArrayList<>();
list.add(u1);
list.add(u2);
out.writeObject(u1);
out.writeObject(u2);
out.writeObject(list);
out.close();
System.out.println("직렬화 완료");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void conductDeserializing(){
try {
FileInputStream fis = new FileInputStream(USERINFO_SER);
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream in = new ObjectInputStream(bis);
User u1 = (User) in.readObject();
User u2 = (User) in.readObject();
ArrayList list = (ArrayList) in.readObject();
System.out.println(u1.toString());
System.out.println(u2.toString());
System.out.println("count :: " + list.size());
System.out.println(list.toString());
in.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
* 실행결과
conductSerializing() 메서드에서 직렬화를 수행하여 user.ser 파일을 생성하고 이를 읽어서 바로 역직렬화 한다. 여기서 눈여겨 볼 것은 실행 결과이다.
역직렬화 결과를 보면 직렬화한 순서 그대로 출력됨을 알 수 있다. 즉, 직렬화와 역직렬화 할 때 순서가 매우 중요하다는 것이다!!
매번 순서를 고려해야하고 하나라도 맞지않으면 역직렬화에 실패한다. 같은 객체를 여러번 보내기보다 ArrayList와 같은 자료구조로 한번에 넣는 것이 더욱 효율적이다.
SerialVersionUID, 클래스의 버전관리
직렬화하면 내부에서 자동으로 SerialVersionUID라는 고유의 번호를 생성하여 관리한다. 이 UID는 직렬화와 역직렬화 할 때 중요한 역할을 한다. 이 값이 맞는지 확인 후 처리를 하기 때문이다. 만약 이 SerialVersionUID가 맞지 않는다면 다음과 같은 오류를 출력한다.
java.io.InvalidClassException: com.java.test.User; local class incompatible: stream classdesc serialVersionUID = -2617558951961522125, local class serialVersionUID = 3278902038563169060
at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)
at java.io.ObjectInputStream.readClassDesc(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)
at com.java.test.MainClass.conductDeserializing(MainClass.java:50)
at com.java.test.MainClass.main(MainClass.java:18)
User 클래스에서 하나의 멤버변수를 추가한 후 위에서 직렬화하여 생성한 user.ser 파일을 역직렬화 한 결과이다. 소스 상에서 UID를 선언하지 않아도 이렇게 내부에서 자동으로 생성하여 관리한다. 생성 당시의 UID와 현재 변경한 이후의 UID가 맞지 않아서 문제가 발생했다. 서비스 배포 이후 객체의 변경은 끊임없이 변경될텐데 이렇게 매번 컴파일하고 배포해야한다면 예외처리를 직접 해주는 등 너무나 불편할 것이다.
이러한 문제 때문일까, Java에서는 이 SerialVersionUID를 직접 선언하고 관리하는 방식을 적극 권장하고 있다. 위에서 구현한 User클래스에 UID를 직접 선언해보자.
* SerialVersionUID를 추가한 User 클래스
public class User implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
private String name;
private transient String password;
private String email;
public int age;
public User(String name, String password, String email, int age) {
this.name = name;
this.password = password;
this.email = email;
this.age = age;
}
public String toString() {
return "(" + name + ", " + password + ", " + email + ", " + age + ")";
}
}
1L 은 입맛에 맞게 변경해주면 된다. 이렇게 선언한다면 추후에 User 클래스에 변경이 생길지라도 UID가 여전히 1이기 때문에 역직렬화를 성공적으로 할 수 있을 것이다.
정리
- 직렬화 가능한 클래스들의 상태를 확인하라.
- 직렬화, 역직렬화 할 경우 객체들의 순서가 중요하다. ArrayList 등을 활용하면 손쉽게 할 수 있다.
- SerialVersionUID 고유의 번호를 관리하라. 자동으로 생성해주지만 직접 관리하는 것을 권장.
'Programming > Java' 카테고리의 다른 글
Java8 람다표현식 (Lambda Expression) (0) | 2018.10.30 |
---|
댓글