JavaRush /Java Blog /Random-KO /Java의 직렬화 및 역직렬화

Java의 직렬화 및 역직렬화

Random-KO 그룹에 게시되었습니다
안녕하세요! 오늘 강의에서는 Java의 직렬화(Serialization)와 역직렬화(Deserialization)에 대해 이야기하겠습니다. 간단한 예부터 시작해 보겠습니다. 당신이 컴퓨터 게임의 제작자라고 가정해 보겠습니다. 90년대에 자랐고 그 당시의 게임 콘솔을 기억한다면 아마도 오늘날 게임 저장 및 로드와 같은 분명한 것이 부족하다는 것을 알고 계실 것입니다. :) 그렇지 않다면... 상상해 보세요! 오늘은 그런 기회가 없는 게임이 실패할까봐 두렵습니다! 그리고 실제로 게임을 "저장"하고 "로드"하는 것은 무엇입니까? 글쎄, 일반적인 의미에서 우리는 그것이 무엇인지 이해합니다. 우리는 지난번에 중단했던 부분부터 게임을 계속하고 싶습니다. 이를 위해 일종의 "체크포인트"를 생성한 다음 게임을 로드하는 데 사용합니다. 그러나 이것은 일상적인 의미가 아니라 "프로그래머"의 의미에서 무엇을 의미합니까? 대답은 간단합니다. 프로그램 상태를 저장합니다. 스페인을 대상으로 전략 게임을 하고 있다고 가정해 보겠습니다. 게임에는 상태가 있습니다. 누가 어떤 영토를 소유하고 있는지, 누가 얼마나 많은 자원을 보유하고 있는지, 누가 누구와 동맹을 맺고 있는지, 반대로 누가 전쟁 중인지 등이 있습니다. 나중에 데이터를 복원하고 게임을 계속하려면 프로그램 상태인 이 정보를 어떻게든 저장해야 합니다. 이것이 바로 직렬화역직렬화 메커니즘이 사용되는 목적입니다 . 직렬화는 객체의 상태를 일련의 바이트로 저장하는 프로세스입니다. 역직렬화는 이러한 바이트에서 개체를 재구성하는 프로세스입니다. 모든 Java 객체는 바이트 시퀀스로 변환됩니다. 그것은 무엇을 위한 것입니까? 우리는 프로그램 자체가 존재하지 않는다고 여러 번 말했습니다. 대부분 그들은 서로 상호 작용하고 데이터를 교환하는 등의 작업을 수행합니다. 그리고 이를 위해서는 바이트 형식이 편리하고 효율적입니다. 예를 들어, 클래스 객체 SavedGame(저장된 게임)를 바이트 시퀀스로 변환하고 해당 바이트를 네트워크를 통해 다른 컴퓨터로 전송한 다음 두 번째 컴퓨터에서 해당 바이트를 다시 Java 객체로 변환할 수 있습니다. 듣기가 어렵죠? 분명히 이 과정을 정리하는 것은 쉽지 않을 것입니다 :/ 다행히도 그렇지 않습니다! :) Java에서는 직렬화 가능 인터페이스가 직렬화 프로세스를 담당합니다 . 이 인터페이스는 매우 간단합니다. 이를 사용하기 위해 단일 메서드를 구현할 필요가 없습니다! 게임 저장 클래스는 다음과 같습니다.
import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private static final long serialVersionUID = 1L;

   private String[] territoriesInfo;
   private String[] resourcesInfo;
   private String[] diplomacyInfo;

   public SavedGame(String[] territoriesInfo, String[] resourcesInfo, String[] diplomacyInfo){
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public String[] getTerritoriesInfo() {
       return territoriesInfo;
   }

   public void setTerritoriesInfo(String[] territoriesInfo) {
       this.territoriesInfo = territoriesInfo;
   }

   public String[] getResourcesInfo() {
       return resourcesInfo;
   }

   public void setResourcesInfo(String[] resourcesInfo) {
       this.resourcesInfo = resourcesInfo;
   }

   public String[] getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(String[] diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoriesInfo=" + Arrays.toString(territoriesInfo) +
               ", resourcesInfo=" + Arrays.toString(resourcesInfo) +
               ", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
               '}';
   }
}
3개의 데이터 세트는 영토, 경제 및 외교에 대한 정보를 담당하며 직렬화 가능 인터페이스는 Java 시스템에 " 모든 것이 정상입니다. 이 클래스의 객체는 직렬화될 수 있습니다 ." 라고 알려줍니다. 메소드가 없는 인터페이스는 이상해 보입니다./ 왜 필요한가요? 이 질문에 대한 대답은 위에 나와 있습니다. 즉, Java 시스템에 필요한 정보만 제공하는 것입니다. 이전 강의 중 하나에서 마커 인터페이스에 대해 간략하게 언급했습니다. 이는 향후 Java 시스템에 유용할 추가 정보로 클래스를 표시하는 특별한 정보 제공 인터페이스입니다. 구현해야 할 메서드가 없습니다. 따라서 직렬화 가능은 그러한 인터페이스 중 하나입니다. private static final long serialVersionUID또 다른 중요한 점은 클래스에서 정의한 변수입니다 . 왜 필요한가요? 이 필드에는 직렬화된 클래스의 고유 버전 식별자가 포함됩니다 . 직렬화 가능 인터페이스를 구현하는 모든 클래스에는 버전 식별자가 있습니다. 이는 클래스의 내용(필드, 선언 순서, 메소드)을 기반으로 계산됩니다. 그리고 클래스의 필드 유형 및/또는 필드 수를 변경하면 버전 식별자가 즉시 변경됩니다. serialVersionUID는 클래스가 직렬화될 때도 작성됩니다. 역직렬화, 즉 바이트 집합에서 개체를 복원하려고 하면 해당 값이 프로그램의 클래스 serialVersionUID값과 비교됩니다 . serialVersionUID값이 일치하지 않으면 java.io.InvalidClassException이 발생합니다. 아래에서 이에 대한 예를 살펴보겠습니다. 이러한 상황을 방지하려면 클래스에 대해 이 버전 ID를 수동으로 설정하면 됩니다. 우리의 경우에는 단순히 1과 같습니다(원하는 다른 숫자로 대체할 수 있습니다). 이제 객체를 직렬화 SavedGame하고 무슨 일이 일어나는지 살펴보겠습니다!
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // create our object
       String[] territoryInfo = {"Spain has 6 provinces", "Russia has 10 provinces", "France has 8 provinces"};
       String[] resourcesInfo = {"Spain has 100 gold", "Russia has 80 gold", "France has 90 gold"};
       String[] diplomacyInfo = {"France is at war with Russia, Spain has taken a position of neutrality"};

       SavedGame savedGame = new SavedGame(territoryInfo, resourcesInfo, diplomacyInfo);

       //create 2 threads to serialize the object and save it to a file
       FileOutputStream outputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

       // save game to file
       objectOutputStream.writeObject(savedGame);

       // close the stream and release resources
       objectOutputStream.close();
   }
}
보시다시피 우리는 2개의 스레드 FileOutputStream와 를 만들었습니다 ObjectOutputStream. 첫 번째는 파일에 데이터를 쓸 수 있고 두 번째는 개체를 바이트로 변환할 수 있습니다. 예를 들어, 이전 강의에서 유사한 "중첩" 구성을 이미 보았으므로 new BufferedReader(new InputStreamReader(...))놀라지 않아도 됩니다 :) 두 스레드의 "체인"을 생성하여 두 작업을 모두 수행합니다. 객체를 SavedGame바이트 집합으로 변환합니다. 메소드를 사용하여 파일에 저장합니다 writeObject(). 그건 그렇고, 우리는 우리가 얻은 것을 확인하지도 않았습니다! 이제 파일을 살펴볼 차례입니다! *참고: 파일을 미리 생성할 필요는 없습니다. 해당 이름의 파일이 없으면 자동으로 생성됩니다* 그리고 그 내용은 다음과 같습니다! ¬н sr SavedGame [ DiplocyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранцвоюРμС‚ SЃ R РѕСЃСЃРёРμРNo, Р~С ЃРѕР° РЅРёСЏ Р·Р°Ряла РœРѕР·РёС†РёСЋ РЅРμРотралитРμтаuq ~ t "РЈ Р~СЃРר°РЅРёРё 100 золотаt РJ R РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Р~соании 6 РœСЂРѕРІРёРЅС †Рё РΛt %РЈ Р РѕСЃСЃРёРё 10 РœСЂРѕРІРёРЅС †РёРоt &РЈ ФранцРеРё 8 РœСЂРѕРІРІРёРЅС†РёРо 앗, 죄송합니다 :( 우리 프로그램이 작동하지 않는 것 같습니다 :( 실제로는 작동했습니다. 우리가 정확히 바이트 세트를 파일에 전송했다는 것을 기억하십니까? 단순한 객체나 텍스트가 아닌가요? 음, 이것이 이 세트의 모습입니다 :) 이것은 저장된 게임입니다! 원래 객체를 복원하려면, 즉 게임을 로드하고 중단했던 부분부터 계속하려면 다음이 필요합니다. 역방향 프로세스, 역직렬화 ... 우리의 경우에는 다음과 같습니다.
import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);
   }
}
결과는 다음과 같습니다! SavedGame{territoriesInfo=[스페인은 6개 지방, 러시아는 10개 지방, 프랑스는 8개 지방], resourcesInfo=[스페인은 100골드, 러시아는 80골드, 프랑스는 90골드], 외교Info=[프랑스는 러시아와 전쟁 중입니다. 스페인이 중립 위치를 차지했습니다.]} 좋습니다! 먼저 게임 상태를 파일에 저장한 다음 파일에서 복원했습니다. 이제 동일한 작업을 수행하되 SavedGame클래스에서 버전 식별자를 제거해 보겠습니다. 우리는 두 클래스를 모두 다시 작성하지 않을 것이며, 그 안의 코드는 동일할 SavedGame것이므로 private static final long serialVersionUID. 다음은 직렬화 후 개체입니다. ¬н sr SavedGameі€MіuОm‰ [ DiplocyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТВзй{G xp t pФранция РІРѕСЋРμС ‚ СЃ Р РѕСЃСЃРёРμРno, Р~СЃРרанЏзаняла RœRѕР·РёС†РёСЋ РЅРμРοС‚СалитРμтаuq ~ t "РЈ Р~ЃРת анРёРё 100 Р · олотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Фрнции 90 золотаuq ~ t &РЈ Р~СЃР Hiании 6 R בСЂРѕРІРёРЅС† РёРКt %РЈ Р РѕСЃСЃРёРё 10 РרѕСЂРѕРІРЅРёРЅС†РёРʽцинt &РЈ Франции 8 РœСЂРѕРІРІРёРЅС†Рё РЅА 그리고 이를 역직렬화하려고 할 때 다음과 같은 일이 발생했습니다: InvalidClassException: local class in Compatible : stream classdesc serialVersionUID = - 196410440475012755, local class serialVersionUID = -6675950253085108747 이것은 위에서 언급한 것과 동일한 예외입니다. 이에 대한 자세한 내용은 우리 학생 중 한 사람의 기사 에서 읽을 수 있습니다. 그런데 우리는 한 가지 중요한 점을 놓쳤습니다. 문자열과 프리미티브는 분명합니다. 쉽게 직렬화됩니다. Java에는 확실히 몇 가지 기능이 있으며 이를 위한 내장 메커니즘이 있습니다. serializable하지만 -class에 기본 요소가 아닌 다른 개체에 대한 참조로 표현되는 필드가 있으면 어떻게 될까요 ? 예를 들어 , class 와 작업하기 TerritoriesInfo위해 별도의 클래스를 만들어 보겠습니다 . ResourcesInfoDiplomacyInfoSavedGame
public class TerritoriesInfo {

   private String info;

   public TerritoriesInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "TerritoriesInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class ResourcesInfo {

   private String info;

   public ResourcesInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "ResourcesInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class DiplomacyInfo {

   private String info;

   public DiplomacyInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "DiplomacyInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}
하지만 이제 질문이 있습니다. 변경된 클래스를 직렬화하려면 이러한 클래스를 모두 직렬화할 수 있어야 합니까 SavedGame?
import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private TerritoriesInfo territoriesInfo;
   private ResourcesInfo resourcesInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public TerritoriesInfo getTerritoriesInfo() {
       return territoriesInfo;
   }

   public void setTerritoriesInfo(TerritoriesInfo territoriesInfo) {
       this.territoriesInfo = territoriesInfo;
   }

   public ResourcesInfo getResourcesInfo() {
       return resourcesInfo;
   }

   public void setResourcesInfo(ResourcesInfo resourcesInfo) {
       this.resourcesInfo = resourcesInfo;
   }

   public DiplomacyInfo getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(DiplomacyInfo diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoriesInfo=" + territoriesInfo +
               ", resourcesInfo=" + resourcesInfo +
               ", diplomacyInfo=" + diplomacyInfo +
               '}';
   }
}
자, 실제로 확인해 봅시다! 지금은 모든 것을 그대로 두고 객체를 직렬화해 보겠습니다 SavedGame.
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // create our object
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourcesInfo resourcesInfo = new ResourcesInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a position of neutrality");


       SavedGame savedGame = new SavedGame(territoriesInfo, resourcesInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}
결과: "main" 스레드 java.io.NotSerializedException에서 예외가 발생했습니다. DiplomacyInfo가 실패했습니다! 사실, 여기에 우리 질문에 대한 답이 있습니다. 객체를 직렬화하면 인스턴스 변수에서 참조하는 모든 객체가 직렬화됩니다. 그리고 해당 개체가 세 번째 개체도 참조하는 경우 해당 개체도 직렬화됩니다. 그리고 무한히 계속됩니다. 이 체인의 모든 클래스는 직렬화 가능해야 합니다. 그렇지 않으면 직렬화 가능하지 않고 예외가 발생합니다. 그런데 이것은 앞으로 문제를 일으킬 수 있습니다. 예를 들어 직렬화 중에 클래스의 일부가 필요하지 않은 경우 어떻게 해야 합니까? 또는 예를 들어 TerritoryInfo프로그램의 클래스를 일부 라이브러리의 일부로 상속했습니다. 그러나 직렬화 가능하지 않으므로 변경할 수 없습니다. TerritoryInfo클래스에 필드를 추가 SavedGame할 수 없다는 것이 밝혀졌습니다 . 왜냐하면 전체 클래스가 SavedGame직렬화 불가능해지기 때문입니다! 문제:/ Java의 직렬화 및 역직렬화 - 2이런 종류의 문제는 Java에서 키워드를 사용하여 해결됩니다 transient. 이 키워드를 클래스 필드에 추가하면 이 필드의 값이 직렬화되지 않습니다. 우리 클래스의 필드 중 하나를 만들어 보겠습니다 SavedGame transient. 그런 다음 하나의 개체를 직렬화하고 복원합니다.
import java.io.Serializable;

public class SavedGame implements Serializable {

   private transient TerritoriesInfo territoriesInfo;
   private ResourcesInfo resourcesInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   //...getters, setters, toString()...
}



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

public class Main {

   public static void main(String[] args) throws IOException {

       // create our object
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourcesInfo resourcesInfo = new ResourcesInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a position of neutrality");


       SavedGame savedGame = new SavedGame(territoriesInfo, resourcesInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}


import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);

       objectInputStream.close();


   }
}
결과는 다음과 같습니다: SavedGame{territoriesInfo=null, resourcesInfo=ResourcesInfo{info='스페인은 100골드, 러시아는 80골드, 프랑스는 90골드'}, DiplocyInfo=DiplomacyInfo{info='프랑스는 러시아와 전쟁 중입니다. 스페인은 입장 중립을 취했습니다.'}} 동시에 - transient필드에 어떤 값이 할당될 것인지에 대한 질문에 대한 답변을 받았습니다. 기본값이 할당되어 있습니다. 객체의 경우 이는 입니다 null. 여가 시간에는 직렬화에 관한 훌륭한 기사를 읽어보세요 . Externalizable또한 다음 강의에서 다루게 될 인터페이스에 대해서도 이야기합니다 . 또한 "Head-First Java" 책에 이 주제에 대한 장이 있으니 주의하세요 :)
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION