직렬화는 Object의 필드들(정보)을 바이트 코드로 변환하는 것을 의미합니다.
역직렬화는 바이트 코드들을 본래의 필드로 복구하는 것을 의미합니다.
클래스를 생성할 때 Serializable를 Implements 해야 합니다.
만약 출력하고 싶지 않은 필드가 있다면 해당 필드에 transient 키워드를 붙이면 됩니다.
다음은 직렬화에 대한 코드입니다.
ClassA
@Data
public class ClassA implements Serializable {
private int field1; // 일반 필드 생성
ClassB field2 = new ClassB(); // ClassB 객체 생성
static int field3; // 정적 필드 생성
transient int field4; // 한정자(transient) 필드 생성, 직렬화 제외
} // end class
클래스B와 클래스C는 간단한 필드만 선언되어 있습니다.
ClassB
@ToString
@NoArgsConstructor
public class ClassB implements Serializable {
int field1;
} // end class
ClassC
@ToString
@NoArgsConstructor
public class ClassC implements Serializable {
int field1;
} // end class
메인 스트림에서 지정한 경로에 필드의 정보를 출력합니다.
SeriaizableWriter
public class SerializableWriter {
public static void main(String[] args) throws Exception{
// @Cleanup 어노테이션으로 자동 자원 해제
@Cleanup FileOutputStream fos = new FileOutputStream("C:/Temp/Object.dat");
@Cleanup ObjectOutputStream oos = new ObjectOutputStream(fos);
ClassA classA = new ClassA(); // 기본 생성자로 객체 생성
classA.setField1(1); // 고유속성 초기화
classA.field2.field1 = 2; // 부품필드 초기화
ClassA.field3 = 3; // 정적필드 초기화
classA.field4 = 4; // transient 한정자가 붙은 필드
// ClassA타입의 객체를 파일에 출력(ObjectOutputStream)
oos.writeObject(classA);
oos.flush();
} // main
} // end class
위 실행 클래스가 정상적으로 동작되었다면, Temp 폴더에 Object.dat 파일이 생성됩니다.
이제 Object.dat 파일을 읽는 실행 클래스를 만들겠습니다.
@Log4j2
public class SerializableReader {
public static void main(String[] args) throws Exception{
@Cleanup FileInputStream fis = new FileInputStream("C:/Temp/Object.dat");
@Cleanup ObjectInputStream ois = new ObjectInputStream(fis);
ClassA objA = (ClassA) ois.readObject();
log.info("1.field1: {} ", objA.getField1());
log.info("2.field2.field1: {} ", objA.field2.field1);
log.info("3.field2: {} ", ClassA.field3);
log.info("4.field3: {} ", objA.field4);
} // main
} // end class
입력받은 각 필드의 키워드는 다음과 같습니다.
- field1 = classA_private int field1 = 1
- field2 = classA_classB_int field1 = 2
- field3 = classA_static int field3 = 3
- field4 = classA_transient int field4 = 4
다음은 출력 결과입니다.
보시는 것처럼,
Serializable를 Implements하는 클래스에 있는 필드들의 출력 파일을 입력받았는데
field3 과 field4는 결과값이 없는 것을 알 수 있습니다.
그렇다면, 파일을 출력하는 과정에서 field3와 field4가 출력되지 않았다고 예상됩니다.
가장 먼저 field 4는 transient 키워드가 붙어있기 때문에 애초에 출력이 되지 않습니다.
field3를 출력하지 못하는 이유는 메모리 영역에서 알 수 있습니다.
객체의 정보(필드)는 힙 메모리 영역에 저장되는 반면 정적 필드는 static 영역에 저장됩니다.
즉, 객체 입출력 스트림은 객체의 정보(필드)를 출력하고 읽는 스트림인데,
일반 필드가 저장되어 있는 힙 영역과 정적 필드가 저장되는 static 영역은 완전히 별개의 영역이기 때문에 입력받지 못하는 것입니다.
마지막으로 간단한 예제를 통해 조금 더 심도있게 살펴보겠습니다.
Parent
@ToString
@NoArgsConstructor
public class Parent {
// Getter, Setter를 사용하지 않고 도트로 접근하기 위해 public
public String field1;
} // end class
Child
@Log4j2
@ToString
@NoArgsConstructor
public class Child
extends Parent
implements Serializable {
private static final long serialVersionUID = 1L;
public String field2;
// 자바언어표준스펙에서 정의된 메서드를 사용함, 직렬화
private void writeObject(ObjectOutputStream out)
throws IOException {
log.trace("writeObject({}) invoked.", out);
// 이 메서드 안에서, 확장받은 부모 객체의 필드가 직렬화 가능하도록
// 상속받은 부모객체의 필드를 직접적으로 출력
out.writeUTF(field1);
out.defaultWriteObject();
} // writeObject
// 역직렬화
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
log.trace("readObject({}) invoked.", in);
// 이 메서드 안에서, 확장받은 부모 객체의 필드가 역직렬화 가능하도록
// 상속받은 부모객체의 필드를 직접적으로 입력
field1 = in.readUTF();
in.defaultReadObject();
} // readObject
} // end class
위 코드에서 serialVersionUID 필드는 만약 클래스가 변경(예를 들어, 필드가 추가되거나 삭제)되면 직렬화된 객체의 형식과 해당 클래스의 형식을 맞추기 위해 사용됩니다.
serialVersionUID는 직렬화/역직렬화 과정에서 클래스 버전을 확인하고 만약 serialVersionUID의 값이 동일하다면 필드의 정보 상관없이 역직렬화할 수 있습니다.
Child 클래스에서 선언된 메서드들은 자바 언어 표준 스펙에서 정의된 메서드를 사용하였습니다.해당 메서드들은 직렬화/역직렬화를 할 때, JVM이 자동으로 콜백(callback)을 합니다. 즉, 이 메서드를 사용함으로써 Serializable하지 못한(직렬화/역직렬화가 불가능한) Parent 클래스의 객체 정보도 직렬화/역직렬화를 가능하게 만듭니다.
다음은 실행 클래스 입니다.
@Log4j2
public class NonSerializableParentExample {
public static void main(String[] args) throws Exception{
@Cleanup FileOutputStream fos = new FileOutputStream("C:/Temp/Object.dat");
@Cleanup ObjectOutputStream oos = new ObjectOutputStream(fos);
// 필드 초기화
Child child = new Child();
child.field1 = "홍길동";
child.field2 = "홍삼원";
// 부모가 serializable하지 않아도 직렬화 가능함
oos.writeObject(child);
oos.flush();
log.info("write done.\n");
// ===================================================
@Cleanup FileInputStream fis = new FileInputStream("C:/Temp/Object.dat");
@Cleanup ObjectInputStream ois = new ObjectInputStream(fis);
Child childObj = (Child) ois.readObject();
// Serializable하지 않은 Parent Class의 필드 정보는 역직렬화 불가능함
log.info("1.field1: {}", childObj.field1);
log.info("2.field2: {}", childObj.field2);
} // main
} // end class
입출력을 하나의 클래스에서 구현하였습니다.
다음은 코드 결과입니다.
보시는 것처럼, JVM은 Parent 클래스가 직렬화/역직렬화가 불가능하다는 것을 알고 Child 클래스의 메서드를 자동으로 콜백하였습니다.
만약 Child 클래스에 해당 메서드가 없다면 어떻게 될까요?
콘솔 결과에서도 알 수 있듯이, Parent 클래스에 속한 field1 필드가 null값으로 입력된 것을 확인할 수 있습니다.
즉, JVM이 직렬화/역직렬화가 불가능한 클래스를 발견하고 콜백할 메서드를 찾지 못하면 직렬화/역직렬화를 못한 상태로 입/출력이 이루어지게 됩니다.