본문 바로가기

CS

데이터 직렬화(Serialization), 역직렬화(Deserialization)

데이터 직렬화(Data Serialization)

메모리의 데이터를 물리적으로 저장하거나 전송할 수 있는 형태로 변환하는 작업 (e.g. Bytes, XML, JSON, YAML...)

직렬화가 필요한 이유

데이터의 메모리 구조

  • 값 형식 데이터(Value Type): Stack에 '값 그 자체'를 저장해서 바이트 형식으로 변환 가능
  • 참조 형식 데이터(Reference Type): Stack에 '참조하는 데이터의 Heap 메모리 주소'를 저장하므로 이 값을 파일 저장하거나 네트워크 전송한들 읽어들이는 시점(프로그램 종료 이후, 외부 메모리 환경)에서는 의미 없는 데이터일 것이다. 따라서 물리적 저장/전송 이전에 직렬화를 통해 참조 시점에 존재하는 데이터를 값 형식으로 변환할 필요가 있다.

직렬화 데이터 형식

다양한 애플리케이션, 플랫폼 또는 시스템 간에 데이터를 교환할 수 있도록 정한  표준화 방식

  • Binary: 컴퓨터가 쉽게 이해할 수 있는 이진 방식 (e.g. Bytes...)
  • Text: 사람이 쉽게 이해할 수 있는 문자열 방식 (e.g. XML, JSON, YAML...)

XML ( Extensible Markup Language )

  • 데이터 정의 규칙을 제공하는 마크업 언어
  • Tag를 사용한 트리 구조로 데이터를 표현하여 HTML과 유사한 형태
  • 방대한 데이터 저장에 유리
  • 스키마 파일로 유효성 검증 가능
  • 주석 지원 O

JSON ( JavaScript Object Notation )

  • JavaScript 객체를 정의하는 표현식
  • 키-값 페어를 사용한 Map과 유사한 형태
  • 간단한 구조로 파싱 용이
  • 표준 JavaScript 함수로 구문 분석 가능 (JSON.stringify(), JSON.parse())
  • 분산 소프트웨어 통신, 웹 애플리케이션, 구성 파일 및 API 등 광범위하게 사용
  • 문법 오류에 취약
  • 주석 지원 X

YAML

  • 키-값 페어를 사용하고 들여쓰기(indent)로 계층을 표현
  • XML을 단순화하기 위해 고안되어 가독성이 좋음
  • 편의성을 우선하여 자동화, DevOps, 코드형 인프라(IaC) 도구 및 서비스의 구성 파일에 사용
  • 주석 지원 O

데이터 역직렬화(Data Deserialization)

디스크나 네트워크로부터 바이트나 문자열을 읽어 메모리에서 사용할 수 있는 형태로 변환하는 작업

자바 객체 직렬화/역직렬화

Serializable 인터페이스, 객체 입출력 스트림

public class Member implements Serializable {
    private static final long serialVersionUID = 100L;
    
    private String username;
    private String email;
    private transient String password;

    public Member(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    public String hello() {
        return "Hello " + username;
    }

    @Override
    public String toString() {
        return  String.format("Member{name=%s, email=%s, password=%s}", username, email, password);
    }
}
  • implements java.io.Serializable: 직렬화/역직렬화 대상 클래스가 구현하는 Marker Interface
  • serialVersionUID
    • Serializable 구현 클래스에서 관리하는 버전 식별자
    • 직렬화 데이터에 대상 객체의 버전을 포함하여 역직렬화 시 버전 체크에 사용
    • 명시적으로 정의하지 않으면 시스템 내부적으로 class의 기본 해시값을 사용하여 유니크한 번호를 생성하여 관리
  • 직렬화하고 싶지 않은 필드는 transient 키워드 사용 
public static void main(String[] args) throws IOException, ClassNotFoundException {
        Member member = new Member("member", "member@sample.com", "password");

        // 직렬화
        byte[] serializedMember;
        try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            try(ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                oos.writeObject(member);
                serializedMember = baos.toByteArray();
            }
        }
        String encodedMember = Base64.getEncoder().encodeToString(serializedMember);
        System.out.println(encodedMember);

        // 역직렬화
        Member deserializedMember;
        try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedMember)) {
            try (ObjectInputStream ois = new ObjectInputStream(bais)) {
                Object objectMember = ois.readObject();
                deserializedMember = (Member) objectMember;
            }
        }
        System.out.println(deserializedMember);
        System.out.println(deserializedMember.hello());
    }
  • java.io.ObjectOutputStream: 메모리 상의 자바 객체를 직렬화하는 메서드( writeObject() ) 제공
  • java.io.ObjectInputStream: 직렬화된 바이트 데이터를 객체로 변환하는 역직렬화 메서드( readObject() ) 제공

실행 결과

rO0ABXNyACVjb20uZXhhbXBsZS5qYXZhcHJvamVjdC5kb21haW4uTWVtYmVyv5xtVCfJQ9cCAAJMAAVlbWFpbHQAEkxqYXZhL2xhbmcvU3RyaW5nO0wACHVzZXJuYW1lcQB+AAF4cHQAEW1lbWJlckBzYW1wbGUuY29tdAAGbWVtYmVy
Member{name=member, email=member@sample.com, password=null}
Hello member

Jackson ObjectMapper

Jackson 라이브러리 추가

<!-- pom.xml 의존성 추가 (maven 방식) -->
<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	<version>버전</version>
</dependency>

 

JSON 직렬화/역직렬화

public static void main(String[] args) throws IOException, ClassNotFoundException {
        Member member = new Member("member", "member@sample.com", "password");
        
        // 직렬화: Object to JSON
        ObjectMapper objectMapper = new ObjectMapper();
        String serializedMember = objectMapper.writeValueAsString(member);
        System.out.println(serializedMember);
        
        // 역직렬화: JSON to Object
        Member deserializedMember = objectMapper.readValue(serializedMember, Member.class);
        System.out.println(deserializedMember);
        System.out.println(deserializedMember.hello());

    }
}
  • ObjectMapper.writeValueAsString() : 객체를 JSON 문자열로 변환
  • ObjectMapper.readValue() : 직렬화된 데이터를 객체로 변환 (String, byte[], URL, File, InputStream, Reader... to Object)
  • 직렬화 과정에서 접근지시자에 의해 하나의 필드도 접근할 수 없으면 InvalidDefinitionException 발생
    • private 필드의 경우 getter 필요
  • 역직렬화 과정에서 Reflection을 통해 객체를 생성하므로 대상 클래스에 기본 생성자(default constructor)가 없으면 InvalidDefinitionException 발생

실행 결과

{"username":"member","email":"member@sample.com","password":"password"}
domain.Member{name=member, email=member@sample.com, password=password}
Hello member

 

 

※ 특정 필드 직렬화/역직렬화 배제하는 방법

  • @JsonIgnoreProperties: 클래스 레벨로 특정 필드 지정
  • @JsonIgnore: 필드 레벨로 지정
  • @JsonIgnoreType: 클래스 레벨로 전체 클래스 지정