▸JAVA/기본 문법

직렬화와 역직렬화 (Serializable)

코데방 2019. 12. 20.
728x90

[ 직렬화 ]

  • 자바의 객체를 외부 데이터로 저장하는 것
  • 객체화된 클래스(인스턴스)의 속성과 데이터를 파일화하여 외부에 저장할 수 있음

[ 역직렬화 ]

  • 직렬화로 저장된 파일을 다시 자바의 객체로 만드는 것
  • 일종의 오버라이딩과 비슷한 개념

간단히 말하면 객체(보통 인스턴스를 의미)의 속성과 데이터를 모두 파일로 저장했다가 필요할 때 다시 객체로 되돌리는 기능이라고 볼 수 있습니다. 프로그램이 꺼지더라도 데이터는 보관되어 있으니 나중에 다시 불러와서 사용할 수 있게 됩니다. 또는 외부로 보내서 데이터를 공유할 수도 있습니다.

 

부모 클래스에서 물려받은 속성을 자식 클래스의 데이터로 덮어쓰는 오버라이딩과 비슷합니다. 아래에서 더 자세히 다루겠지만 공통된 속성을 가진 부모클래스로만 오버라이딩이 가능하듯이 역직렬화 시에도 꼭 일치해야 하는 속성이 있어야만 합니다.

 

메소드야 어차피 직렬화된 파일에서 새로 만들어서 넣을 수도 없으니 결국 필드의 데이터를 외부에 저장한다는 의미가 됩니다. 따라서 직렬화란 결국 필드 데이터만 어떻게든 외부로 빼내서 보관했다가 필요할 때 다시 가져와서 같은 객체에다가 넣어주는 것으로 볼 수 있습니다. 직렬화를 해주는 자바의 기본 라이브러리를 사용하지 않더라도 여러 형태(csv, json, 일반 파일 등)로 직렬화/역직렬화 수행이 가능합니다.

 

자바에서 제공하는 직렬화 기능은 오직 자바 프로그램끼리만 공유 가능한 데이터가 됩니다. 다른 언어로 된 프로그램으로 전달해서 사용하기는 매우 어렵습니다. 또한 나중에 코드 수정을 하거나 자바의 버전이 달라 클래스 속성이 바뀌면 사용할 수 없게 됩니다. 이 점은 아래에서 좀 더 자세히 다루겠습니다.

 


 

[ 직렬화를 사용할 클래스 생성 ]

 

직렬화와 역직렬화를 사용하기 위한 클래스는 무조건 "java.io.Serializable" 인터페이스를 구현해야합니다. 아래와 같이 클래스를 하나 만들어 줍니다. getter/setter 등은 그냥 생략하고 간단히 하겠습니다.

package study.first;

import java.io.Serializable;

public class Test implements Serializable {
	
	int a;
	String b;
	
	public Test(int a, String b) { 
		
		this.a = a;
		this.b = b;
	}
		
	void test() {
		
		System.out.println("테스트 메소드..!");
	}
}

 


 

[ 객체 직렬화하기 ]

  • java.io.FileOutputStream 으로 .txt 파일을 열어줌
  • java.io.ObjectOutputStream으로 해당 파일에 직렬화 수행 (데이터 출력)

외부의 파일을 하나 생성해서 Output Stream을 열고 해당 클래스를 통해 직렬화를 수행해 객체의 정보를 모두 파일로 출력해줍니다. 직렬화와 역직렬화의 사용법 자체는 아주 간단합니다. 생성된 파일을 열어보면 알 수 없는 요상한 내용이 써져있습니다. 

 

  • 생성자 : new ObjectOutputStream (FileOutputStream 인스턴스)
  • 직렬화 수행 : writeObject(직렬화할 인스턴스)

package study.first;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class Main {
	public static void main(String[] args) {
		
		// 직렬화가 선언된 클래스 인스턴스 생성
		Test t1 = new Test(10, "Hello~!!");

		try( ObjectOutputStream out = new ObjectOutputStream
				(new FileOutputStream("Serial.txt"))) {
			
			// 파일에 t1 객체 출력 
			out.writeObject(t1);
			
		} catch(Exception e) {
			
			System.out.println("직렬화 실패~!");
		}
	}
}

 


 

[ 객체 역직렬화하기 ]

 

이제 만들어진 파일을 부활시키겠습니다. 그냥 딱 위의 코드를 반대로 짜면 됩니다. 혹시 데이터 입출력 클래스의 사용법이나 개념이 헷갈리시는 분은 아래 세 개 글을 먼저 보시면 됩니다.

 

2019/12/16 - [JAVA/기본 문법] - 외부 데이터 입출력_io / nio / nio2 [1/3]

2019/12/16 - [JAVA/기본 문법] - 외부 데이터 입출력_java.io [2/3]

2019/12/16 - [JAVA/기본 문법] - 외부 데이터 입출력_java.nio [3/3]

 

아래 결과와 같이 필드 데이터와 메소드 모두 정상적으로 부활했습니다.

 

package study.first;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class Serial {

	public static void main(String[] args) {

		// 역질렬화로 부활시킬 껍데기 인스턴스 생성
		Test p1 = null;

		try( ObjectInputStream in = new ObjectInputStream
				(new FileInputStream("Serial.txt"))) {
			
			// 파일에서 t1으로 역직렬화 수행
			p1 = (Test)in.readObject();
			
		} catch(Exception e) {
			
			System.out.println("직렬화 실패~!");
		}
		
		// 역직렬화로 생성된 인스턴스 필드와 메소드 확인
		System.out.println(p1.a);
		System.out.println(p1.b);
		p1.test();
	}
}

 


 

위와 같이 직렬화와 역직렬화를 구현하는 것은 매우 쉽습니다. 하지만 개념을 정확히 이해하고 있어야 사용하는데 무리가 없습니다. 위에서는 역직렬화는 일종의 오버라이딩과 비슷한 개념이라고 언급했습니다. 즉, 일치하는 속성이 있어야 오버라이딩이 가능한 것처럼 역직렬화 시에도 직렬화를 했던 시점과 클래스 속성이 달라져 있으면 예외가 발생합니다. 

 

그렇다면 자바가 어떻게 역직렬화된 파일과 클래스의 속성이 일치하는지 판단하는지 알아야 합니다. 기본적으로 속성을 클래스 구조정보의 해시코드로 비교한다고 합니다. "구조 정보"만을 가지고 해시코드를 만들어 비교하기 때문에 메소드의 내용이 변경되는 것은 상관없지만 메소드 이름이라던가 필드의 이름이 한글자만 바껴도 역직렬화 시 예외를 뱉어냅니다. 

 

부모클래스를 물려받은 자식클래스가 새로운 필드와 메소드를 추가하더라도 오버라이딩하면 물려받은 속성의 내용만 덮어쓰듯이, 직렬화/역직렬화의 과정에서도 파일의 내용과 클래스의 내용이 공통점이 있다는 점을 명시해줄 수 있습니다. 아래의 필드 값을 이용하는 방법입니다.

 

[ serialVersionUID 필드 ]

  • Serializable 인터페이스의 필드 값
  • 클래스에 수동으로 ID(번호표)를 부여하므로써 내용에 관계 없이 동일 클래스로 인식함
  • 필수 값은 아니지만 클래스에 내용이 추가될 경우에도 역직렬화를 사용할 수 있게 해줌

이 필드 값은 Serializable 인터페이스에 구현된 것으로, 직렬화를 선언한 클래스를 컴파일할 때 컴파일러가 클래스 구조 정보를 해시값으로 변환한 값을 자동으로 넣어줍니다. 구조가 바뀌지 않는 이상 계속 컴파일해도 값이 바뀔일이 없지만 구조가 조금이라도 바뀌면 바로 값이 바껴서 컴파일 됩니다. 이 값이 바뀌게 되면 역직렬화를 할 때 다른 클래스 타입으로 인식해서 예외가 발생하는 것입니다. 따라서 이 값을 클래스에서 수동으로 넣어주면 구조가 바뀌더라도 값이 바뀌지 않아 역직렬화 시 발생할 수 있는 예외를 피할 수 있습니다.

 

아래와 같이 값을 직렬화를 수행할 클래스에 넣어줍니다. 딱 저렇게만 쓸 수 있도록 되어 있고 UID 정보를 넣지 않으면 이클립스에서 계속 경고를 띄웁니다.

package study.first;

import java.io.Serializable;

public class Test implements Serializable {
	
	private static final long serialVersionUID = 1004;
	
	int a;
	String b;
	
	public Test(int a, String b) { 
		
		this.a = a;
		this.b = b;
	}
		
	void test() {
		
		System.out.println("테스트 메소드.....??!!!");
		System.out.println("냥냥냥");
	}	
}

 


 

위 과정을 통해 이제 기존 클래스에 필드나 메소드를 추가하거나 삭제하더라도 역직렬화가 무사히 수행되는 것을 확인할 수 있습니다. 필드를 삭제하면 기존에 저장된 값은 그냥 무시되고 필드를 추가하면 기본값으로 셋팅됩니다. 메모리 편에서 다뤘듯이 스택 프레임에 생성되는 지역변수는 직접 초기화를 해줘야 하지만 힙 영역에 생성되는 객체의 변수(필드)는 0 또는 null 값으로 자동 초기화됩니다. 

 

하지만 필히 주의해야할 점은 직렬화를 수행할 시점에 있던 동일한 필드명의 타입을 변경하면 역직렬화가 수행되지 않는다는 것입니다. 원래 int타입이었는데 이보다 더 큰 long으로 바꿔두면 별로 상관없을 것 같지만 자바에서 허용해주지 않습니다. Java SE 버전이 올라가면서 바뀔 수도 있는 문제지만 아직까지는 되지 않으니 주의해야 합니다.

 


[ 직렬화 / 역직렬화 사용 시 주의사항 요약 ]

  • 직렬화 인터페이스 구현 시 serialVersionUID는 꼭 직접 넣어주고 관리해주는 것이 좋음
  • 직렬화 시 존재했던 동일 필드명/메소드명의 타입은 절대 바꾸면 안됨 (예외 발생)
  • 필드 및 메소드의 추가/삭제는 허용함 (추가된 값은 초기화된 값으로 생성)

따라서 웬만하면 내용 변경이 없을만한 클래스에 사용하는 것이 좋고, 사용하더라도 장기 보관용으로는 적합하지가 않습니다. 오래 보관해야한다면 그냥 다른 파일 형태로 보관하는 것이 좋습니다. 또한 자바 라이브러리로 직렬화시키면 데이터 뿐만 아니라 다른 구조 데이터 등의 정보도 포함되기 때문에 용량도 더 커진다는 단점도 있습니다. 

728x90

댓글

💲 추천 글