[Kotlin] 직렬화 및 역직렬화

송규빈·2022년 9월 20일
1

직렬화란(Serialization)?

Serializable 어디서 많이 봤을 것이다. Retrofit을 사용할 때 모델 클래스에 @Serializable 이라는 어노테이션을 붙여서 사용하는데 그렇다면 이게 왜 필요할까?

직렬화는 객체를 전송 가능한 형태로 만들어주는 것이다.
즉 객체 데이터를 연속적인 데이터로 변형해서 데이터를 읽을 수 있도록 하는 것이다.

역직렬화란(Deserialization)?

이름에서부터 감은 왔을텐데 말 그대로 직렬화된 파일의 스트림 데이터를 읽어서 원래 객체의 형태로 복원하는 과정이다.

Serializable

직렬화를 하기 위해서는 직렬화가 가능한 클래스를 만들어야 한다.

직렬화가 가능한 클래스를 만드는 것은 간단하다. 위처럼 Serializable 인터페이스를 implement하면 된다.

이를 본다면 궁금증이 생길 것이 있는데 그렇다면 따로 구현해줘야할 것은 없을까?
해당 인터페이스는 메서드나 필드가 따로 존재하지 않고, 단순히 직렬화 가능의 여부를 식별하는데 쓰인다고 한다.
직렬화가 이뤄지는 것은 내부적으로 따로 구현이 되어있다고 한다.

궁금해서 Serializable 인터페이스를 탐색해봤지만 역시 별다른 필드나 메서드들이 존재하지 않았다.

예제

Serializable class

우선 Serializable한 클래스를 만든다.

data class SerializablePeople(val name: String, val age: Int, val company: String) : Serializable

그 후에 직렬화를 시켜본다.

// 직렬화
fun serialize() {
    try {
        val fos = FileOutputStream("people.ser")
        val bos = BufferedOutputStream(fos)
        val oos = ObjectOutputStream(bos)

        val serializablePeople = SerializablePeople(name = "송구글", age = 29, company = "구글")
        oos.writeObject(serializablePeople)
        oos.close()
        System.out.println("직렬화 완료")
    } catch (e: java.lang.Exception) {
        e.printStackTrace()
    }
}

그리고 직렬화가 잘 되었는지 확인을 위해서 역직렬화도 해본다.

// 역직렬화
fun deserialize() {
    try {
        val fis = FileInputStream("people.ser")
        val bis = BufferedInputStream(fis)
        val oin = ObjectInputStream(bis)

        val serializablePeople = oin.readObject()
        System.out.println(serializablePeople.toString())

        oin.close();
    } catch (e: Exception) {
        // TODO Auto-generated catch block
        e.printStackTrace()
    }

}

그리고 이를 실행시켜보면

 fun main() {
    serialize()
    deserialize()
}

이렇게 데이터가 잘 나오는 것을 볼 수 있다.

Not Serializable class

그렇다면 Serializable을 implements 하지 않았을 때는 어떻게 될까?

data class NormalPeople(val name: String, val age: Int, val company:String)

위에 코드에서 SerializablePeople 대신 NormalPeople로 교체해서 실행시켜보면

이와 같이 NotSerializableException을 경험할 수 있다.

Transient

Transient는 직렬화 대상에서 제외해야할 property가 있을 때 해당 어노테이션을 붙여서 선언하게 된다면 직렬화를 할 때 대상에서 제외가 된다.

자바에서는 키워드로 선언하지만 코틀린에서는 어노테이션만 추가해주면 된다.

SerializablePeople에서 회사명은 제외하고 직렬화를 시키고 싶을 때

이렇게 company 앞에 어노테이션을 추가해준다.

이 상태에서 직렬화를 해보면

똑같이 송구글/29/구글로 객체를 생성했지만 @Transient가 붙은 company는 null로 나오는 것을 볼 수 있다.
그렇기에 해당 프로퍼티는 항상 default값을 설정해두는 것이 좋다.

data class SerializablePeople(val name: String, val age: Int, @Transient val company: String = "비공개") : Serializable

다른 객체 프로퍼티

개발하다보면 다른 객체를 멤버로 가져야할 때가 있다. 예를 들면 집의 위치와 집주인의 정보를 갖고있는 Home클래스를 직렬화 해야한다고 하자.

data class Home(val location: String, val master: People) : Serializable

이럴 때는 People도 꼭 Serializable한 클래스여야만 한다.
즉, 다른 여러 객체 프로퍼티를 갖고있는데 단 하나라도 Serializable하지 않으면 NotSerializableException이 발생한다.

SerialVersionUID

직렬화하는 과정에 있어서 내부적으로 SerialVersionUID라는 고유 번호를 생성하여 버전이 관리된다.
이 SerialVersionUID라는 값을 확인하여 맞는지 여부에 따라 직렬화와 역직렬화가 처리된다.
serialVersionUID가 선언되어 있지 않으면 클래스의 기본 해쉬값을 사용한다.

serialVersionUID가 맞지 않을 시 예외를 발생시킨다.

또한 serialVersionUID가 맞다면 멤버를 추가해도 default값이 null로 들어가며 역직렬화하는데 문제가 없다. (단 기존에 있던 멤버의 자료형을 바꿀 시에는 예외가 발생한다.)

그래서 serialVersionUID는 개발자가 직접 관리하는 게 낫다고 한다.
serialVersionUID에 대해 좀 더 자세한 내용은 여기를 참고하기 바란다.

profile
🚀 상상을 좋아하는 개발자

0개의 댓글