Tiny Star

🪄Interview/✏️Study

[CS STUDY INTERVIEW] 5주차 - 직렬화(Serialization)

청크 2024. 3. 11. 23:11

CS 스터디 5주차

 

직렬화(Serialization)


먼저 직렬화란

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

 

변환을 통해 파일을 저장하거나 네트워크를 통해 전송할 수 있도록 하며

객체의 상태를 저장하고 다른 프로그램이나 시스템에서 객체를 복원하는데 사용된다.

 

이를 시스템적으로 접근해보면 JVM의 힙 또는 스택 메모리에 상주하고 있는 객체 데이터를

직렬화를 통해 바이트 형태로 변환하여 데이터베이스나 파일과 같은 외부 저장소에 저장해두고,

다른 컴퓨터에서 이 파일을 가져와서 역직렬화를 통해 객체로 변환해서 JVM 메모리에 적재하는 것으로 볼 수 있다.

 

직렬화를 사용하는 이유는 각자 PC의 OS마다 서로 다른 가상 메모리 주소 공간을 갖는데,

Reference Type의 데이터들은 인스턴스를 전달할 수 없다.

이런 문제를 해결하기 위해 주소값이 아닌 Byte 형태로 직렬화된 객체 데이터를 전달해야하는데

직렬화된 데이터들  모두 Primitive Type(기본형)이 되고, 이는 파일 저장이나 네트워크 전송 시

파싱이 가능한 유의미한 데이터가 된다.

 

즉, 전송 및 저장이 가능한 데이터로 만들어주는 것이 바로 직렬화(Serialization)다.

더보기

※ 바이트 스트림이란?

스트림은 클라이언트나 서버 간에 출발지-목적지로 입출력을 위한 데이터가 흐르는 통로이다.

바이트 스트림은 자바에서 데이터를 바이트 단위로 읽거나 쓰는 데 사용되는 스트림으로 데이터 입출력을 위해 스트림을 사용한다.

 

1) 입력 바이트 스트림(InputByteStream)

입력 바이트 스트림은 외부 소스에서 데이터를 읽어오는 데 사용된다.

파일, 네트워크 연결, 키보드 입력 등과 같은 소스에서 데이터를 읽어오는 작업에 사용하며

자바에서는 InputStream 클래스와 이를 상속받는 여러 하위 클래스들이 입력 바이트 스트림을 처리한다.

2) 출력 바이트 스트림 (OutputByteStream)

출력 바이트 스트림은 데이터를 외부 소스로 보내는 데 사용된다.

파일에 데이터를 쓰거나 네트워크를 통해 데이터를 보내는 작업에 사용하며

자바에서는 OutputStream 클래스와 이를 상속받는 여러 하위 클래스들이 출력 바이트 스트림을 처리한다.


직렬화 조건

자바에서는  'java.io.Serializable' 인터페이스를 구현하는 클래스의 인스턴스만이 직렬화될 수 있다.

이 인터페이스는 직렬화 가능한 객체를 표시하기 위해 사용되며 Serializable 인터페이스는

메서드를 정의하지 않지만,  마커(marker) 인터페이스로서 자바 가상 머신(JVM)에게 해당 클래스의 인스턴스가 직렬화될 수 있음을 알려준다.

 

직렬화 대상은  java.io.Serializable 인터페이스를 상속 받은 객체나 Primitive 타입의 데이터다.

Reference 타입처럼 주소값을 지닌 객체들은 바이트로 변환하기 위해선 Serializable 인터페이스를 구현해야 한다.


직렬화의 장점

1) 객체 저장과 전송의 용이

직렬화를 통해 객체를 바이트 스트림으로 변환하여 파일에 저장하거 네트워크를 통해 전송이 되며

이를 통해 객체의 상태를 유지하면서 다른 프로그램이나 시스템으로 객체를 전달하거나 저장이 가능하다.

 

2) 데이터 구조의 보존

객체의 필드와 상태가 유지되므로 나중에 해당 객체를 복원할 때 원래의 상태를 정확하게 재현할 수 있어

객체의 데이터 구조를 보존할 수 있다.

 

3) 네트워크 통신의 효율성

 객체를 바이트 스트림으로 변환하면 더 작은 데이터 크기로 전송할 수 있으며, 

이는 대역폭을 절약하고 통신 성능을 향상시켜 네트워크를 통해 전송할 때 효율적이다.

 

4) 캐시와 저장소 활용

직렬화된 객체는 캐시에 저장하거나 데이터베이스에 저장하는데, 공간을 효율적으로 활용할 수 있다.

 

5) 다양한 환경에서 호환성

예를 들어, 서로 다른 플랫폼 또는 언어에서 직렬화된 데이터를 읽을 수 있다.
이는 시스템 간의 상호 운용성을 향상시키고 다양한 애플리케이션 간의 통합을 가능하게 하여
다양한 환경에서 객체를 교환할 수 있다.


직렬화 사용방법

직렬화를 사용하는 방법은 프로그래밍 언어나 개발 환경에 따라 달라질 수 있지만

일반적으로 거치는 단계들이 있다.

 

1. 직렬화 할 대상을 결정

직렬화 할 데이터나 객체를 결정하고 이 데이터나 객체는 직렬화된 형태로 변환되어 저장되거나 전송된다.

 

2. 직렬화 라이브러리 선택: 대부분의 프로그래밍 언어에는 직렬화를 수행하는 내장 또는 외부 라이브러리가 있는데,

이러한 라이브러리를 사용하여 데이터나 객체를 직렬화하고 다시 역직렬화가 가능하다.


3. 데이터 구조와 객체를 직렬화: 선택한 라이브러리를 사용하여 데이터 구조나 객체를 직렬화한다.

이는 주로 해당 라이브러리의 함수나 메서드를 호출하여 수행되고,

직렬화된 데이터는 일반적으로 파일에 쓰여질 수 있거나 네트워크를 통해 전송될 수 있다.


4. 필요에 따라 역직렬화: 저장된 직렬화된 데이터를 다시 읽거나 전송받은 데이터를 역직렬화하여 원래의 데이터나 객체로 복원한다.

이 단계는 데이터를 사용할 때 필요한 경우에만 수행되는 단계이다.

 

Java에서 직렬화를 사용하는 방법은  Serializable 인터페이스를 구현해야한다.

import java.io.Serializable;

public class MyClass implements Serializable {
    // 클래스 멤버 및 메서드 정의
}

이 인터페이스의 경우는 메서드나 필드를 포함하지 않고 단순 표시용으로 사용한다.

 

그 다음 객체를 직렬화하려면 ObjectOutputStream 클래스를 사용하여 객체를 파일에 쓰고, 

ObjectInputStream 클래스를 사용하여 파일에서 객체를 읽는다.

import java.io.*;

public class Main {
    public static void main(String[] args) {
        // 객체 직렬화
        try {
            MyClass obj = new MyClass();
            FileOutputStream fileOut = new FileOutputStream("object.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(obj);
            out.close();
            fileOut.close();
            System.out.println("객체가 직렬화되어 object.ser 파일에 저장되었습니다.");
        } catch(IOException i) {
            i.printStackTrace();
        }
        
        // 객체 역직렬화
        try {
            FileInputStream fileIn = new FileInputStream("object.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            MyClass obj = (MyClass) in.readObject();
            in.close();
            fileIn.close();
            System.out.println("object.ser 파일에서 객체를 역직렬화했습니다.");
        } catch(IOException | ClassNotFoundException i) {
            i.printStackTrace();
        }
    }
}

직렬화를 할 때는 보안상의 이유로 신뢰할 수 없는 데이터에 사용하지 않는 것이 좋으며

또한 직렬화된 데이터는 외부로 노출될 수 있으므로, 중요한 데이터의 경우 다른 방법을 고려해야 한다는 유의점도 존재한다.


역직렬화

역직렬화(Deserialization)는 직렬화된 데이터를 다시 객체나 데이터 구조로 변환하는 과정으로

즉, 직렬화된 데이터를 읽어서 원래의 객체나 데이터 구조를 복원하는 작업을 의미한다.

자바에서 역직렬화를 수행하는 방법은 직렬화와 유사한데,

역직렬화를 위해 ObjectInputStream 클래스를 사용하여 직렬화된 데이터를 읽고, 이를 원래의 객체로 변환하게 된다.

import java.io.*;

public class Main {
    public static void main(String[] args) {
        try {
            // 객체 역직렬화
            FileInputStream fileIn = new FileInputStream("object.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            MyClass obj = (MyClass) in.readObject(); // 역직렬화하여 MyClass 객체로 변환
            in.close();
            fileIn.close();
            System.out.println("object.ser 파일에서 객체를 역직렬화했습니다.");
            
            // 역직렬화된 객체 사용
            System.out.println("역직렬화된 객체의 정보: " + obj.toString());
        } catch(IOException | ClassNotFoundException i) {
            i.printStackTrace();
        }
    }
}

 

역직렬화를 수행할 때 직렬화된 클래스가 수정되었을 경우, 버전 호환성 문제가 발생할 수 있으니

이를 방지하기 위해서는 직렬화된 클래스에 serialVersionUID를 명시적으로 선언하고, 클래스 버전을 관리하는 것이 좋다.

private static final long serialVersionUID = 1L;

직렬화 요소 제외

보안상이나 직렬화의 목적에 맞지 않는 필드를 제외할 때 유용한 직렬화 요소 제외다.

클래스에 transient 키워드를 사용하여 특정 필드를 직렬화에서 제외할 수 있다.

패스워드나 보안 정보와 같이 민감한 정보는 직렬화되지 않도록 제외하는 것이 바람직한데,

이를 위해 해당 필드에 transient 키워드를 사용하여 직렬화에서 제외하는 것이다.

 

예를 들어,

private String sensitiveData; // 직렬화되어야 하는 민감한 정보
private transient String transientData; // 직렬화에서 제외되는 필드

 

두 가지의 필드가 있다면 transient 키워드가 사용된 transientData 필드는 직렬화되지 않으며, 

역직렬화 시에도 기본값으로 초기화된다.

이렇게 함으로써 보안 상의 이슈를 방지하거나, 직렬화의 목적에 맞지 않는 데이터를 제외할 수 있다.


커스텀 직렬화

커스텀 직렬화는 자바에서 직렬화나 역직렬화를 개발자가 직접 제어하고자 할 때 사용할 수 있다.

기본적인 직렬화 방식이나 매커니즘을 사용하기에는 제한이 있거나 또는 원하는 방식으로 객체이 직렬화를 수행하고자 할 때 유용하다.

 

커스텀 직렬화의 방법은 직렬화를 제어하기 위한 두 가지 메서드를 정의하는데

writeObject()와 readObject() 메서드이며, 이 메서드를 사용할 때는 반드시 private 접근 제어자를 사용해야 한다.

 

또한 이 메서드는 반드시 IOException과 ClassNotFoundException 예외를 던져야하는데

그 이유는 이렇다.

 

1) IOException

예를 들어, 파일 입출력 중에 파일이 존재하지 않거나 읽기/쓰기 권한이 없는 경우 등이 있다.

즉, 객체를 직렬화하거나 역직렬화하는 과정에서 입출력 오류가 발생할 수 있기 때문에 이러한 상황에 대비하여 IOException을 처리해야 한다.

2) ClassNotFoundException

역직렬화 시에는 직렬화된 데이터의 클래스를 찾지 못할 수 있다.

이는 직렬화된 데이터가 해당 클래스의 버전과 호환되지 않거나, 클래스가 존재하지 않는 등의 이유로 발생할 수 있는데,

이러한 경우에는 직렬화된 데이터를 읽을 수 없으므로 ClassNotFoundException을 던져야 한다.

 

결국 두 메서드에서 일어날 수 있는 예외를 처리하여 코드의 안정성을 높이고 예상치 못한 오류에 대응을 하는 것이다.

 

커스텀 직렬화는 아래 예제처럼 사용할 수 있다.

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;

public class CustomSerializationExample implements Serializable {
    private transient Date date; // 직렬화에서 제외될 필드

    public CustomSerializationExample() {
        this.date = new Date();
    }

    // 직렬화 메서드: 객체를 직렬화하는 메서드
    private void writeObject(ObjectOutputStream out) throws IOException {
        // SimpleDateFormat을 사용하여 날짜 데이터를 형식화하여 문자열로 변환
        String formattedDate = new SimpleDateFormat("yyyy-MM-dd").format(date);
        out.writeObject(formattedDate); // 형식화된 날짜 데이터를 직렬화
    }

    // 역직렬화 메서드: 객체를 역직렬화하는 메서드
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 직렬화된 날짜 데이터를 읽고, SimpleDateFormat을 사용하여 파싱하여 Date 객체로 변환
        String formattedDate = (String) in.readObject();
        date = new SimpleDateFormat("yyyy-MM-dd").parse(formattedDate); // 문자열을 Date로 파싱
    }

    // 객체의 정보를 출력하는 메서드
    public void printDate() {
        System.out.println("Date: " + date);
    }

    public static void main(String[] args) {
        try {
            // 객체 직렬화
            CustomSerializationExample example = new CustomSerializationExample();
            FileOutputStream fileOut = new FileOutputStream("example.ser");
            ObjectOutputStream out = new ObjectOutputStream(fileOut);
            out.writeObject(example);
            out.close();
            fileOut.close();
            System.out.println("객체가 직렬화되어 example.ser 파일에 저장되었습니다.");

            // 객체 역직렬화
            FileInputStream fileIn = new FileInputStream("example.ser");
            ObjectInputStream in = new ObjectInputStream(fileIn);
            CustomSerializationExample restoredExample = (CustomSerializationExample) in.readObject();
            in.close();
            fileIn.close();
            System.out.println("example.ser 파일에서 객체를 역직렬화했습니다.");

            // 역직렬화된 객체의 날짜 정보 출력
            restoredExample.printDate();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

이 예제에서는 Date 객체를 직렬화할 때 transient 키워드를 사용하여 직렬화에서 제외한다.

대신에, 직렬화 메서드인 writeObject()에서는 SimpleDateFormat을 사용하여 날짜를 형식화하고

역직렬화 메서드인 readObject()에서는 이 형식화된 문자열을 다시 파싱하여 Date 객체로 설정하는데,

이렇게 하면 직렬화된 데이터를 읽을 때 날짜를 원하는 형식으로 관리할 수 있다.


직렬화 버전

자바 직렬화는 serialVersionUID라는 것을 사용하여 직렬화 버전을 명시적으로 관리한다.

이 값은 직렬화된 클래스의 버전을 식별하기 위해 사용되며, 역직렬화 시에 직렬화된 클래스의 버전과 

현재 클래스의 버전을 비교하여 버전 충돌을 방지할 수 있다.

 

serialVersionUID는 long 타입의 정적 상수로 선언되어야 하는데, 자동 생성되지 않으므로 개발자가 명시적으로 값을 지정해주어야 하

만약 serialVersionUID가 명시적으로 지정되지 않으면 컴파일러가 자동으로 생성한 값이 사용될 수 있다.

 

ex) private static final long serialVersionUID = 123456789L;

 

이 UID의 값은 변경되지 않는 한 항상 동일해야하고, 클래스 구조가 변경되면 수동으로 업데이트가 되어야 한다.

 

만약 클래스의 구조가 변경되어 직렬화 버전을 업데이트해야 하는 경우, 

serialVersionUID의 값을 수동으로 변경하고, 변경된 클래스가 이전 버전과 호환되지 않을 수 있다는 것을 염두에 두고

이전 버전과의 호환성을 유지하려면 직렬화된 데이터를 역직렬화할 때 적절한 대응이 필요하다.


직렬화 예외

직렬화를 수행 할 때 다양한 예외가 발생할 수 있다.

 

1) NotSerializableException

직렬화하려는 클래스가 Serializable 인터페이스를 구현하지 않은 경우에 발생하고

이 예외는 직렬화를 시도하는 클래스가 Serializable을 구현하지 않아 직렬화할 수 없다는 것을 나타낸다.

2) InvalidClassException

직렬화된 객체를 역직렬화하려는 동안, 클래스의 시그니처가 변경된 경우에 발생할 수 있다.

이는 클래스의 구조가 변경되었을 때 또는 클래스의 serialVersionUID 값이 변경되었을 때 발생한다.

3) IOException

파일 또는 네트워크와 관련된 입출력 오류가 발생할 때 IOException이 발생하는데

이는 파일을 열거나 쓸 때, 네트워크를 통해 데이터를 전송할 때 발생할 수 있는 예외이다.

4) ClassNotFoundException

역직렬화할 때 해당 클래스가 클래스패스에 없거나, 클래스의 이름이 변경되어 클래스를 찾을 수 없을 때 발생하는 예외이다.

5) InvalidObjectException

역직렬화된 객체의 유효성이 검증되지 않을 때 발생한다.

이는 역직렬화 과정 중에 객체의 유효성을 검사하고, 유효하지 않은 경우에 발생하는 예외이다.

6) StreamCorruptedException

이 예외는 데이터가 손상( 직렬화 데이터의 형식이 손상 )되었거나, 유효하지 않은 경우, 다른 형식으로 직렬화되었을 때 발생한다.


7) SecurityException

보안 관련 예외로, 직렬화 또는 역직렬화 작업이 보안 정책에 위배되는 경우, 보안 관련 설정이나 정책에 문제가 있는 경우 발생할 수 있다.


직렬화의 문제점

직렬화는 데이터나 객체를 외부 형태로 변환하여 저장하거나 전송하는 프로세스라고 했다.

매우 유용한 기능이지만 다양한 문제가 발생할 확률도 높다.

 

1) 보안 문제

직렬화된 데이터는 외부로 노출될 수 있는데, 중요한 정보가 포함된 객체를 직렬화할 때 보안 문제가 발생할 수 있다.

민감한 정보가 포함된 객체를 직렬화해야 하는 경우, 데이터를 암호화하거나 보안 프로토콜을 추가하여 보호해야한다.

2) 버전 관리 / 유지보수

클래스의 구조가 변경되면 직렬화된 데이터의 버전 관리가 어려울 수 있다.

만약 새로운 필드가 추가되거나 기존 필드가 수정될 때, 이전 버전과 호환성을 유지하기 위해 추가적인 처리가 필요하다.

3) 성능 문제

객체를 직렬화하고 역직렬화하는 과정은 추가적인 자원과 시간을 필요로 하는데,

특히 큰 객체나 복잡한 구조의 객체를 직렬화할 때 성능 문제가 발생할 수 있다.

4) 이식성 문제

서로 다른 환경이나 다른 플랫폼, 언어 간에 직렬화된 데이터를 주고받는 경우, 이식성 문제  (직렬화 및 역직렬화 프로세스가 호환)가 발생할 수 있다. 

5) 자원 누수

직렬화된 객체는 메모리 내부에도 존재하며, 이를 처리하지 않고 계속 유지하면 자원 누수가 발생할 수 있기 때문에

따라서 객체를 직렬화한 후에는 적절히 자원을 해제해주어야 한다.

이러한 문제점들을 고려하여 직렬화를 사용할 때는 신중하게 설계하고, 적절한 보안 및 성능 대책을 수립해야한다.


면접 예상 질문과 답변

Q1.직렬화(Serialization)란?
A1. 직렬화는객체나 데이터 구조를 바이트 스트림으로 변환하는 과정을 말합니다.

 

Q2. 직렬화의 주요 목적?

A2.주요 목적은 객체나 데이터를 파일에 저장하거나 네트워크를 통해 전송할 수 있도록 하여 영속성(Persistence)을 제공하고, 

분산 시스템에서 객체를 교환하는 것입니다.

 

Q3. 직렬화와 역직렬화 설명

A3.  직렬화는 객체를 바이트 스트림으로 변환하는 과정이며, 역직렬화는 바이트 스트림에서 객체를 다시 만드는 과정입니다.

 

Q4. 어떤 경우에 직렬화를 사용해야하는가?

A4. 주로 객체나 데이터를 파일에 저장하거나 네트워크를 통해 전송해야 할 때 사용합니다.

 

Q5.  어떤 데이터 유형을 직렬화할 수 있는가?

A5.  대부분의 객체와 기본 데이터 유형을 직렬화할 수 있습니다. 하지만 직렬화가 가능한 객체는 java.io.Serializable 인터페이스를 구현해야 합니다.

 

Q6.  직렬화된 데이터를 보호하기 위한 방법 ?

A6. 데이터 암호화, 서명 및 검증을 통한 데이터 무결성 보호 등의 방법을 사용할 수 있습니다.

 

Q7.  직렬화의 장점과 단점

A7. 데이터를 영속화하고 네트워크를 통해 전송할 수 있다는 장점이 있지만 보안 문제, 버전관리의 어려움, 직렬화나 역직렬화에

소요되는 자원 등의 단점이 존재합니다.

 

Q8. 직렬화된 데이터를 읽거나 쓰는 과정에서 발생할 수 있는 보안 문제는 무엇이 있는가?

A8.  악의적인 데이터 변조, 악성 코드 삽입, 중간자 공격 등의 보안 문제가 발생할 수 있습니다.

 

Q9.직렬화된 객체의 버전 관리는 왜 중요한가?

A9.객체의 구조가 변경될 때 이전 버전과 호환성을 유지하기 위해 중요합니다. 

새로운 필드 추가, 기존 필드 삭제 등의 변경 사항이 있는 경우 버전 관리를 통해 호환성을 유지할 수 있습니다.