본문 바로가기
JAVA/[JAVA] 바구니

[JAVA] JAVA Serialize

by oncerun 2021. 4. 23.
반응형

프로젝트를 보게 되면 심심치 않게 보는 코드가 implements Serializable입니다.

 

저는 아무렇지도 않게 넘기다가 문득 궁금해져서 한번 파보려고 합니다.

 

자바에서 직렬화란 자바 시스템에서 사용되는 객체 혹은 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트 형태로 데이터를 변환하는 기술과 역으로 바이트 형태의 데이터를 객체로 바인딩시켜주는 것을 말합니다.

 

사실 자바스크립트에서도 JSON.parse를 통해 객체를 json으로 직렬 화하는 코드를 많이 보셨을 겁니다. JSON과 마찬가지로 자바 직렬 화도 하나의 포맷 형태로 생각할 수 있습니다. 

 

어떤 데이터 직렬 화가 존재할까요?

 

CSV : 표 형태의 다량의 데이터를 직렬 화하는데 가장 많이 쓰이는 포맷으로 , (콤마)를 기준으로 데이터를 구분합니다.

자바에서는 Apache Commons CSV, opencsv 등의 라이브러리 이용하여 사용 가능합니다.

 

JSON : 요즘 가장 많이 사용하는 형태로 구조화된 데이터를 직렬화 할 때 사용합니다. 예전에는 XML을 많이 사용했습니다. 자바스크립트 자체에서도 지원하기도 하고, 다른 포맷 방식보다 오버헤드가 적다는 점 등 특징이 있습니다.

자바에서는 GSON, Jackson 등 JSON을 위한 라이브러리가 존재합니다.  

스프링 부트 프레임워크에서는 Jackson을 기본값으로 사용합니다. Jackson은 ObjectMapper을 통해 자바 객체를 JSON 포맷으로 변경시켜 외부 시스템과 통신을 합니다.

 

실습을 위해 Jackson core/ annotations / databind의 라이브러리를 다운로드하여 프로젝트에 추가시켜줬습니다.

 

  ByteArrayInputStream baisInput = new ByteArrayInputStream(serializeMember);
        ObjectInputStream ois = new ObjectInputStream(baisInput);

        Object objectMember = ois.readObject();
        Member member1 = (Member) objectMember;
        String memberJson = objectMapper.writeValueAsString(member1);
        System.out.println(memberJson);

결괏값은 다음과 같습니다.

{"name":"sung","email":"naver","age":24,"orderList":[]}

 

이때 오류가 나시는 분들은 다음을 고려해야 합니다.

스프링 환경에서 한다면 ObjectMapper는 객체를 JSON으로 변환할 때 getter메서드를 사용합니다. 따라서 getter가 존재하지 않으면 에러가 발생합니다.

역으로 JSON을 객체로 변환할 때 기본 생성자는 필수입니다. 생성자를 오버라이드 하셨다면 기본 생성자가 생성되지 않으므로 생성하셔야 합니다.

 

 

 

 

JSON, CSV, 프로토콜 버퍼(구글) 등은 시스템의 고유 특성과 상관없는 대부분의 시스템에서의 데이터 교환에서 많이 사용되지만 자바 직렬화는 자바 시스템 간의 데이터 교환에 만을 위해서 존재합니다. 

 

 

 

이 부분은 자바 개발자에게는 편한 부분이기도 하지만 확장에는 무리가 올 거라고 생각합니다.

 

JVM내의 데이터 교환은 implements Serializable을 통해 별다른 라이브러리 추가 없이 쉽게 복잡한 객체의 데이터의 교환이 가능하지만, 그렇지 않은 경우는 특정 라이브러리를 활용하거나, 특정 기술에 의존하여 데이터를 교환해야 합니다.

 

 

그럼 어디서 언제 사용될까?

 

JVM의 메모리에서만 상주되어 있는 객체를 데이터 그대로 영속화할 경우 사용합니다. 시스템이 종료되더라도 특정 장소에 보관할 수 있으며 네트워크 통신을 통해 전송도 가능합니다. 또한 필요할 때 역직렬화를 통해 객체로 재사용이 가능하죠.

 

대신 직렬화된 데이터를 역직렬화 하는 곳에서는 다음과 같은 조건이 구현되어야 합니다.

 

역직렬화 조건

직렬화 대상이 된 객체의 클래스가 클래스 패스에 존재햐아 하며 import 되어 있어야 합니다.

중요한점은
직렬화와 역직렬화를 진행하는 시스템이 서로 다를 수 있다는 것을 반드시 고려해야 합니다.
같은 시스템 내부이라도 소스버전이 다를 수 있습니다.

자바 직렬화 대상 객체는 동일한 serialVersionUID를 가지고 있어야 합니다.

private static final long serialVersionUID = 1L;
다만 SUID는 필수 값이 아닙니다. 만약 생략한 후 직렬화를 하게 되면 클래스의 hashcode값으로 SUID가 생성돼 어전 달 됩니다.  그렇기 때문에 보내는 쪽이 아닌 받는 쪽에서 해당 클래스의 구조를 변경하게 되면 역직렬화 예외가 발생합니다.

그래서 보내는 쪽 받는 쪽 모두 같은 SUID를 사용해야 하고 개발자는 이를 관리해야 합니다.

SUID만 관리한다면 필드 추가와 메서드 추가에는 어떠한 문제도 없습니다. 다만 타입에 변경을 엄격히 관리하기 때문에 int형을 Long으로 변경하는 등 타입 변경에도 예외가 발생하게 됩니다.

 

 

이러한 자바 직렬화는 다음과 같은 곳에서 사용됩니다.

 

서블릿 세션

 

서블릿 기반의 WAS들은 대부분 세션의 자바 직렬화를 지원하고 있습니다.
단순히 세션을 서블릿 메모리 위에서 운용한다면 직렬 화가 필요 없지만 파일로 저장하거나 세션 클러스터링,, DB를 저장하는직렬 화가 되어서 저장된 후 전달됩니다.
그래서 세션에 필요한 객체는 java.io.Serializable 인터페이스를 구현해(implements) 해두는 것을 추천합니다.

 

캐시


자바 시스템에서 퍼포먼스를 위해 Ehcache, Redis, 라이브러리를 시스템에 많이 이용하게 됩니다.
자바 시스템을 개발하다 보면 상당수의 클래스가 만들어집니다.예를 들면 DB를 조회한 후 가져온 데이터 객체 같은 경우 실시간 형태로 요구하는 데이터가
아니라면 메모리, 외부 조정소, 파일 등을 저장소로 이용해서 데이터 객체를 저장한 후 동일 요청이 올 시 DB에 재요청하는 것이 나라 저장된 객체를 찾아서 응답하게 하는 형태를 보통 캐시를 사용한다고 합니다.
캐시를 이용하면 DB에 대한 리소를 절약할 수 있기 때문에 많은 시스템에서 자주 활용됩니다.
자바자바 RMI(Remote Method Invocation)


원격 시스템 간의 메시지 교환을 위해서 자바에서 지원하는 기술.
보통은 원격의 시스템과 통신을 위해서 IP와 포트를 이용해서 소켓통신을 해야 하지만 RMI는 추상화하여 원격에 있는 시스템의 메서드를 로컬 시스템의 메서드인 것처럼 호출할 수 있습니다.
원격의 시스템의 메서드를 호출 시에 전달하는 메시지(보통 객체)를 자동으로 직렬화 시켜 사용됩니다.
받은쪽은 역직렬화를 하여 사용합니다.
자바 개발자 입장에서는 상당히 쉽고 빠르게 사용할 수 있도록 만든 기술입니다.

 

 

하나의 클래스를 생성해 자바 직렬화를 이용해 보겠습니다.

먼저 기본 조건을 만족시킬 클래스를 정의합니다.

package com.company.object;

import java.io.Serializable;
import java.util.List;


//Serializable 인터페이스를 상속받은 객체는 직렬화할 수 있는 기본 조건을 가짐.
public class Member implements Serializable {

    private String name;
    private String email;
    private int age;
    private List<Order> orderList;
    public Member() {
    }

    public Member(String name, String email, int age, List<Order> orderList) {
        this.name = name;
        this.email = email;
        this.age = age;
        this.orderList = orderList;
    }

    public List<Order> getOrderList() {
        return orderList;
    }

    public String getName() {
        return name;
    }
    public String getEmail() {
        return email;
    }
    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Member{" +
                "name='" + name + '\'' +
                ", email='" + email + '\'' +
                ", age=" + age +
                '}';
    }
}

 

 

Member 객체를 직렬 화하기 위해선 자바 I/O를 사용해야 합니다. 바이트 형태로 보내기 위해선 ByteArrayStream을 이용해야 하고 보조 스트림으로 ByteArrayStream을 wrapping한후ObjectStream으로 객체 쓰거나 읽습니다.

 

   //io 패키지의 ObjectOutputStream 객체를 사용합니다.

        Member member = new Member("신진이", "sincostan@naver.com", 42, Arrays.asList(new Order("test")));

        byte[] serializeMember;

        try(ByteArrayOutputStream baos = new ByteArrayOutputStream()){
            try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                oos.writeObject(member);
                serializeMember = baos.toByteArray();
                System.out.println("serialize된 Member객체 "  + serializeMember);
            }
        }
        //Base64로 인코딩된 바이트배열 객체
        System.out.println("Base64로 인코딩된 바이트배열 객체 " + Base64.getEncoder().encodeToString(serializeMember));

다음은 결과 값입니다.

 

serialize된 Member객체 [B@54bedef2
Base64로 인코딩된 바이트배열 객체 rO0ABXNyABljb20uY29tcGFueS5vYmplY3QuTWVtYmVyZ5SvNjXf9jICAARJAANhZ2VMAAVlbWFpbHQAEkxqYXZhL2xhbmcvU3RyaW5nO0wABG5hbWVxAH4AAUwACW9yZGVyTGlzdHQAEExqYXZhL3V0aWwvTGlzdDt4cAAAACp0ABNzaW5jb3N0YW5AbmF2ZXIuY29tdAAJ7Iug7KeE7J20c3IAGmphdmEudXRpbC5BcnJheXMkQXJyYXlMaXN02aQ8vs2IBtICAAFbAAFhdAATW0xqYXZhL2xhbmcvT2JqZWN0O3hwdXIAG1tMY29tLmNvbXBhbnkub2JqZWN0Lk9yZGVyO2ctxXRKPlWMAgAAeHAAAAABc3IAGGNvbS5jb21wYW55Lm9iamVjdC5PcmRlchaV7VyQ5YktAgABTAAEbmFtZXEAfgABeHB0AAR0ZXN0

 

그럼 이제 역직렬화를 진행해야 합니다 동일한 Stream을 사용하여 역직렬화를 해야 합니다.

 

직렬화된 데이터를 파일에서 읽든 전달 받든 그것은 구현에 따라 다른 거 아시죠?

저는 직접 생성했기 때문에 바로 넣어줄 것입니다.

 //base64 디코딩
        String base64Member = Base64.getEncoder().encodeToString(serializeMember);
        serializeMember = Base64.getDecoder().decode(base64Member);


        ByteArrayInputStream bais = new ByteArrayInputStream(serializeMember);
        ObjectInputStream ois = new ObjectInputStream(bais);

        Object objectMember = ois.readObject();
        Member member1 = (Member) objectMember;

        System.out.println(member1);

 

그 결괏값은 다음과 같습니다.

Member{name='신진이', email='sincostan@naver.com', age=42, orderList=[Order{name='test'}]}

 

 

정리


SUID의 값은 개발 시 직접 관리해야 한다..
SUID의 값이 동일하면 멤버 변수 및 메서드 추가는 크게 문제가 없다.
역직렬화 대상의 클래스의 타입 변경을 지양해야 한다..
외부(캐시, DB , NoSQL)에 장기간 저장될 정보는 자바 직렬화 사용을 지양해야 합니다. (대상 클래스가

결국 역직렬 화가 되지 않을 때와 같은 예외처리는 기본적으로 해야 한다..
용량이 크다. 메타. 정보도 가지고 있기 때문. 자바 웹 시스템에서 가장 많이 사용되는 스프링 프레임워크에서 기본적으로 지원하는 캐시 모듈 중 외부 시스템에 저장하는 형태에서 기본적으로 자바 직렬화 형태로 제공된다. (Spring Data Redis, Spring Session...)

 

메타 정보를 포함한 자바 직렬화 포맷은 타 데이터 포맷과는 다르게 용량이 상대적으로 크기 때문에 이 차이가 쌓이다 보면 저장공간을 위협할 수 있습니다.

B2C와 같은 시스템에서 자바 직렬화 정보를 캐시 서버에 저장할 때는 비효율적인 문제를 가지고 있다. 용량크기에 따른 네트워크 비용과, 캐시 서버 비용
JSON형태 혹은 다른 형태를 새로운 서비스를 할 때는 고려해보는 것이 좋다.

 

용량 실습

package com.company.object;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;

public class Capacity {

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

        ObjectMapper objectMapper = new ObjectMapper();

        Member member = new Member("sung", "naver", 24, new ArrayList<Order>());

        ByteArrayOutputStream bais = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bais);

        oos.writeObject(member);

        byte[] serializeMember = bais.toByteArray();

        System.out.println(serializeMember);
        System.out.printf("serializedMember (byte size = %s) \n", serializeMember.length);

        ByteArrayInputStream baisInput = new ByteArrayInputStream(serializeMember);
        ObjectInputStream ois = new ObjectInputStream(baisInput);

        Object objectMember = ois.readObject();
        Member member1 = (Member) objectMember;
        //JSON 용량 체크
        String memberJson = objectMapper.writeValueAsString(member1);
        System.out.println(memberJson);
        System.out.printf("json (byte size = %s) \n", memberJson.getBytes(StandardCharsets.UTF_8).length);




    }
}

 

 

결과 값은 다음과 같습니다. 

[B@30c7da1e
serializedMember (byte size = 197) 
{"name":"sung","email":"naver","age":24,"orderList":[]}
json (byte size = 55) 

 

 

좋은 이론과 실습을 제공해 준 우아한 블로그에 감사를

woowabros.github.io/experience/2017/10/17/java-serialize.html

 

자바 직렬화, 그것이 알고싶다. 훑어보기편 - 우아한형제들 기술 블로그

자바의 직렬화 기술에 대한 대한 이야기입니다. 간단한 질문과 답변 형태로 자바 직렬화에 대한 간단한 설명과 직접 프로젝트를 진행하면서 겪은 경험에 대해 이야기해보려 합니다.

woowabros.github.io

 

반응형

'JAVA > [JAVA] 바구니' 카테고리의 다른 글

SSL/TLS 서버 통신 (JSSE, TrustManager)  (1) 2021.11.06
짧)[JAVA] 객체지향 세계  (0) 2021.04.25
[JAVA] Stream  (0) 2021.04.19
[JAVA] 디자인 패턴  (0) 2021.04.13
[JAVA] 예외 처리  (0) 2021.03.27

댓글