웹사이트 검색

Java의 직렬화 - Java 직렬화


Java의 직렬화는 JDK 1.1에서 도입되었으며 Core Java의 중요한 기능 중 하나입니다.

Java의 직렬화

  1. 자바에서 직렬화 가능
  2. 직렬화 및 serialVersionUID를 사용한 클래스 리팩토링
  3. Java 외부화 가능 인터페이스
  4. 자바 직렬화 방법
  5. 상속을 통한 직렬화
  6. 직렬화 프록시 패턴

Java에서 직렬화 가능

클래스 개체를 직렬화할 수 있게 하려면 java.io.Serializable 인터페이스를 구현하기만 하면 됩니다. Java에서 Serializable은 마커 인터페이스이며 구현할 필드나 메서드가 없습니다. 클래스를 직렬화할 수 있게 만드는 옵트인 프로세스와 같습니다. Java의 직렬화는 ObjectInputStreamObjectOutputStream에 의해 구현되므로 파일에 저장하거나 네트워크를 통해 전송하기 위한 래퍼만 있으면 됩니다. Java 프로그램 예제에서 간단한 직렬화를 살펴보겠습니다.

package com.journaldev.serialization;

import java.io.Serializable;

public class Employee implements Serializable {

//	private static final long serialVersionUID = -6470090944414208496L;
	
	private String name;
	private int id;
	transient private int salary;
//	private String password;
	
	@Override
	public String toString(){
		return "Employee{name="+name+",id="+id+",salary="+salary+"}";
	}
	
	//getter and setter methods
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public int getSalary() {
		return salary;
	}

	public void setSalary(int salary) {
		this.salary = salary;
	}

//	public String getPassword() {
//		return password;
//	}
//
//	public void setPassword(String password) {
//		this.password = password;
//	}
	
}

몇 가지 속성과 getter-setter 메서드가 있는 간단한 Java bean이라는 점에 유의하십시오. 개체 속성을 스트림으로 직렬화하지 않으려면 급여 변수에 사용한 것처럼 임시 키워드를 사용할 수 있습니다. 이제 개체를 파일에 쓴 다음 동일한 파일에서 역직렬화하려고 한다고 가정합니다. 따라서 직렬화 목적으로 ObjectInputStreamObjectOutputStream을 사용할 유틸리티 메서드가 필요합니다.

package com.journaldev.serialization;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * A simple class with generic serialize and deserialize method implementations
 * 
 * @author pankaj
 * 
 */
public class SerializationUtil {

	// deserialize to Object from given file
	public static Object deserialize(String fileName) throws IOException,
			ClassNotFoundException {
		FileInputStream fis = new FileInputStream(fileName);
		ObjectInputStream ois = new ObjectInputStream(fis);
		Object obj = ois.readObject();
		ois.close();
		return obj;
	}

	// serialize the given object and save it to file
	public static void serialize(Object obj, String fileName)
			throws IOException {
		FileOutputStream fos = new FileOutputStream(fileName);
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(obj);

		fos.close();
	}

}

메서드 인수는 모든 Java 개체의 기본 클래스인 개체와 함께 작동합니다. 이런 식으로 작성되어 본질적으로 일반적입니다. 이제 Java Serialization이 실제로 작동하는지 확인하는 테스트 프로그램을 작성해 보겠습니다.

package com.journaldev.serialization;

import java.io.IOException;

public class SerializationTest {
	
	public static void main(String[] args) {
		String fileName="employee.ser";
		Employee emp = new Employee();
		emp.setId(100);
		emp.setName("Pankaj");
		emp.setSalary(5000);
		
		//serialize to file
		try {
			SerializationUtil.serialize(emp, fileName);
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}
		
		Employee empNew = null;
		try {
			empNew = (Employee) SerializationUtil.deserialize(fileName);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
		
		System.out.println("emp Object::"+emp);
		System.out.println("empNew Object::"+empNew);
	}
}

Java에서 직렬화를 위해 위의 테스트 프로그램을 실행하면 다음과 같은 결과가 나타납니다.

emp Object::Employee{name=Pankaj,id=100,salary=5000}
empNew Object::Employee{name=Pankaj,id=100,salary=0}

급여는 임시 변수이므로 값이 파일에 저장되지 않아 새 개체에서 검색되지 않습니다. 마찬가지로 정적 변수 값도 개체가 아닌 클래스에 속하므로 직렬화되지 않습니다.

직렬화 및 serialVersionUID를 사용한 클래스 리팩토링

Java의 직렬화는 무시할 수 있는 경우 Java 클래스의 일부 변경을 허용합니다. 역직렬화 프로세스에 영향을 미치지 않는 클래스의 일부 변경 사항은 다음과 같습니다.

  • 클래스에 새 변수 추가
  • 직렬화를 위해 일시적인 변수를 일시적이지 않은 변수로 변경하면 새 필드가 있는 것과 같습니다.
  • 변수를 정적에서 비정적으로 변경하면 직렬화를 위해 새 필드를 갖는 것과 같습니다.

그러나 이러한 모든 변경 사항이 작동하려면 Java 클래스에 해당 클래스에 대해 정의된 serialVersionUID가 있어야 합니다. 이전 테스트 클래스에서 이미 직렬화된 파일의 역직렬화만을 위한 테스트 클래스를 작성해 보겠습니다.

package com.journaldev.serialization;

import java.io.IOException;

public class DeserializationTest {

	public static void main(String[] args) {

		String fileName="employee.ser";
		Employee empNew = null;
		
		try {
			empNew = (Employee) SerializationUtil.deserialize(fileName);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
		
		System.out.println("empNew Object::"+empNew);
		
	}
}

이제 "password\ 변수와 Employee 클래스의 getter-setter 메소드의 주석을 제거하고 실행하십시오. 아래 예외가 발생합니다.

java.io.InvalidClassException: com.journaldev.serialization.Employee; local class incompatible: stream classdesc serialVersionUID = -6470090944414208496, local class serialVersionUID = -6234198221249432383
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
	at com.journaldev.serialization.SerializationUtil.deserialize(SerializationUtil.java:22)
	at com.journaldev.serialization.DeserializationTest.main(DeserializationTest.java:13)
empNew Object::null

이전 클래스와 새 클래스의 serialVersionUID가 다르기 때문에 그 이유는 분명합니다. 실제로 클래스가 serialVersionUID를 정의하지 않으면 자동으로 계산되어 클래스에 할당됩니다. Java는 클래스 변수, 메소드, 클래스 이름, 패키지 등을 사용하여 이 고유한 긴 숫자를 생성합니다. IDE로 작업하는 경우 "직렬화 가능한 클래스 Employee가 long 유형의 정적 최종 serialVersionUID 필드를 선언하지 않습니다\라는 경고가 자동으로 표시됩니다. Java 유틸리티 "serialver\를 사용하여 serialVersionUID 클래스를 생성할 수 있습니다. 직원 클래스는 아래 명령으로 실행할 수 있습니다.

SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee

이 프로그램 자체에서 직렬 버전이 생성될 필요는 없으며 원하는 대로 이 값을 할당할 수 있습니다. 역직렬화 프로세스에 새 클래스가 동일한 클래스의 새 버전이며 가능한 역직렬화되어야 함을 알리기 위해 거기에 있어야 합니다. 예를 들어 Employee 클래스에서 serialVersionUID 필드만 주석 처리를 제거하고 SerializationTest 프로그램을 실행합니다. 이제 Employee 클래스에서 암호 필드의 주석을 제거하고 DeserializationTest 프로그램을 실행하면 Employee 클래스의 변경 사항이 직렬화 프로세스와 호환되기 때문에 개체 스트림이 성공적으로 역직렬화되는 것을 볼 수 있습니다.

Java 외부화 가능 인터페이스

Java 직렬화 프로세스를 발견하면 자동으로 수행됩니다. 때때로 우리는 무결성을 유지하기 위해 개체 데이터를 모호하게 만들고 싶습니다. java.io.Externalizable 인터페이스를 구현하고 직렬화에 사용할 writeExternal()readExternal() 메서드 구현을 제공하여 이를 수행할 수 있습니다. 프로세스.

package com.journaldev.externalization;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Person implements Externalizable{

	private int id;
	private String name;
	private String gender;
	
	@Override
	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeInt(id);
		out.writeObject(name+"xyz");
		out.writeObject("abc"+gender);
	}

	@Override
	public void readExternal(ObjectInput in) throws IOException,
			ClassNotFoundException {
		id=in.readInt();
		//read in the same order as written
		name=(String) in.readObject();
		if(!name.endsWith("xyz")) throw new IOException("corrupted data");
		name=name.substring(0, name.length()-3);
		gender=(String) in.readObject();
		if(!gender.startsWith("abc")) throw new IOException("corrupted data");
		gender=gender.substring(3);
	}

	@Override
	public String toString(){
		return "Person{id="+id+",name="+name+",gender="+gender+"}";
	}
	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getGender() {
		return gender;
	}

	public void setGender(String gender) {
		this.gender = gender;
	}

}

스트림으로 변환하기 전에 필드 값을 변경한 다음 읽는 동안 변경 사항을 되돌렸습니다. 이런 식으로 우리는 어떤 종류의 데이터 무결성을 유지할 수 있습니다. 스트림 데이터를 읽은 후 무결성 검사가 실패하면 예외가 발생할 수 있습니다. 실제로 작동하는지 테스트 프로그램을 작성해 봅시다.

package com.journaldev.externalization;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ExternalizationTest {

	public static void main(String[] args) {
		
		String fileName = "person.ser";
		Person person = new Person();
		person.setId(1);
		person.setName("Pankaj");
		person.setGender("Male");
		
		try {
			FileOutputStream fos = new FileOutputStream(fileName);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
		    oos.writeObject(person);
		    oos.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		FileInputStream fis;
		try {
			fis = new FileInputStream(fileName);
			ObjectInputStream ois = new ObjectInputStream(fis);
		    Person p = (Person)ois.readObject();
		    ois.close();
		    System.out.println("Person Object Read="+p);
		} catch (IOException | ClassNotFoundException e) {
			e.printStackTrace();
		}
	    
	}
}

위의 프로그램을 실행하면 다음과 같은 결과를 얻습니다.

Person Object Read=Person{id=1,name=Pankaj,gender=Male}

따라서 어느 것이 Java에서 직렬화에 사용되는 것이 더 낫습니다. 실제로 Serializable 인터페이스를 사용하는 것이 더 좋으며 기사가 끝날 때쯤이면 그 이유를 알게 될 것입니다.

Java 직렬화 방법

우리는 Java의 직렬화가 자동이며 필요한 것은 Serializable 인터페이스를 구현하는 것임을 확인했습니다. 구현은 ObjectInputStream 및 ObjectOutputStream 클래스에 있습니다. 그러나 데이터를 저장하는 방식을 변경하려는 경우에는 어떻게 해야 합니까? 예를 들어 객체에 민감한 정보가 있고 저장/검색하기 전에 이를 암호화/해독하려고 합니다. 이것이 직렬화 동작을 변경하기 위해 클래스에서 제공할 수 있는 네 가지 메서드가 있는 이유입니다. 이러한 메서드가 클래스에 있으면 직렬화 목적으로 사용됩니다.

  1. readObject(ObjectInputStream ois): 이 메서드가 클래스에 있는 경우 ObjectInputStream readObject() 메서드는 스트림에서 개체를 읽는 데 이 메서드를 사용합니다.
  2. writeObject(ObjectOutputStream oos): 이 메서드가 클래스에 있는 경우 ObjectOutputStream writeObject() 메서드는 객체를 스트림에 쓰는 데 이 메서드를 사용합니다. 일반적인 사용법 중 하나는 데이터 무결성을 유지하기 위해 객체 변수를 숨기는 것입니다.
  3. 객체 writeReplace(): 이 메서드가 있으면 직렬화 프로세스 후에 이 메서드가 호출되고 반환된 개체가 스트림으로 직렬화됩니다.
  4. 객체 readResolve(): 이 메서드가 있으면 역직렬화 프로세스 후에 호출자 프로그램에 최종 개체를 반환하기 위해 이 메서드가 호출됩니다. 이 방법의 사용법 중 하나는 직렬화된 클래스로 싱글톤 패턴을 구현하는 것입니다. 직렬화 및 싱글톤에서 자세히 알아보세요.

일반적으로 위의 메서드를 구현하는 동안 하위 클래스가 재정의할 수 없도록 비공개로 유지됩니다. 직렬화 목적으로만 사용되며 비공개로 유지하면 보안 문제를 피할 수 있습니다.

상속을 통한 직렬화

때때로 우리는 Serializable 인터페이스를 구현하지 않는 클래스를 확장해야 합니다. 자동 직렬화 동작에 의존하고 상위 클래스에 상태가 있으면 스트림으로 변환되지 않으므로 나중에 검색되지 않습니다. 이것은 readObject() 및 writeObject() 메서드가 실제로 도움이 되는 한 곳입니다. 구현을 제공함으로써 슈퍼 클래스 상태를 스트림에 저장한 다음 나중에 검색할 수 있습니다. 이것을 실제로 봅시다.

package com.journaldev.serialization.inheritance;

public class SuperClass {

	private int id;
	private String value;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getValue() {
		return value;
	}
	public void setValue(String value) {
		this.value = value;
	}	
}

SuperClass는 단순한 자바 빈이지만 Serializable 인터페이스를 구현하지 않습니다.

package com.journaldev.serialization.inheritance;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SubClass extends SuperClass implements Serializable, ObjectInputValidation{

	private static final long serialVersionUID = -1322322139926390329L;

	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	@Override
	public String toString(){
		return "SubClass{id="+getId()+",value="+getValue()+",name="+getName()+"}";
	}
	
	//adding helper method for serialization to save/initialize super class state
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
		ois.defaultReadObject();
		
		//notice the order of read and write should be same
		setId(ois.readInt());
		setValue((String) ois.readObject());	
	}
	
	private void writeObject(ObjectOutputStream oos) throws IOException{
		oos.defaultWriteObject();
		
		oos.writeInt(getId());
		oos.writeObject(getValue());
	}

	@Override
	public void validateObject() throws InvalidObjectException {
		//validate the object here
		if(name == null || "".equals(name)) throw new InvalidObjectException("name can't be null or empty");
		if(getId() <=0) throw new InvalidObjectException("ID can't be negative or zero");
	}	
}

스트림에 추가 데이터를 쓰고 읽는 순서는 동일해야 합니다. 데이터를 읽고 쓰는 데 몇 가지 논리를 넣어 보안을 유지할 수 있습니다. 또한 클래스가 ObjectInputValidation 인터페이스를 구현하고 있음을 확인하십시오. validateObject() 메서드를 구현하면 데이터 무결성이 손상되지 않도록 비즈니스 유효성 검사를 수행할 수 있습니다. 테스트 클래스를 작성하고 직렬화된 데이터에서 슈퍼 클래스 상태를 검색할 수 있는지 확인합시다.

package com.journaldev.serialization.inheritance;

import java.io.IOException;

import com.journaldev.serialization.SerializationUtil;

public class InheritanceSerializationTest {

	public static void main(String[] args) {
		String fileName = "subclass.ser";
		
		SubClass subClass = new SubClass();
		subClass.setId(10);
		subClass.setValue("Data");
		subClass.setName("Pankaj");
		
		try {
			SerializationUtil.serialize(subClass, fileName);
		} catch (IOException e) {
			e.printStackTrace();
			return;
		}
		
		try {
			SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName);
			System.out.println("SubClass read = "+subNew);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
	}
}

위의 클래스를 실행하면 다음과 같은 결과를 얻습니다.

SubClass read = SubClass{id=10,value=Data,name=Pankaj}

따라서 이러한 방식으로 Serializable 인터페이스를 구현하지 않더라도 상위 클래스 상태를 직렬화할 수 있습니다. 이 전략은 수퍼 클래스가 변경할 수 없는 타사 클래스일 때 유용합니다.

직렬화 프록시 패턴

Java의 직렬화에는 다음과 같은 몇 가지 심각한 함정이 있습니다.

  • Java 직렬화 프로세스를 중단하지 않고는 클래스 구조를 많이 변경할 수 없습니다. 따라서 나중에 일부 변수가 필요하지 않더라도 이전 버전과의 호환성을 위해서만 유지해야 합니다.
  • 직렬화는 엄청난 보안 위험을 초래하며 공격자는 스트림 순서를 변경하고 시스템에 해를 끼칠 수 있습니다. 예를 들어 사용자 역할이 직렬화되고 공격자가 스트림 값을 변경하여 관리자로 만들고 악성 코드를 실행합니다.

Java 직렬화 프록시 패턴은 직렬화로 보안을 강화하는 방법입니다. 이 패턴에서 내부 전용 정적 클래스는 직렬화 목적을 위한 프록시 클래스로 사용됩니다. 이 클래스는 메인 클래스의 상태를 유지하는 방식으로 설계되었습니다. 이 패턴은 readResolve()writeReplace() 메서드를 올바르게 구현하여 구현됩니다. 먼저 직렬화 프록시 패턴을 구현하는 클래스를 작성하고 더 나은 이해를 위해 이를 분석하겠습니다.

package com.journaldev.serialization.proxy;

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Data implements Serializable{

	private static final long serialVersionUID = 2087368867376448459L;

	private String data;
	
	public Data(String d){
		this.data=d;
	}

	public String getData() {
		return data;
	}

	public void setData(String data) {
		this.data = data;
	}
	
	@Override
	public String toString(){
		return "Data{data="+data+"}";
	}
	
	//serialization proxy class
	private static class DataProxy implements Serializable{
	
		private static final long serialVersionUID = 8333905273185436744L;
		
		private String dataProxy;
		private static final String PREFIX = "ABC";
		private static final String SUFFIX = "DEFG";
		
		public DataProxy(Data d){
			//obscuring data for security
			this.dataProxy = PREFIX + d.data + SUFFIX;
		}
		
		private Object readResolve() throws InvalidObjectException {
			if(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){
			return new Data(dataProxy.substring(3, dataProxy.length() -4));
			}else throw new InvalidObjectException("data corrupted");
		}
		
	}
	
	//replacing serialized object to DataProxy object
	private Object writeReplace(){
		return new DataProxy(this);
	}
	
	private void readObject(ObjectInputStream ois) throws InvalidObjectException{
		throw new InvalidObjectException("Proxy is not used, something fishy");
	}
}

  • DataDataProxy 클래스 모두 Serializable 인터페이스를 구현해야 합니다.
  • DataProxy는 데이터 개체의 상태를 유지할 수 있어야 합니다.
  • DataProxy는 내부 전용 정적 클래스이므로 다른 클래스가 액세스할 수 없습니다.
  • DataProxy에는 Data를 인수로 사용하는 단일 생성자가 있어야 합니다.
  • Data 클래스는 DataProxy 인스턴스를 반환하는 writeReplace() 메서드를 제공해야 합니다. 따라서 Data 개체가 직렬화될 때 반환되는 스트림은 DataProxy 클래스입니다. 그러나 DataProxy 클래스는 외부에서 보이지 않으므로 직접 사용할 수 없습니다.
  • DataProxy 클래스는 Data 개체를 반환하는 readResolve() 메서드를 구현해야 합니다. 따라서 Data 클래스가 역직렬화되면 내부적으로 DataProxy가 역직렬화되고 readResolve() 메서드가 호출되면 Data 객체를 얻습니다.
  • 마지막으로 Data 클래스에 readObject() 메서드를 구현하고 InvalidObjectException을 발생시켜 Data 개체 스트림을 조작하고 구문 분석하려는 해커 공격을 방지합니다.

구현이 제대로 되었는지 확인하기 위해 작은 테스트를 작성해 봅시다.

package com.journaldev.serialization.proxy;

import java.io.IOException;

import com.journaldev.serialization.SerializationUtil;

public class SerializationProxyTest {

	public static void main(String[] args) {
		String fileName = "data.ser";
		
		Data data = new Data("Pankaj");
		
		try {
			SerializationUtil.serialize(data, fileName);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		try {
			Data newData = (Data) SerializationUtil.deserialize(fileName);
			System.out.println(newData);
		} catch (ClassNotFoundException | IOException e) {
			e.printStackTrace();
		}
	}

}

위의 클래스를 실행하면 콘솔에서 아래 출력을 얻습니다.

Data{data=Pankaj}

data.ser 파일을 열면 DataProxy 객체가 파일에 스트림으로 저장되어 있는 것을 확인할 수 있습니다.

Java 직렬화 프로젝트 다운로드

이것이 Java의 직렬화에 대한 전부입니다. 간단해 보이지만 신중하게 사용해야 하며 항상 기본 구현에 의존하지 않는 것이 좋습니다. 위 링크에서 프로젝트를 다운로드하고 함께 플레이하여 자세히 알아보세요.