-
[Java] 직렬화(Serialization)란 무엇일까?Language/Java 2021. 1. 30. 01:39728x90반응형
Serializable에 대해서 알아보기
직렬화라는 용어에 대해서 들어만 보고 공부해본 적은 없는데 이번 기회에 정리를 하게 되었습니다,, 이번 글에서는
직렬화
에 대해서 알아보겠습니다.public interface Serializable { }
Serializable
의 인터페이스를 보면 메소드가 하나도 없는 것을 볼 수 있습니다.아무런 구현해야 할 메소드도 없는 이 인터페이스가 도대체 왜 있는 것일까요?
개발을 하다 보면 아래와 같은 경우가 존재합니다.
- 생성한 객체를 파일로 저장할 일이 있을 수도 있습니다.
- 저장한 객체를 읽을 일이 생길 수도 있습니다.
- 다른 서버에서 생성한 객체를 받을 일도 생길 수 있습니다.
이럴 때 꼭 필요한 것이
Serializable
입니다. 우리가 만든 클래스가 파일에 읽거나 쓸 수 있도록 하거나, 다른 서버로 보내거나 받을 수 있도록 하려면반드시 이 인터페이스를 구현
해야 합니다.Serializable 인터페이스를 구현하면 JVM에서 해당 객체는 저장하거나 다른 서버로 전송할 수 있도록 해준다.
그래서 직렬화가 무엇인가?
- 자바 직렬화란 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 기술과 바이트로 변환된 데이터를 다시 객체로 변환하는 기술(역직렬화)을 아울러서 이야기합니다.
- 시스템적으로 이야기하자면 JVM(Java Virtual Machine 이하 JVM)의 메모리에 상주(힙 또는 스택)되어 있는 객체 데이터를 바이트 형태로 변환하는 기술과 직렬화된 바이트 형태의 데이터를 객체로 변환해서 JVM으로 상주시키는 형태를 같이 이야기합니다.
여기 에서 참고한 정의입니다.
Serializable
인터페이스를 구현한 클래스들을 보면serialVersionUID
라는 값을 지정해주는 것을 본 적이 있을 것입니다.public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L; }
예를들어,
HashMap
클래스를 보면 위와 같은 변수를 볼 수 있습니다. 이렇게Serializable
인터페이스를 구현한 후에는 위와 같이serialVersionUID
라는 값을 지정해 주는 것을 권장합니다.
(만약 별도로 지정하지 않으면, 자바 소스가 컴파일될 때 자동으로 생깁니다.)static final long serialVersionUID = 1L;
위와 같이 반드시
static final long
으로 선언해야 하며, 변수명도serialVersionUID
로 선언해 주어야 자바에서 인식을 할 수 있습니다.그러면 이 값은 어디에 사용되고 어떤 값을 넣어야 할까요?
- 값은 아무런 값이나 지정해주면 됩니다.
값의 의미는 해당 객체의 버전을 명시하는 데 사용합니다.
예를들어 보겠습니다.A라는 서버에서 B라는 서버로 SerialDTO라는 클래스의 객체를 전송한다고 가정하겠습니다. 전송하는 A 서버에 SerialDTO라는 클래스가 있어야 하고, 전송받는 B 서버에는 SerialDTO라는 클래스가 있어야만 합니다.
그래야만 그 클래스의 객체임을 알고 데이터를 받을 수 있습니다.그런데 만약 A 서버가 갖고 있는 SerialDTO에는 변수가 3개 있고, B 서버의 SerialDTO에는 변수가 4개 있는 상황이 발생하면 어떻게 될까요?
이러면 자바에서는 제대로 처리를 못하게 됩니다. 따라서 각 서버가 쉽게 해당 객체가 같은지 다른지를 확인할 수 있도록 하기 위해서는 serialVersionUID로 관리를 해주어야만 합니다.즉 클래스 이름이 같더라도 이 ID가 다르면 다른 클래스라고 인식합니다. 게다가, 같은 UID라고 할지라도, 변수의 개수나 타입 등이 다르면 이 경우도 다른 클래스로 인식합니다.
글만 봐서는 쉽지가 않은데 자세한 내용은 조금만 있다가 다시 더 알아보겠습니다.
객체를 저장해보기
import java.io.Serializable; public class SerialDTO implements Serializable { private String booName; private int bookOrder; private boolean bestSeller; private long soldPerDay; public SerialDTO(String booName, int bookOrder, boolean bestSeller, long soldPerDay) { this.booName = booName; this.bookOrder = bookOrder; this.bestSeller = bestSeller; this.soldPerDay = soldPerDay; } @Override public String toString() { return "SerialDTO{" + "booName='" + booName + '\'' + ", bookOrder=" + bookOrder + ", bestSeller=" + bestSeller + ", soldPerDay=" + soldPerDay + '}'; } }
DTO 클래스를 저장해보는 예제를 해보겠습니다.
import java.io.FileOutputStream; import java.io.ObjectOutputStream; public class ManageObject { public static void main(String[] args) { ManageObject manage = new ManageObject(); String fullPath = "/Users/choejeong-gyun/Documents/test.md"; SerialDTO dto = new SerialDTO("God of Java", 1, true, 100); manage.saveObject(fullPath, dto); } public void saveObject(String fullPath, SerialDTO dto) { FileOutputStream fos = null; ObjectOutputStream oos = null; try { fos = new FileOutputStream(fullPath); oos = new ObjectOutputStream(fos); oos.writeObject(dto); System.out.println("Write Success"); } catch (Exception e) { e.printStackTrace(); } finally { if (oos != null) { try { oos.close(); } catch (Exception e) { e.printStackTrace(); } } if (fos != null) { try { fos.close(); } catch (Exception e) { e.printStackTrace(); } } } } }
- 자바에서는
ObjectOutputStream
클래스를 사용하면 객체를 저장할 수 있습니다.ObjectInputStream
을 사용하면 저장해놓은 객체를 읽을 수 있습니다. - 위의 코드에서도
FileOutputStream
객체를 만든 후에ObjectOutputStream
의 매개변수로 넘겼습니다. 이렇게 하면 해당 객체는 파일에 저장됩니다. - writeObject()를 통해서 매개변수로 넘어온 객체를 저장합니다.
그리고 파일을 확인해보면 파일에 객체가 저장이 된 것을 볼 수 있습니다.
객체를 읽어보기
import java.io.*; public class ManageObject { public static void main(String[] args) { ManageObject manage = new ManageObject(); String fullPath = "/Users/choejeong-gyun/Documents/test.md"; manage.loadObject(fullPath); } public void loadObject(String fullPath) { FileInputStream fis = null; ObjectInputStream ois = null; try { fis = new FileInputStream(fullPath); ois = new ObjectInputStream(fis); Object obj = ois.readObject(); SerialDTO dto = (SerialDTO)obj; System.out.println(dto); } catch (Exception e) { e.printStackTrace(); } finally { if (ois != null) { try { ois.close(); } catch (Exception e) { e.printStackTrace(); } } } if (fis != null) { try { fis.close(); } catch (Exception e) { e.printStackTrace(); } } } }
SerialDTO{booName='God of Java', bookOrder=1, bestSeller=true, soldPerDay=100}
그러면 위와 같이 파일에 저장된 객체 정보를 읽을 수 있습니다. 그리고 이번에
SerialDTO
클래스의 필드를 하나 추가한 후에 위의 코드를 다시 실행해보겠습니다.java.io.InvalidClassException: FileIO.SerialDTO; local class incompatible: stream classdesc serialVersionUID = -358710248991570103, local class serialVersionUID = 1424372278057927306 at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:689) at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1982) at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1851) at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2139) at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1668) at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:482) at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:440) at FileIO.ManageObject.loadObject(ManageObject.java:49) at FileIO.ManageObject.main(ManageObject.java:12)
그러면 위와 같은 결과가 나옵니다. 위에서 볼 수 있듯이
serialVersionUID
값이 다르다는 에러 메세지가 출력됩니다. 이렇게 객체 형태가 변경되면 컴파일시 serialVersionUID가 다시 생성되기 때문에 이러한 문제가 발생하게 됩니다.transient라는 예약어는 Serializable과 떨어질 수 없는 관계
transient private int bookOrder;
SerialDTO 클래스에
transient
라는 예약어를 추가한 후에 다시 객체를 파일에 저장하고 읽어오는 코드를 실행해보겠습니다.Write Success SerialDTO{booName='God of Java', bookOrder=0, bestSeller=true, soldPerDay=100}
그러면 객체를 생성할 때 bookOrder에 1을 넣었지만 결과에는 0이 나오는 것을 볼 수 있습니다.
0이 나오는 이유가 무엇일까요?
객체를 저장하거나, 다른 JVM으로 보낼 때, transient 예약어를 사용하여 선언한 변수는 Serializable의 대상에서 제외됩니다.
그러면 뭐하러 이것을 사용하나 싶을 수 있지만, 패스워드와 같이 보안상 중요한 변수나 꼭 저장해야 할 필요가 없는 변수에 대해서는
transient
를 사용할 수 있습니다.직렬화 참고하기
public class SuperUserInfo implements Serializable { String name; String password; }
public class UserInfo extends SuperUserInfo { int age; }
이러한 상속 관계가 있을 때,
SuperUserInfo
클래스를 직렬화 했지만, 하위 클래스인UserInfo
클래스도 직렬화가 가능하게 됩니다.
UserInfo를 직렬화하면 부모 클래스의 name, password도 같이 직렬화가 됩니다.public class SuperUserInfo { String name; String password; }
public class UserInfo extends SuperUserInfo implements Serializable { int age; }
하지만 위와 같이 부모 클래스가 직렬화를 구현하지 않았다면 자식 클래스에서 직렬화할 때 name, password는 직렬화 대상에서 제외됩니다.
public class UserInfo implements Serializable { int age; Object object = new Object(); // Object 객체는 직렬화할 수 없다. }
위의 코드에서 UserInfo 클래스는 Serializable을 구현하고 있어서 직렬화 할 수 있다고 생각할 수 있지만 직렬화를 시도하면
java.io.NotSerializableException
이 발생합니다.이유가 무엇일까요?
바로 Object 객체 때문입니다. 위의 예제에서 보았듯이 부모 클래스에서 Serializable을 구현하고 있다면 자식 클래스도 직렬화가 가능했습니다.
Object는 모든 클래스의 최고 조상이기 때문에 이 클래스가 Serializable을 구현하다면 모든 클래스들이 직렬화가 가능했을 것입니다.그렇기 때문에 Object 클래스는 Serializable을 구현하지 않아 직렬화를 할 수 없습니다.
public class UserInfo implements Serializable { int age; Object object = new String("abc"); }
하지만 위와 같이 다형성을 이용한 코드는 직렬화를 할 수 있습니다. 인스턴스 변수의 타입은 Object의 타입이지만 실제로 저장된 객체는 직렬화가 가능한 String 인스턴스이기 때문에 직렬화가 가능합니다.
인스턴스 변수의 타입이 아닌 실제로 연결된 객체의 종류에 의해서 결정된다는 것을 알아두면 좋을 것 같습니다.
Reference
반응형'Language > Java' 카테고리의 다른 글
[Java] Java Synchronized Lock의 범위는? (0) 2021.02.11 [Java] ConcurrentHashMap 이란 무엇일까? (9) 2021.02.11 [Java] Generic Type erasure란 무엇일까? (2) 2021.01.26 [Java] equals()를 재정의 하려거든 hashCode()도 재정의 해야 하는이유 (0) 2021.01.26 [Java] ArrayList와 Vector 클래스의 차이는? (0) 2021.01.26