개발 기록

> [Java] 직렬화 (Serializable) 에 대해서 1. 개념과 구현 본문

JAVA

> [Java] 직렬화 (Serializable) 에 대해서 1. 개념과 구현

1z 2020. 9. 22. 11:23

 

1. 개념 

직렬화:  객체를 정적 바이트 스트림으로 변환하는 기술 
역직렬화 : 바이트로 변환된 데이터를 다시 객체로 변환하는 기술
여기서 객체는 JVM(Java Virtual Machine 이하 JVM)의 메모리에 상주(힙 또는 스택)되어 있는 객체 데이터를 말한다.

직렬화된 객체는 외부의 자바 시스템에서도 사용할 수 있고 데이터베이스에 저장하거나 네트워크를 통해 전송할 수 있다. (파일, 통신이 주 용도)

2. 직렬화 

 

(1) 직렬화 조건

자바 기본(primitive) 타입과 java.io.Serializable 인터페이스를 상속받은 객체는 직렬화 할 수 있는 기본 조건을 가진다.

Static Field 는 Class 에 속하기 때문에 Serialize 되지 않는다.

클래스가  Serializable interface 구현하면 모든 하위 클래스도 직렬화 가능하다.

객체가 다른 객체에 대한 참조를 갖고 있는 경우 해당 객체도 직렬화 가능 인터페이스를 별도로 구현해야 한다. 그렇지 않으면 NotSerializedException이 발생한다.

 

*  Serialize하는 과정에 제외하고 싶은 경우 transient 키워드를 사용하면 된다.

* Java 직렬화는 I/O 스트림을 많이 사용한다. 스트림을 닫는 것을 잊어 버리면 리소스 누수가 발생하기 때문에 꼭 stream 을 닫아야한다.  (리소스 누출 방지 : try-with-resources)

 

 

(2) 직렬화 구현

 

객체를 직렬화하기 위해 ObjectOutputStream 클래스 의 writeObject() 메서드를 호출한다.

writeObject() 메서드는 직렬화 가능한 개체를 가져와 이를 바이트 시퀀스(스트림)로 변환한다. 

public final void writeObject(Object o) throws IOException;

 

직렬화 가능한 개체를 만들기 위해 Serializable interface 구현 

    /**
    * 직렬화할 회원 클래스
    */
    @Data
    public class User implements Serializable {
        private String name;
        private String email;
        private int age;
  }

 

예제 1. Person 유형의 객체를 로컬 파일에 저장한다. 직렬화 중에 바이트 스트림으로 변환되어 파일에 저장된다.

@Test 
public void whenSerializingAndDeserializing_ThenObjectIsTheSame() () 
  throws IOException, ClassNotFoundException { 
    User user = new User("김회원", "user@email.com", 25);
    // 데이터를 저장하기 위한 파일 경로
    FileOutputStream fileOutputStream
      = new FileOutputStream("yourfile.txt");
   
   // user 개체를 파일로 직렬화
   // User 개체 의 상태를 "user.txt" 파일 에 저장하기 위해 ObjectOutputStream을 사용
    ObjectOutputStream objectOutputStream 
      = new ObjectOutputStream(fileOutputStream);
    objectOutputStream.writeObject(person);
    objectOutputStream.flush();
    objectOutputStream.close();
}

 

예제 2. 객체를 직렬화하여 바이트 배열(byte []) 형태로 변환한다.

  	User user = new User("김회원", "user@email.com", 25);
    byte[] serializedMember;
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(user);
            // serializedMember -> 직렬화된 user 객체 
            serializedMember = baos.toByteArray();
        }
    }
    // 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
    System.out.println(Base64.getEncoder().encodeToString(serializedMember));
}

 

 

(3) Serial Version UID 

직렬화 가능한 각 클래스와 연결하는데 사용한다

저장된 바이트 스트림(역직렬화)을 읽어 해석할 경우 읽은 바이트 스트림이 해당 객체와 동일한 속성을 가지며 따라서 직렬화에 호환되는지 검사한다. serialVersionUID 다를경우 java.io.InvalidClassException 을 발생한다. 


직렬화 가능 클래스에 serialVersionUID가 없으면 JVM 이 런타임에 자동으로 생성한다. 하지만 컴파일러에 따라 생성된 클래스가 다르므로 예기치 않은 InvalidClassExceptions가  발생할 수 있어서 각 클래스에서 serialVersionUID를 선언하는 것이 좋다. (더 알아보기)

 

(4) 직렬화 사용처

ⓛ Hibernate, JPA

② RMI( Remote Method Invocation, 원격 메소드 호출 )

③ EJB( Enterprise JavaBeans; EJB ) 

④ JMS( Java Message Service ) 등

 

★ Java 직렬화 장점

- 간단하면서도 확장 가능하다.

- 객체 유형과 속성을 직렬화된 형태로 유지한다.

- 원격 개체에 필요한 마샬링 및 역마샬링을 지원하도록 확장 가능하다.

- Java 에서 기본으로 제공하므로 외부 라이브러리가 필요하지 않다.


3. 역직렬화 

 

(1) 역직렬화 조건

① 직렬화 대상이 된 객체의 클래스가 클래스 패스에 존재해야 하며 import 되어 있어야 한다.

* 중요한 점은 직렬화와 역직렬화를 진행하는 시스템이 서로 다를 수 있다는 것을 반드시 고려해야 한다.

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

 

(2) 역직렬화 구현 

 

역직렬화를 수행하기 위해 ObjectInputStream 클래스의 readObject() 메서드를 사용한다. readObject() 메서드는 바이트 스트림을 읽고 이를 다시 Java 객체로 변환한 다음 원래 개체로 다시 변환한다.

public final Object readObject() 
  throws IOException, ClassNotFoundException;

 

 예제 1. user 개체를 파일로 직렬화 한 값을 다시 읽어들인다.

FileInputStream fileInputStream 
  = new FileInputStream("yourfile2.txt");
ObjectInputStream objectInputStream 
  = new ObjectInputStream(fileInputStream);
Employee e2 = (Employee) objectInputStream.readObject();
objectInputStream.close();

 

 

 예제 2. user 개체를 직렬화한 바이트 배열(byte []) 값을 역직렬화한다.

    // 직렬화 예제에서 생성된 base64 데이터 
    String base64Member = "...생략";
    byte[] serializedMember = Base64.getDecoder().decode(base64Member);
    try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedMember)) {
        try (ObjectInputStream ois = new ObjectInputStream(bais)) {
            // 역직렬화된 Member 객체를 읽어온다.
            Object objectMember = ois.readObject();
            Member member = (Member) objectMember;
            System.out.println(member);
        }
    }

4. 직렬화  유효성 검증 

 

(1)  try

해당 객체가 java.io.Serialized 또는 java.io.Externalized 의 인스턴스인지 확인한다. 가장 간단하지만 객체 직렬화를 보장하지 않는다.

@Test(expected = NotSerializableException.class)
public void whenSerializing_ThenThrowsError() throws IOException {
    Address address = new Address();
    address.setHouseNumber(10);
    FileOutputStream fileOutputStream = new FileOutputStream("yofile.txt");
    try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
        objectOutputStream.writeObject(address);
    }
}

 

(2) Spring core core Serialization Utils

Spring core SerializationUtils  serialize () 메서드를 사용한다. 이 메소드는 직렬화 가능하지 않은 객체를 허용하지 않는다.

- 유형 변환을 통해 직렬화할 수 없는 Address 객체를 직렬화하려고 하면 런타임 시 ClassCastException 이 발생한다. 

Address address = new Address();
address.setHouseNumber(10);
org.springframework.util.SerializationUtils.serialize((Serializable) address);

 

*유사한 방식으로는 Apache Commons SerializationUtils 이 있다. 

 

(3) 사용자 정의 Serialization  Utils

public static boolean isSerializable(Class<?> it) {
    boolean serializable = it.isPrimitive() || it.isInterface() || Serializable.class.isAssignableFrom(it);
    if (!serializable) {
        return false;
    }
    Field[] declaredFields = it.getDeclaredFields();
    for (Field field : declaredFields) {
        if (Modifier.isVolatile(field.getModifiers()) || Modifier.isTransient(field.getModifiers()) || 
          Modifier.isStatic(field.getModifiers())) {
            continue;
        }
        Class<?> fieldType = field.getType();
        if (!isSerializable(fieldType)) {
            return false;
        }
    }
    return true;
}

 

 

 

 

 

 

참고

https://www.baeldung.com/java-serial-version-uid

https://www.baeldung.com/java-serialization-approaches

https://www.baeldung.com/java-serialization

https://www.baeldung.com/java-validate-serializable