본문 바로가기
이직&취업/Java 기초 상식

JAVA 직렬화(Serialization) 란 무엇인가?

by journeylabs 2025. 3. 29.
728x90
반응형

목차

    JAVA직렬화(Serialization) 

    JAVA직렬화는 객체를 바이트 스트림으로 변환하여 파일에 저장하거나 네트워크를 통해 전송할 수 있도록 하는 강력한 메커니즘입니다. 이 글에서는 자바 직렬화의 기본 개념부터 고급 활용, 주의사항까지 상세하게 다룹니다.

     

    1. 용어 및 주요 개념

    1.1. 직렬화 (Serialization)

    • 정의: 직렬화는 객체의 데이터 (필드 값)를 연속적인 바이트 스트림으로 변환하는 프로세스입니다. 이 바이트 스트림은 메모리, 파일, 네트워크 연결 등 다양한 저장 매체에 저장하거나 전송할 수 있습니다.
    • 핵심: 객체의 상태를 "캡처"하여 저장하거나 다른 환경으로 전송할 수 있도록 하는 것이 핵심입니다. 객체의 클래스 정보, 필드 값, 심지어 다른 객체에 대한 참조까지 바이트 스트림에 포함됩니다.
    • 역할:
      • 객체 영속성 (Object Persistence): 객체의 상태를 파일에 저장하여 프로그램을 종료했다가 다시 실행해도 객체를 복원할 수 있도록 합니다.
      • 원격 통신: 객체를 네트워크를 통해 다른 시스템으로 전송하여 분산 시스템 간의 데이터 교환을 가능하게 합니다.
      • 데이터 캐싱: 객체를 직렬화하여 캐시에 저장하고 필요할 때 빠르게 복원하여 애플리케이션의 성능을 향상시킵니다.

    1.2. 역직렬화 (Deserialization)

    • 정의: 역직렬화는 직렬화된 바이트 스트림을 읽어 원래의 객체와 동일한 상태의 객체를 생성하는 프로세스입니다.
    • 핵심: 직렬화된 데이터를 기반으로 새로운 객체 인스턴스를 생성하고, 바이트 스트림에 저장된 필드 값을 해당 객체의 필드에 할당하는 것이 핵심입니다.
    • 역할:
      • 객체 복원: 직렬화된 파일 또는 네트워크 스트림에서 객체를 다시 생성하여 애플리케이션에서 사용할 수 있도록 합니다.
      • 상태 복구: 저장된 객체의 상태를 복원하여 이전 상태로 애플리케이션을 되돌릴 수 있습니다.

    1.3. 직렬화 가능 (Serializable)

    • 정의: java.io.Serializable 인터페이스는 직렬화 가능함을 나타내는 마커 인터페이스입니다. 직렬화하려는 클래스는 반드시 이 인터페이스를 구현해야 합니다.
    • 핵심: Serializable 인터페이스는 추상 메서드를 포함하지 않습니다. 단순히 JVM에게 해당 클래스의 객체를 직렬화할 수 있음을 알리는 역할을 합니다.
    • Serializable 구현의 의미: JVM은 Serializable 인터페이스를 구현한 클래스의 객체를 직렬화할 때 특별한 처리를 수행합니다. 객체의 필드를 바이트 스트림으로 변환하고, 클래스 정보와 함께 저장합니다.
    • Serializable 미구현 시: Serializable 인터페이스를 구현하지 않은 클래스의 객체를 직렬화하려고 하면 java.io.NotSerializableException 예외가 발생합니다.

    1.4. transient

    • 정의: transient 키워드는 클래스의 필드에 적용하여 해당 필드를 직렬화 과정에서 제외하도록 지정합니다.
    • 핵심: 민감한 정보 (예: 비밀번호, 보안 키) 또는 직렬화할 필요가 없는 데이터 (예: 스레드, 소켓 연결)를 직렬화 대상에서 제외하여 보안 및 성능상의 이점을 얻을 수 있습니다.
    • 역할:
      • 보안 강화: 중요한 데이터를 직렬화하지 않아 데이터 유출 위험을 줄입니다.
      • 성능 향상: 불필요한 데이터를 직렬화하지 않아 직렬화/역직렬화 시간을 단축합니다.
      • 객체 일관성 유지: 직렬화할 수 없는 객체 (예: 스레드)를 참조하는 필드를 transient로 선언하여 NotSerializableException을 방지합니다.
    • 주의 사항: transient 필드는 역직렬화 후 기본값 (예: null, 0, false)으로 초기화됩니다. 필요한 경우 writeObject  readObject 메서드를 사용하여 transient 필드를 직접 처리해야 합니다.

    1.5. serialVersionUID

    • 정의: serialVersionUID Serializable 인터페이스를 구현한 클래스에 정의되는 static final long 타입의 필드입니다. 클래스의 버전을 식별하는 데 사용됩니다.
    • 핵심: 직렬화된 객체를 역직렬화할 때 클래스의 serialVersionUID와 직렬화된 데이터에 저장된 serialVersionUID를 비교하여 클래스의 호환성을 검증합니다.
    • 역할:
      • 클래스 호환성 검증: 클래스의 구조가 변경되었을 때 역직렬화 과정에서 InvalidClassException이 발생하는 것을 방지합니다.
      • 버전 관리: 클래스의 구조가 변경될 때 serialVersionUID를 변경하여 이전 버전과의 호환성을 끊을 수 있습니다.
    • serialVersionUID 미지정 시: 클래스에 serialVersionUID를 명시적으로 지정하지 않으면 JVM이 자동으로 serialVersionUID를 생성합니다. 하지만 클래스 구조가 변경될 경우 자동으로 생성되는 serialVersionUID가 변경될 수 있으므로, 명시적으로 지정하는 것이 좋습니다.
    • 권장 사항: serialVersionUID를 명시적으로 선언하고 관리하는 것이 좋습니다. serialVersionUID는 클래스 구조가 변경될 때마다 신중하게 업데이트해야 합니다.

    1.6. writeObject() 및 readObject()

    • 정의: writeObject() 메서드는 객체를 직렬화할 때 호출되는 메서드이며, readObject() 메서드는 객체를 역직렬화할 때 호출되는 메서드입니다.
    • 핵심: writeObject()  readObject() 메서드를 구현하여 직렬화 및 역직렬화 과정을 커스터마이징할 수 있습니다. 예를 들어, 특정 필드를 암호화하거나, 직렬화/역직렬화 로직을 직접 제어할 수 있습니다.
    • 역할:
      • 직렬화/역직렬화 로직 제어: 기본 직렬화/역직렬화 방식 외에 추가적인 작업을 수행할 수 있습니다.
      • 보안 강화: 민감한 데이터를 암호화하거나, 데이터 유효성 검사를 수행할 수 있습니다.
      • transient 필드 처리: transient 필드를 직렬화/역직렬화하는 데 사용될 수 있습니다.
    • 구현 방법:
      • writeObject() 메서드는 private void writeObject(ObjectOutputStream out) throws IOException 형태로 정의해야 합니다.
      • readObject() 메서드는 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException 형태로 정의해야 합니다.
      • writeObject()  readObject() 메서드 내에서는 out.defaultWriteObject()  in.defaultReadObject() 메서드를 호출하여 기본 직렬화/역직렬화를 수행해야 합니다.

    1.7. ObjectOutputStream 및 ObjectInputStream

    • 정의: ObjectOutputStream 클래스는 객체를 바이트 스트림으로 직렬화하는 데 사용되는 클래스이며, ObjectInputStream 클래스는 바이트 스트림을 객체로 역직렬화하는 데 사용되는 클래스입니다.
    • 핵심: ObjectOutputStream ObjectInputStream은 각각 OutputStream InputStream을 확장한 클래스이며, 객체 단위로 데이터를 읽고 쓰는 기능을 제공합니다.
    • 역할:
      • 직렬화 수행: ObjectOutputStream.writeObject(Object obj) 메서드를 사용하여 객체를 바이트 스트림으로 변환합니다.
      • 역직렬화 수행: ObjectInputStream.readObject() 메서드를 사용하여 바이트 스트림을 객체로 복원합니다.
    • 사용 방법:
      • ObjectOutputStream은 파일 또는 네트워크 연결과 연결된 OutputStream을 기반으로 생성됩니다.
      • ObjectInputStream은 파일 또는 네트워크 연결과 연결된 InputStream을 기반으로 생성됩니다.

     

    주요 특징

    • 객체 보존: 객체의 상태를 그대로 저장하고 복원할 수 있습니다.
    • 플랫폼 독립성: 직렬화된 바이트 스트림은 자바가 실행되는 모든 플랫폼에서 역직렬화할 수 있습니다.
    • 네트워크 전송: 객체를 네트워크를 통해 다른 시스템으로 전송할 수 있습니다.
    • 영속성 (Persistence): 객체를 파일에 저장하여 영구적으로 보관할 수 있습니다.

    2. 조건 및 상황, 상속 관계

    직렬화 조건

    • Serializable 인터페이스 구현: 직렬화하려는 클래스는 반드시 java.io.Serializable 인터페이스를 구현해야 합니다. 이 인터페이스는 추상 메서드를 포함하지 않는 마커 인터페이스(Marker Interface)입니다.
    • 직렬화 불가능한 필드: 직렬화할 수 없는 필드(예: 스레드, 소켓 등)는 transient 키워드를 사용하여 직렬화 대상에서 제외해야 합니다. 그렇지 않으면 NotSerializableException이 발생할 수 있습니다.
    • serialVersionUID: 클래스의 구조가 변경될 경우 역직렬화 시 InvalidClassException이 발생할 수 있습니다. 이를 방지하기 위해 serialVersionUID를 명시적으로 선언하고 관리하는 것이 좋습니다.

    직렬화 상황

    • 세션 관리: 웹 애플리케이션에서 세션 객체를 파일이나 데이터베이스에 저장하여 세션을 유지하는 데 사용됩니다.
    • 원격 메서드 호출 (RMI): RMI에서 객체를 네트워크를 통해 전송하는 데 사용됩니다.
    • 데이터 캐싱: 객체를 파일에 저장하여 캐시로 활용하여 애플리케이션의 성능을 향상시킬 수 있습니다.
    • 분산 시스템: 객체를 여러 시스템 간에 공유하고 교환하는 데 사용됩니다.

    상속 관계

    • 상위 클래스가 Serializable을 구현하는 경우: 하위 클래스는 자동으로 직렬화 가능하게 됩니다. 별도로 Serializable 인터페이스를 구현하지 않아도 됩니다.
    • 상위 클래스가 Serializable을 구현하지 않는 경우: 하위 클래스가 Serializable을 구현하더라도 상위 클래스의 필드는 직렬화되지 않습니다. 상위 클래스의 필드를 직렬화하려면 상위 클래스에서 기본 생성자를 제공하거나, 하위 클래스에서 writeObjectreadObject 메서드를 구현하여 상위 클래스의 필드를 직접 처리해야 합니다.

    3. 예제 및 예제 상세 설명

    예제 1: 기본적인 직렬화 및 역직렬화

    import java.io.*;
    
    class Person implements Serializable {
        private static final long serialVersionUID = 1L; // 명시적인 serialVersionUID
        private String name;
        private int age;
        private transient String password; // 직렬화에서 제외
    
        public Person(String name, int age, String password) {
            this.name = name;
            this.age = age;
            this.password = password;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", password='" + password + '\'' +
                    '}';
        }
    }
    
    public class SerializationExample {
        public static void main(String[] args) {
            // 직렬화
            Person person = new Person("홍길동", 30, "secret123");
            try (FileOutputStream fileOut = new FileOutputStream("person.ser");
                 ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
                out.writeObject(person);
                System.out.println("직렬화 완료");
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            // 역직렬화
            try (FileInputStream fileIn = new FileInputStream("person.ser");
                 ObjectInputStream in = new ObjectInputStream(fileIn)) {
                Person deserializedPerson = (Person) in.readObject();
                System.out.println("역직렬화 완료: " + deserializedPerson);
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    1. Person 클래스: Serializable 인터페이스를 구현하여 직렬화 가능하도록 선언합니다. serialVersionUID를 명시적으로 선언하여 클래스 버전 관리를 용이하게 합니다. password 필드는 transient로 선언하여 직렬화에서 제외합니다.
    2. 직렬화 과정: FileOutputStreamObjectOutputStream을 사용하여 Person 객체를 "person.ser" 파일에 직렬화합니다. writeObject() 메서드를 사용하여 객체를 바이트 스트림으로 변환하고 파일에 저장합니다.
    3. 역직렬화 과정: FileInputStreamObjectInputStream을 사용하여 "person.ser" 파일에서 바이트 스트림을 읽어와 Person 객체로 역직렬화합니다. readObject() 메서드를 사용하여 바이트 스트림을 객체로 복원합니다. transient로 선언된 password 필드는 역직렬화 후 기본값(null)으로 설정됩니다.

    예제 2: 상속 관계에서의 직렬화

    import java.io.*;
    
    class Animal {
        private String name;
    
        public Animal(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    
        @Override
        public String toString() {
            return "Animal{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    
    class Dog extends Animal implements Serializable {
        private static final long serialVersionUID = 2L;
        private String breed;
    
        public Dog(String name, String breed) {
            super(name);
            this.breed = breed;
        }
    
        public String getBreed() {
            return breed;
        }
    
        @Override
        public String toString() {
            return "Dog{" +
                    "name='" + getName() + '\'' +
                    ", breed='" + breed + '\'' +
                    '}';
        }
    }
    
    public class InheritanceSerializationExample {
        public static void main(String[] args) {
            // 직렬화
            Dog dog = new Dog("멍멍이", "푸들");
            try (FileOutputStream fileOut = new FileOutputStream("dog.ser");
                 ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
                out.writeObject(dog);
                System.out.println("직렬화 완료");
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            // 역직렬화
            try (FileInputStream fileIn = new FileInputStream("dog.ser");
                 ObjectInputStream in = new ObjectInputStream(fileIn)) {
                Dog deserializedDog = (Dog) in.readObject();
                System.out.println("역직렬화 완료: " + deserializedDog);
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    1. Animal 클래스: Serializable 인터페이스를 구현하지 않습니다.
    2. Dog 클래스: Animal 클래스를 상속받고 Serializable 인터페이스를 구현합니다. Dog 클래스의 인스턴스를 직렬화할 때, Animal 클래스의 name 필드는 직렬화되지 않습니다.
    3. 직렬화 및 역직렬화: Dog 객체를 직렬화하고 역직렬화합니다. 역직렬화된 Dog 객체의 name 필드는 기본값(null)으로 설정됩니다. 만약 Animal 클래스의 name 필드도 직렬화 하고 싶다면, Animal 클래스 또한 Serializable 인터페이스를 구현해야 합니다.

    예제 3: writeObject 및 readObject 메서드 사용

    import java.io.*;
    
    class CustomPerson implements Serializable {
        private static final long serialVersionUID = 3L;
        private String name;
        private int age;
        private transient String password;
    
        public CustomPerson(String name, int age, String password) {
            this.name = name;
            this.age = age;
            this.password = password;
        }
    
        private void writeObject(ObjectOutputStream out) throws IOException {
            // 기본 직렬화 수행
            out.defaultWriteObject();
            // password 필드 암호화 후 저장
            String encryptedPassword = encrypt(password);
            out.writeObject(encryptedPassword);
        }
    
        private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
            // 기본 역직렬화 수행
            in.defaultReadObject();
            // 암호화된 password 필드 복호화
            String encryptedPassword = (String) in.readObject();
            this.password = decrypt(encryptedPassword);
        }
    
        private String encrypt(String password) {
            // 암호화 로직 구현 (예시)
            return new StringBuilder(password).reverse().toString();
        }
    
        private String decrypt(String encryptedPassword) {
            // 복호화 로직 구현 (예시)
            return new StringBuilder(encryptedPassword).reverse().toString();
        }
    
        @Override
        public String toString() {
            return "CustomPerson{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", password='" + password + '\'' +
                    '}';
        }
    }
    
    public class CustomSerializationExample {
        public static void main(String[] args) {
            // 직렬화
            CustomPerson person = new CustomPerson("홍길동", 30, "secret123");
            try (FileOutputStream fileOut = new FileOutputStream("custom_person.ser");
                 ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
                out.writeObject(person);
                System.out.println("직렬화 완료");
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            // 역직렬화
            try (FileInputStream fileIn = new FileInputStream("custom_person.ser");
                 ObjectInputStream in = new ObjectInputStream(fileIn)) {
                CustomPerson deserializedPerson = (CustomPerson) in.readObject();
                System.out.println("역직렬화 완료: " + deserializedPerson);
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    1. CustomPerson 클래스: writeObjectreadObject 메서드를 구현하여 직렬화 및 역직렬화 과정을 커스터마이징합니다.
    2. writeObject 메서드: 기본 직렬화를 수행한 후 password 필드를 암호화하여 저장합니다.
    3. readObject 메서드: 기본 역직렬화를 수행한 후 암호화된 password 필드를 복호화합니다.
    4. 암호화 및 복호화 로직: 예시로 간단한 문자열 반전 로직을 사용했습니다. 실제로는 더 강력한 암호화 알고리즘을 사용해야 합니다.

    4. 주의사항

    • 보안 문제: 직렬화된 데이터는 쉽게 변조될 수 있으므로 민감한 정보는 암호화해야 합니다. 역직렬화 과정에서 객체가 생성되므로, 악의적인 바이트 스트림을 역직렬화하면 보안 취약점이 발생할 수 있습니다. (역직렬화 취약점)
    • 성능 문제: 직렬화 및 역직렬화는 CPU와 메모리를 많이 사용하는 작업이므로 대규모 객체를 처리할 때는 성능에 영향을 미칠 수 있습니다.
    • 클래스 버전 관리: 클래스의 구조가 변경될 경우 역직렬화 시 InvalidClassException이 발생할 수 있습니다. serialVersionUID를 명시적으로 선언하고 관리하여 클래스 버전 관리를 철저히 해야 합니다.
    • Circular Reference: 객체 간에 순환 참조가 있을 경우 직렬화 과정에서 무한 루프가 발생할 수 있습니다. 순환 참조를 피하거나, transient 키워드를 사용하여 특정 필드를 직렬화 대상에서 제외해야 합니다.
    • Transient 필드: transient로 선언된 필드는 직렬화되지 않으므로, 역직렬화 후 기본값으로 설정됩니다. 필요한 경우 writeObjectreadObject 메서드를 사용하여 직접 처리해야 합니다.
    • 외부 라이브러리: Jackson, Gson 등 외부 라이브러리를 사용하여 JSON 형태로 직렬화/역직렬화하는 것이 더 효율적이고 안전할 수 있습니다.

     

    자바 직렬화는 객체를 영구적으로 저장하거나 네트워크를 통해 전송하는 데 유용한 메커니즘이지만, 보안 및 성능 문제를 고려해야 합니다. 위에서 설명한 내용들을 숙지하고 적절하게 활용하면 자바 직렬화를 효과적으로 사용할 수 있습니다.

    728x90
    반응형