본문 바로가기
Language/자바&코틀린

직렬화와 역직렬화

by 코딩공장공장장 2024. 7. 19.

직렬화(Serialization)

메모리에 존재하는 데이터를 디스크에 저장하거나 다른 네트워크에 전송하기 위한 포맷으로 변환(직렬화)하고
이를 다시 재구성 할 수 있는 포맷으로 변환(역직렬화)하는 과정

직렬화의 종류

  • json 직렬화
    • 정의 : json 형식의 문자열로 변환
    • 장점 : json 형식이기에 모든 프로그램에서 사용 가능함
    • 사용 예시 :  - 직렬화 : 스프링 서버에서 응답 객체를 클라이언트에 json 형식으로 변환함
                        - 역직렬화 : 클라이언트가 json형식으로 전송한 http 메시지의 request body를 객체로 변환함
  • 바이너리 직렬화
    • 정의 : 바이너리 코드라는 기계어로 변환
    • 장점 : 텍스트 기반 직렬화보다 저장 공간에 효율적이며, 전송 속도가 빠름
    • 사용예시- 직렬화 : 브라우저에서 다른 서버에 요청을 보낼때, OS가 HTTP메시지를 바이너리 코드로 변환
                      - 역직렬화 : OS가 다른 서버로 부터 전달 받은 바이너리 코드 형식의 데이터를 HTTP메시지 문자열로 변환
  • 자바 직렬화 
    • 자바 바이트 코드로 표현
    • 자바 시스템끼리만 사용 가능
    • 클래스 타입, 필드 타입과 같은 정보가 포함됨

직렬화가 필요한 이유

프로그래밍 상에 메모리 영역에 생성되는 객체는 스택에서 주소값을 통해 참조하게 된다. 실제 값을 갖고 있지 않는다.

만약 디스크에 저장하거나 외부 네트워크로 전달시 참조 주소를 보내게 되면

이후 역직렬화시에 객체가 제거됬거나 프로그램을 재시작하게 되면 해당 주소에 객체가 존재하지 않을 수 있다.

따라서 객체의 실제 값을 가져와 데이터를 만들어줘야하는데 이 과정이 직렬화의 과정이다.

직렬화 예제

json 직렬화

아래는 jakson 라이브러리를 통한 json 직렬화 처리 예제이다.

private static void jsonSerializer() {

   // 자바 객체 생성

   User user = new User();

   user.setEmail("afjl@test.com");

   user.setPasswd("4567");

 

   // ObjectMapper 인스턴스 생성

   ObjectMapper objectMapper = new ObjectMapper();

   try {

       // 자바 객체를 문자열로 변환 (직렬화)

       String userStr = objectMapper.writeValueAsString(user);

 

       // 결과 출력

       System.out.println(user);

   } catch (Exception e) {

       e.printStackTrace();

   }

}

 

private static void jsonDeserializer() {

   // JSON 문자열

   String jsonString = "{\"email\":\"sad@text.com\", \"passwd\":4567}";

 

   // ObjectMapper 인스턴스 생성

   ObjectMapper objectMapper = new ObjectMapper();

   try {

       // JSON 문자열을 User 객체로 변환 (역직렬화)

       User user = objectMapper.readValue(jsonString, User.class);

 

       // 결과 출력

       System.out.println(user);

   } catch (Exception e) {

       e.printStackTrace();

   }

}

 

직렬화와 역직렬화의 결과를 출력해보니 아래와 같았다.

Member{email='afjl@test.com', passwd='4567'}

자바 직렬화

자바의 직렬화는 Serializable 인터페이스를 상속 받아 사용한다.

public interface Serializable {

}

Serializable 인터페이스는 아무런 추상 메서드가 없는 마커 인터페이스로 단지 클래스가 직렬화가 가능하다는 것을 표시하는 목적이다.

 

[예제]

아래와 같이 직렬화 대상이 되는 클래스에 Serializable 인터페이스를 상속 받았다.

public class Member implements Serializable {

   private String email;

   private String passwd;

 

   public void setEmail(String email) {

       this.email = email;

   }

 

   public void setPasswd(String passwd) {

       this.passwd = passwd;

   }

 

   @Override

   public String toString() {

       return "Member{email='" + email + "', passwd='" + passwd + "'}";

   }

}

스트림을 통해 직렬화와 역직렬화 과정을 구현하였다.

내용은 코드를 통해 충분히 이해할 수 있을 것이다.

public class SerializerMain {

   public static void main(String[] args) {

       serialize();

       deserialize();

   }

 

   // 자바 직렬화

   private static void serialize() {

       // 객체 생성

       Member member = new Member();

       member.setEmail("serializer@test.com");

       member.setPasswd("1234");

 

       // 파일 쓰기

       String fileName = "serializeTest.ser";

       try (

           FileOutputStream fos = new FileOutputStream(fileName);

           ObjectOutputStream out = new ObjectOutputStream(fos)

       ) {

           // 직렬화 데이터 파일에 쓰기

           out.writeObject(member);

       } catch (IOException e) {

           e.printStackTrace();

       }

   }

 

   // 자바 역직렬화

   private static void deserialize() {

       // 파일 읽기

       String fileName = "serializeTest.ser";

       try (

           FileInputStream fos = new FileInputStream(fileName);

           ObjectInputStream in = new ObjectInputStream(fos)

       ) {

           // 역직렬화

           Member mem = (Member) in.readObject();

           System.out.println(mem);

       } catch (IOException | ClassNotFoundException e) {

           e.printStackTrace();

       }

   }

}

 

 

직렬화된 파일을 보니 아래와 같이 읽기 어려운 문자들이 적혀있다. 바이트 코드 형태로 저장된다.

읽을 수 있는 부분은 com.javastudy.serialize.Member라는 패키지 정보를 포함한 클래스 타입 java/lang/String 라는 필드 타입,

serializer@test.com과 1234라는 필드 값이다.
직렬화에 필드값 뿐만 아니라 클래스와 필드 타입 정보가 함께 저장되는 것을 알 수 있다.
json 문자열 직렬화에 비해 저장되는 데이터가 많다.

��srcom.javastudy.serialize.Member�����WLemailtLjava/lang/String;Lpasswdq~xptserializer@test.comt1234

 

자바 직렬화의 특징

자바 직렬화는 json 문자열과 다르게 클래스와 필드의 타입 정보가 함께 저장된다. 

자바 객체 메타 정보를 포함하기에 역직렬화시 클래스의 상위 타입으로 선언하더라도 인스턴스의 실제타입은 직렬화 데이터에 포함한 클래스 타입으로 메모리에 객체가 생성된다.

 

따라서 자바 역직렬화 Object타입으로 받은 이후 실제 인스턴스 타입으로 형변환이 가능하다.

Object mem = in.readObject();

Member mem2 = (Member) mem;

System.out.println(mem2);

 

허나, json 역직렬화시에는 Object타입으로 받은 이후 형변환시 java.lang.ClassCastException 이 발생하게 된다.

Object user = objectMapper.readValue(jsonString, Object.class);

User user2 = (User) user;

 

직렬화 데이터가 아래와 같은 구조였기에 자바에서는 email과 passwd를 포함하는 클래스가 아니라면 LinkedHashMap 형태로 변환한다.

String jsonString = "{\"email\":\"sad@text.com\", \"passwd\":4567}";

 

이러한 특징으로 자바 시스템끼리 직렬화에는 최적화 되어있지만 메타 정보가 존재하기에 저장공간을 많이 필요로한다.

이로인해 자바 시스템이 아닌 외부 시스템에 저장하는 경우라면 자바 직렬화를 사용해야하는 이유가 부족해진다.

또한, 자바 역직렬화 과정에서는 바이트 코드를 실행하게 되어 전송 과정에서 제 3자에게 탈취되어 변조되는 경우
프로그래밍 서버에서 변조된 코드가 실행되는 보안상의 치명적인 단점이 존재한다.

 

자바 직렬화 사용법

transient

transient 키워드는 직렬화 대상에서 제외하는 경우에 사용되는 키워드이다.

public class Member implements Serializable {

   private static final long serialVersionUID = 1L;

   private String emails;

 

   // transient 키워드 사용시 직렬화에 포함 안됨

   transient private String passwd;

 

   // . . .

}

Member{email='serializer@test.com', passwd='null, profile.gender='남자'}

trasient를 사용하면 참조타입은 null, 원시타입은 기본값으로 변환된다.(ex. int는 0)

 

상속관계

public class Bird {

   private String wing;

 

   public String getWing() {

       return wing;

   }

 

   public void setWing(String wing) {

       this.wing = wing;

   }

}

 

public class Eagle extends Bird implements Serializable {

   private String claw;

 

   public void setClaw(String claw) {

       this.claw = claw;

   }

 

   @Override

   public String toString() {

       return "Eagle{wing='" + super.getWing() + "', claw='" + claw + "'}";

   }

}

 

상속관계에서 상위 타입에 Serializable을 선언하고 하위타입에 선언하지 않은 경우 당연히 Serializable까지 상속되므로

직렬화시 상위 타입의 필드도 포함하지만, 위와 같이 하위타입에만 선언한 경우 상위타입 필드는 포함되지 않는다.

 

출력 결과 : 

Eagle{wing='null', claw='발톱'}

 

serialVersionUID

Serializable 인터페이스를 상속받은 클래스 파일은 serialVersionUID라는 고유한 값을 할당 받는다.

자바 직렬화는 클래스 타입, 필드 타입과 같은 정보를 저장하기에 클래스 구조를 식별하기 위한 고유한 식별값을 사용한다.

따라서 클래스 구조를 바꾸면 식별값을 바꿔 역직렬화시 아래와 같은 에러를 터트리게 할 수 있다.

java.io.InvalidClassException: com.javastudy.serialize.Member; local class incompatible:

 

따라서 직렬화 클래스에 아래와 같이 serialVersionUID 값을 직접 명시해주는 것이 바람직하고

버전이 바뀌었을 때 새로운 식별값을 할당해줘야 또다른 예외 발생에서 벗어날 수 있다.

private static final long serialVersionUID = 1L;

 허나, 직렬화 버전 관리는 단순히 버전관리의 일환이고 예외 상황에서 디버깅을 수월하게 하는데 도움이 되는 것이지

이전 버전으로 역직렬화를 해주는 것은 아니다.

반응형