[Java] 직렬화와 역직렬화

DEINGVELOP·2023년 3월 17일
0

Serialization

Java 객체의 직렬화

: 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트(byte) 형태로 데이터 변환하는 기술

  • Java에서 입출력은 스트림이라는 데이터 통로를 통해 이동한다. 다만 객체는 바이트형이 아니라, 스트림을 통해 파일에 저장하거나 네트워크에 전송할 수 없다.

  • 따라서 객체를 스트림을 통해 입출력하려면, 바이트 배열로 변환하는 것이 필요하다. 이를 직렬화라고 한다.

  • 시스템 적으로는 JVM(Java Virtual Machine)의 메모리에 상주(heap 또는 stack) 되어 있는 객체 데이터를 바이트 형태로 변환하는 기술과 직렬화된 바이트 형태의 데이터를 객체로 변환해서 JVM으로 상주시키는 형태를 말함.

Deserialization (역직렬화)

  • 반대로 스트림을 통해 받은 직렬화된 객체를 원래 모양으로 만드는 과정을 역직렬화라고 한다.

📌 용어 '직렬화'
사실 직렬화라는 말은 Java에서만 쓰이는 것이 아니며, Computer Science 전반에서 범용적으로 쓰이는 말이다.


직렬화 적용하기

자바 직렬화를 하기 위해서 type이 primitive type(기본형 타입)이거나 java.io.Serializable 인터페이스를 상속 받아야 한다.

  • Serializable

    package java.io;
    
    public interface Serializable {}
    • 다른 특별한 기능 없이 Serializable 인터페이스를 구현한 객체는 직렬화 가능하다는 걸 알 수 있는 용도로만 사용된다.
  • Serializable 인터페이스를 구현한 User Class 예시

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class User implements Serializable {
        private String name;
        private int age;
        private String email;
    
        @Override
        public String toString() {
            return String.format("User name: %s, age: %s, email: %s", name, age, email);
        }
    }
  • 위의 클래스 직렬화하기
    java.io.ByteArrayOutputStream, java.io.ObjectOutputStream을 이용하면 된다.

    @Slf4j
    public class SerializeTest {
        @Test
        @DisplayName("Serialize Test")
        public void SerializeTest() {
            User user = User.builder()
                    .name("Happykoo")
                    .age(30)
                    .email("rudals4549@gmail.com")
                    .build();
    
            String serializedUserBase64;
    
            try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                try(ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                    oos.writeObject(user);
                    //직렬화(byte array)
                    byte[] serializedUser = baos.toByteArray();
                    //byte array를 base64로 변환
                    serializedUserBase64 = Base64.getEncoder().encodeToString(serializedUser);
                    log.debug("serializedUserBase64 >> {}", serializedUserBase64);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    • 결과
      ```java
      serializedUserBase64 >> rO0ABXNyABduZXQuaGFwcHlrb28ubW9kZWwuVXNlcqLhsk0TYPUEAgADSQADYWdlTAAFZW1haWx0ABJMamF2YS9sYW5nL1N0cmluZztMAARuYW1lcQB+AAF4cAAAAB50ABRydWRhbHM0NTQ5QGdtYWlsLmNvbXQACEhhcHB5a29
      ```

  • 다시 역직렬화하기
    java.io.ByteArrayInputStream, java.io.ObjectInputStream을 이용하면 된다.

    @Slf4j
    public class SerializeTest {
        ...
    
        @Test
        @DisplayName("Deserialize Test")
        public void DeserializeTest() {
            String serializedUserBase64 = "rO0ABXNyABduZXQuaGFwcHlrb28ubW9kZWwuVXNlcqLhsk0TYPUEAgADSQADYWdlTAAFZW1haWx0ABJMamF2YS9sYW5nL1N0cmluZztMAARuYW1lcQB+AAF4cAAAAB50ABRydWRhbHM0NTQ5QGdtYWlsLmNvbXQACEhhcHB5a29v";
            byte[] serializedUser = Base64.getDecoder().decode(serializedUserBase64);
    
            try(ByteArrayInputStream bais = new ByteArrayInputStream(serializedUser)) {
                try(ObjectInputStream ois = new ObjectInputStream(bais)) {
                    //역직렬화(byte array -> object)
                    Object objectUser = ois.readObject();
                    User user = (User) objectUser;
                    log.debug(user.toString());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    • 결과
      User name: Happykoo, age: 30, email: rudals4549@gmail.com

직렬화에서 제외시키기

transient 키워드를 사용하면, 직렬화하는 대상에서 제외시킬 수 있다.

public class User implements Serializable {
    private String name;
    private int age;
    //직렬화에서 제외
    private transient String email;

    ...
}

serialVersionUID를 설정해야 하는 이유

  • 객체를 자바 직렬화한 후 데이터(클래스) 구조를 변경하면, 클래스 버전이 맞지 않기 때문에 제대로 역직렬화되지 않음. 에러가 발생함!

  • serialVersionUID 가 다르다며 에러가 발생한다.

  • 즉, serialVersionUID는 직접 기술하지 않아도 내부적으로 추가되며, 클래스 구조 정보를 이용하여 생성된 해쉬값을 이용한다.

  • serialVersionUID 를 비교하여 다르다면, 역직렬화가 불가능하다. (클래스 구조가 변경되면 serialVersionUID 도 변경 됨)

  • 클래스 구조가 변경되었을 때, 역직렬화 시 에러를 발생시켜야하는 시스템이 아니라면, serialVersionUID를 개발자가 직접 관리할 수도 있다.

  • serialVersionUID 설정하기

    public class User implements Serializable {
    
        //serialVersionUID 추가
        private static final long serialVersionUID = 1L;
    
        private String name;
        private int age;
        private String email;
        ...
    }

    위와 같이 설정해두면, 클래스 버전이 바뀌었어도 제대로 역직렬화된다. (다만 새로 추가된 property의 값은 null로 세팅됨)

  • 역직렬화에 영향을 주는 클래스 구조 변경

    • 직렬화 후에 property를 추가/삭제/이름 변경 → 에러 (serialVersionUID 설정시에는 단순히 값이 삭제되거나 null로 세팅됨)
    • 직렬화 후 property 타입 변경 → serialVersionUID 했어도 에러 발생 (직렬화는 타입에 민감함)

따라서 자바 직렬화를 자주 변경되는 클래스의 객체에 사용하는 것을 지양해야 한다.

또한, 또한, 외부(DB, Cache, NoSQL 서버)에 장기간 저장될 정보에 자바 직렬화를 사용하는 것도 피해야 한다. 데이터가 저장되어 있는 동안 데이터 구조가 변경된다면, 그 직렬화된 데이터는 쓰레기(Garbage) 혹은 나중에 에러를 발생시킬 시한폭탄이 되어 버릴 가능성이 높기 때문이다.

0개의 댓글