Java Language
직렬화
수색…
소개
Java는 객체 직렬화라고하는 메커니즘을 제공합니다. 객체는 객체의 데이터뿐만 아니라 객체의 유형 및 객체에 저장된 데이터 유형에 대한 정보를 포함하는 일련의 바이트로 나타낼 수 있습니다.
직렬화 된 객체가 파일에 기록 된 후에는 파일에서 읽고 객체를 나타내는 유형 정보와 바이트 및 데이터를 사용하여 메모리에 객체를 다시 생성 할 수 있습니다.
Java의 기본 직렬화
직렬화 란?
직렬화는 개체의 상태 (참조 포함)를 바이트 시퀀스로 변환하는 프로세스이며 이러한 바이트를 이후의 특정 시점에 라이브 개체로 다시 작성하는 프로세스입니다. 직렬화는 개체를 유지하려는 경우에 사용됩니다. 또, Java RMI는, 클라이언트로부터 서버에의 메소드 호출의 인수로서, 또는 메소드 호출로부터의 반환 값으로서, 또는 리모트 메소드에 의한 예외로서 JVM간에 객체를 건네주기 위해서 (때문에) 사용합니다. 일반적으로 직렬화는 객체가 JVM의 수명을 초과하여 존재하기를 원할 때 사용됩니다.
java.io.Serializable
은 마커 인터페이스 (본문 없음)입니다. 이것은 단지 Java 클래스를 직렬화 가능으로 "표시"하는 데 사용됩니다.
직렬화 런타임은 각 직렬화 가능 클래스에 serialVersionUID
라는 버전 번호를 연결합니다. 직렬화 된 객체의 발신자와 수신자가 직렬화와 관련하여 해당 객체에 대한 클래스를로드했는지 확인하기 위해 직렬화 해제 중에 사용됩니다. 수신자가 해당 송신자의 클래스와 다른 serialVersionUID
를 가진 객체의 클래스를로드 한 경우 deserialization으로 인해 InvalidClassException
이 발생합니다. 직렬화 가능 클래스는 static, final,
및 long
유형이어야하는 serialVersionUID
라는 필드를 선언하여 명시 적으로 자체 serialVersionUID
선언 할 수 있습니다.
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;
클래스를 직렬화 할 수있게 만드는 방법
객체를 지속시키기 위해서는 각각의 클래스가 java.io.Serializable
인터페이스를 구현해야한다.
import java.io.Serializable;
public class SerialClass implements Serializable {
private static final long serialVersionUID = 1L;
private Date currentTime;
public SerialClass() {
currentTime = Calendar.getInstance().getTime();
}
public Date getCurrentTime() {
return currentTime;
}
}
객체를 파일에 쓰는 법
이제이 객체를 파일 시스템에 작성해야합니다. 이를 위해 java.io.ObjectOutputStream
을 사용한다.
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
public class PersistSerialClass {
public static void main(String [] args) {
String filename = "time.ser";
SerialClass time = new SerialClass(); //We will write this object to file system.
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
out.writeObject(time); //Write byte stream to file system.
out.close();
} catch(IOException ex){
ex.printStackTrace();
}
}
}
직렬화 된 상태에서 개체를 다시 만드는 방법
저장된 객체는 나중에 볼 수 있듯이 java.io.ObjectInputStream
을 사용하여 나중에 파일 시스템에서 읽을 수 있습니다.
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
import java.io.java.lang.ClassNotFoundException;
public class ReadSerialClass {
public static void main(String [] args) {
String filename = "time.ser";
SerialClass time = null;
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
time = (SerialClass)in.readObject();
in.close();
} catch(IOException ex){
ex.printStackTrace();
} catch(ClassNotFoundException cnfe){
cnfe.printStackTrace();
}
// print out restored time
System.out.println("Restored time: " + time.getTime());
}
}
직렬화 된 클래스는 2 진 형식입니다. 직렬화 복원은 클래스 정의가 변경되면 문제가 될 수 있습니다. 자세한 내용 은 Java Serialization Specification의 직렬화 된 객체 버전 지정 장 을 참조하십시오.
객체를 직렬화하면 그 객체가 루트 인 전체 객체 그래프가 직렬화되며 순환 그래프가있는 경우 올바르게 작동합니다. reset()
메소드는 ObjectOutputStream
이 이미 직렬화 된 객체를 잊도록 강제하기 위해 제공됩니다.
Gson과의 직렬화
Gson을 사용한 직렬화는 쉽고 올바른 JSON을 출력합니다.
public class Employe {
private String firstName;
private String lastName;
private int age;
private BigDecimal salary;
private List<String> skills;
//getters and setters
}
(직렬화)
//Skills
List<String> skills = new LinkedList<String>();
skills.add("leadership");
skills.add("Java Experience");
//Employe
Employe obj = new Employe();
obj.setFirstName("Christian");
obj.setLastName("Lusardi");
obj.setAge(25);
obj.setSalary(new BigDecimal("10000"));
obj.setSkills(skills);
//Serialization process
Gson gson = new Gson();
String json = gson.toJson(obj); //{"firstName":"Christian","lastName":"Lusardi","age":25,"salary":10000,"skills":["leadership","Java Experience"]}
무한 재귀가 발생하므로 순환 참조로 객체를 직렬화 할 수 없습니다.
(비 직렬화)
//it's very simple...
//Assuming that json is the previous String object....
Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj
Jackson 2와의 직렬화
다음은 객체를 해당 JSON 문자열로 직렬화하는 방법을 보여주는 구현입니다.
class Test {
private int idx;
private String name;
public int getIdx() {
return idx;
}
public void setIdx(int idx) {
this.idx = idx;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
직렬화 :
Test test = new Test();
test.setIdx(1);
test.setName("abc");
ObjectMapper mapper = new ObjectMapper();
String jsonString;
try {
jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(test);
System.out.println(jsonString);
} catch (JsonProcessingException ex) {
// Handle Exception
}
산출:
{
"idx" : 1,
"name" : "abc"
}
필요하지 않으면 Default Pretty Printer를 생략 할 수 있습니다.
여기에 사용 된 종속성은 다음과 같습니다.
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.3</version>
</dependency>
사용자 정의 직렬화
이 예제에서 우리는 초기화 할 때 인자로 넘겨지는 두 정수의 범위 사이의 난수를 생성하고 콘솔에 출력 할 클래스를 생성하려고합니다.
public class SimpleRangeRandom implements Runnable {
private int min;
private int max;
private Thread thread;
public SimpleRangeRandom(int min, int max){
this.min = min;
this.max = max;
thread = new Thread(this);
thread.start();
}
@Override
private void WriteObject(ObjectOutputStreamout) throws IO Exception;
private void ReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
public void run() {
while(true) {
Random rand = new Random();
System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
이제이 클래스를 Serializable으로 만들려면 몇 가지 문제가 있습니다. 스레드는 직렬화 할 수없는 특정 시스템 수준 클래스 중 하나입니다. 따라서 스레드를 일시적 으로 선언해야합니다. 이렇게하면이 클래스의 객체를 직렬화 할 수 있지만 여전히 문제가 발생합니다. 생성자에서 볼 수 있듯이 랜덤 화기의 최소값과 최대 값을 설정하고 난 후 랜덤 값을 생성하고 인쇄하는 스레드를 시작합니다. 따라서 readObject () 를 호출하여 지속 된 객체를 복원 할 때 생성자는 새 객체를 만들지 않으므로 다시 실행되지 않습니다. 이 경우 클래스 내부에 두 가지 메서드를 제공하여 사용자 지정 serialization 을 개발해야합니다. 그 방법은 다음과 같습니다.
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
따라서 우리의 구현을 readObject () 에 추가함으로써 스레드를 시작하고 시작할 수 있습니다 :
class RangeRandom implements Serializable, Runnable {
private int min;
private int max;
private transient Thread thread;
//transient should be any field that either cannot be serialized e.g Thread or any field you do not want serialized
public RangeRandom(int min, int max){
this.min = min;
this.max = max;
thread = new Thread(this);
thread.start();
}
@Override
public void run() {
while(true) {
Random rand = new Random();
System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
thread = new Thread(this);
thread.start();
}
}
우리 예제의 주요 내용은 다음과 같습니다.
public class Main {
public static void main(String[] args) {
System.out.println("Hello");
RangeRandom rangeRandom = new RangeRandom(1,10);
FileOutputStream fos = null;
ObjectOutputStream out = null;
try
{
fos = new FileOutputStream("test");
out = new ObjectOutputStream(fos);
out.writeObject(rangeRandom);
out.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
RangeRandom rangeRandom2 = null;
FileInputStream fis = null;
ObjectInputStream in = null;
try
{
fis = new FileInputStream("test");
in = new ObjectInputStream(fis);
rangeRandom2 = (RangeRandom)in.readObject();
in.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
catch(ClassNotFoundException ex)
{
ex.printStackTrace();
}
}
}
메인을 실행하면 (자) , 각 RangeRandom 인스턴스마다 2 개의 thread가 실행되고있는 것을 볼 수 있습니다. Thread.start () 메소드가 생성자와 readObject () 모두에 존재하기 때문입니다.
버전 관리 및 serialVersionUID
java.io.Serializable
인터페이스를 구현하여 클래스를 직렬화 가능하게 만들 때 컴파일러는 long
유형의 serialVersionUID
라는 static final
필드를 찾습니다. 클래스가이 필드를 명시 적으로 선언하지 않으면 컴파일러는 해당 필드를 하나 만들고 serialVersionUID
의 구현에 따라 달라지는 값을 할당합니다. 이 계산은 클래스의 다양한 측면에 따라 다르며 Sun에서 제공 한 객체 직렬화 사양을 따릅니다. 그러나이 값은 모든 컴파일러 구현에서 동일하지 않을 수도 있습니다.
이 값은 직렬화와 관련하여 클래스의 호환성을 검사하는 데 사용되며 저장된 객체를 비 직렬화하는 동안 수행됩니다. 직렬화 런타임은 직렬화 해제 된 데이터에서 읽은 serialVersionUID
와 클래스에 선언 된 serialVersionUID
가 정확히 일치하는지 확인합니다. 그렇지 않은 경우 InvalidClassException
시킵니다.
serialize 할 모든 클래스에서이 필드에 대한 값의 기본 계산에 의존하지 않고 명시 적으로 long 유형의 final, finalVersionUID라는 정적 필드를 명시 적으로 선언하고 초기화하는 것이 좋습니다. 버전 관리를 사용하십시오. 'serialVersionUID'계산은 매우 민감하며 컴파일러 구현마다 다를 수 있으므로 직렬화 프로세스의 보낸 사람과받는 사람의 끝에 다른 컴파일러 구현을 사용했기 때문에 같은 클래스에 대해서도 InvalidClassException
을 얻을 수 있습니다.
public class Example implements Serializable {
static final long serialVersionUID = 1L /*or some other value*/;
//...
}
serialVersionUID
가 같으면 Java 직렬화는 다른 버전의 클래스를 처리 할 수 있습니다. 호환성이 있거나 호환되지 않는 변경 사항은 다음과 같습니다.
호환 가능한 변경
- 필드 추가 : 재구성되는 클래스가 스트림에서 발생하지 않는 필드를 가질 때, 객체의 해당 필드는 해당 유형의 기본값으로 초기화됩니다. 클래스 고유의 초기화가 필요한 경우, 클래스는 필드를 기본값 이외의 값으로 초기화 할 수있는 readObject 메서드를 제공 할 수 있습니다.
- 클래스 추가 : 스트림에는 스트림의 각 객체에 대한 유형 계층 구조가 포함됩니다. 스트림의이 계층 구조를 현재 클래스와 비교하면 추가 클래스를 감지 할 수 있습니다. 스트림에 객체를 초기화 할 정보가 없기 때문에 클래스의 필드는 기본값으로 초기화됩니다.
- 클래스 제거 : 스트림의 클래스 계층을 현재 클래스의 클래스 계층과 비교하면 클래스가 삭제되었음을 감지 할 수 있습니다. 이 경우 해당 클래스에 해당하는 필드 및 객체가 스트림에서 읽습니다. 원시적 필드는 파기됩니다 만, 삭제 된 클래스에 의해 참조되는 객체는 나중에 스트림에서 참조 될 수 있기 때문에 작성됩니다. 스트림이 가비지 수집되거나 재설정 될 때 가비지 수집됩니다.
- writeObject / readObject 메소드의 추가 : 스트림을 읽는 버젼에 이러한 메소드가있는 경우, readObject는, 통상과 같이, 디폴트의 직렬화에 의해 스트림에 기입 해지는 필수 데이터를 읽어들이는 것이 요구됩니다. 선택적 데이터를 읽기 전에 먼저 defaultReadObject를 호출해야합니다. writeObject 메소드는 평소처럼 defaultWriteObject를 호출하여 필요한 데이터를 쓰고 선택적 데이터를 쓸 수 있습니다.
- java.io.Serializable 추가 : 이것은 유형을 추가하는 것과 같습니다. 이 클래스의 스트림에는 값이 없기 때문에 필드가 기본값으로 초기화됩니다. 직렬화 할 수없는 클래스의 서브 클래스를 지원하려면 클래스의 상위 유형에 인수가없는 생성자가 있어야하고 클래스 자체가 기본값으로 초기화되어야합니다. 인수가없는 생성자를 사용할 수 없으면 InvalidClassException이 throw됩니다.
- 필드에 대한 액세스 변경 : 액세스 수정 자 public, package, protected 및 private은 값을 필드에 할당하는 직렬화 기능에 영향을주지 않습니다.
- 필드를 static에서 nonstatic 또는 transient에서 nontransient로 변경 : 기본 직렬화를 사용하여 직렬화 가능 필드를 계산할 때이 변경은 클래스에 필드를 추가하는 것과 같습니다. 새 필드는 스트림에 기록되지만 직렬화는 정적 또는 일시적인 필드에 값을 할당하지 않으므로 이전 클래스는 값을 무시합니다.
호환되지 않는 변경 사항
- 필드 삭제 : 클래스에서 필드가 삭제되면 작성된 스트림에는 해당 값이 포함되지 않습니다. 이전 클래스에서 스트림을 읽을 때 스트림에서 값을 사용할 수 없으므로 필드 값이 기본값으로 설정됩니다. 그러나이 기본값은 계약을 이행하기위한 이전 버전의 능력을 저하시킬 수 있습니다.
- 클래스를 위 또는 아래로 이동 : 스트림의 데이터가 잘못된 순서로 나타나기 때문에 허용 할 수 없습니다.
- 비 정적 필드를 정적 또는 비 transient 필드로 변경 : 기본 직렬화에 의존 할 때이 변경은 클래스에서 필드를 삭제하는 것과 같습니다. 이 버전의 클래스는 해당 데이터를 스트림에 쓰지 않으므로 이전 버전의 클래스에서 읽을 수 없습니다. 필드를 삭제할 때와 마찬가지로 이전 버전의 필드가 기본값으로 초기화되므로 예기치 않은 방식으로 클래스가 실패 할 수 있습니다.
- 기본 필드의 선언 된 유형 변경 : 클래스의 각 버전은 선언 된 유형으로 데이터를 씁니다. 스트림의 데이터 유형이 필드의 유형과 일치하지 않으므로 필드를 읽으려고 시도하는 이전 버전의 클래스가 실패합니다.
- writeObject 또는 readObject 메소드를 변경하여 더 이상 기본 필드 데이터를 쓰거나 읽지 못하도록하거나 이전 버전이 아닌 경우 읽거나 읽으려고 시도하도록 변경합니다. 기본 필드 데이터는 일관되게 스트림에 나타나거나 나타나지 않아야합니다.
- 클래스가 사용 가능 클래스의 구현과 호환되지 않는 데이터를 포함하기 때문에 클래스를 Serializable에서 Externalizable 또는 그 반대로 변경하면 호환되지 않는 변경 사항입니다.
- 스트림에는 사용 가능한 클래스의 구현과 호환되지 않는 데이터가 포함되므로 클래스를 비 enum 유형에서 enum 유형으로 또는 그 반대로 변경하십시오.
- Serializable 또는 Externalizable 중 하나를 제거하면 호환되지 않는 변경 사항이 작성됩니다.이 클래스는 이전 버전의 클래스에서 필요했던 필드를 더 이상 제공하지 않기 때문입니다.
- 비헤이비어가 이전 버전의 클래스와 호환되지 않는 객체를 생성하는 경우 클래스에 writeReplace 또는 readResolve 메서드를 추가하면 호환되지 않습니다.
Jackson을 사용한 사용자 정의 JSON 역 직렬화
우리는 rest API를 JSON 형식으로 사용하고이를 POJO로 비 정렬 화합니다. Jackson의 org.codehaus.jackson.map.ObjectMapper는 "즉시 작동합니다"그리고 대부분의 경우 실제로 아무것도하지 않습니다. 그러나 때로는 사용자 지정 필요성을 충족시키기 위해 사용자 지정 deserializer가 필요하며이 자습서는 사용자 지정 deserializer를 만드는 과정을 안내합니다.
다음 엔티티가 있다고 가정 해 보겠습니다.
public class User {
private Long id;
private String name;
private String email;
//getter setter are omitted for clarity
}
과
public class Program {
private Long id;
private String name;
private User createdBy;
private String contents;
//getter setter are omitted for clarity
}
먼저 객체를 직렬화 / 마샬링합니다.
User user = new User();
user.setId(1L);
user.setEmail("[email protected]");
user.setName("Bazlur Rahman");
Program program = new Program();
program.setId(1L);
program.setName("Program @# 1");
program.setCreatedBy(user);
program.setContents("Some contents");
ObjectMapper objectMapper = new ObjectMapper();
final String json = objectMapper.writeValueAsString (program); System.out.println (json);
위의 코드는 다음 JSON-
{
"id": 1,
"name": "Program @# 1",
"createdBy": {
"id": 1,
"name": "Bazlur Rahman",
"email": "[email protected]"
},
"contents": "Some contents"
}
이제 반대를 아주 쉽게 할 수 있습니다. 이 JSON이 있으면 ObjectMapper를 사용하여 다음과 같이 프로그램 객체를 비 정렬화할 수 있습니다.
이제는 이것이 실제 사례가 아니라고 가정 해 봅시다. 우리는 Program
클래스와 일치하지 않는 API와는 다른 JSON을 가질 것입니다.
{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}
JSON 문자열을 보면 owenerId라는 다른 필드가 있다는 것을 알 수 있습니다.
이제 이전처럼이 JSON을 직렬화하려면 예외가 발생합니다.
예외를 피하고이를 직렬화하는 두 가지 방법이 있습니다.
알 수없는 필드를 무시하십시오.
온 onwerId
무시하십시오. Program 클래스에 다음 주석을 추가하십시오.
@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}
사용자 정의 디시리얼라이저 작성
그러나 owerId
필드가 필요한 경우가 있습니다. 이것을 User
클래스의 id로 연결한다고 가정 해 보겠습니다.
이 경우에는 사용자 정의 디시리얼라이저를 작성해야합니다.
당신이 볼 수 있듯이, 먼저는 액세스 할 JsonNode
으로부터 JonsParser
. 그런 다음 get()
메서드를 사용하여 JsonNode
에서 정보를 쉽게 추출 할 수 있습니다. 필드 이름을 반드시 확인해야합니다. 철자 오류로 인해 예외가 발생할 수있는 정확한 이름이어야합니다.
마지막으로 ObjectMapper
ProgramDeserializer를 등록해야합니다.
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Program.class, new ProgramDeserializer());
mapper.registerModule(module);
String newJsonString = "{\"id\":1,\"name\":\"Program @# 1\",\"ownerId\":1,\"contents\":\"Some contents\"}";
final Program program2 = mapper.readValue(newJsonString, Program.class);
또는 주석을 사용하여 디시리얼라이저를 직접 등록 할 수 있습니다.
@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}