CodeGym /행동 /JAVA 25 SELF /중첩 및 계층 객체: 그래프 직렬화

중첩 및 계층 객체: 그래프 직렬화

JAVA 25 SELF
레벨 44 , 레슨 1
사용 가능

1. 컬렉션 안의 컬렉션 직렬화

Java에서 컬렉션은 단순 타입(예: String)뿐 아니라 다른 컬렉션이나 객체도 담을 수 있습니다. 이는 복잡한 구조를 만드는 길을 엽니다. 예를 들어 Map<String, List<User>>처럼, 여기서 User는 여러분이 만든 클래스입니다.

예시: 중첩된 List가 있는 Map 직렬화

각 사용자에게 친구 목록이 있는 작은 소셜 네트워크 예시를 살펴보겠습니다.

import java.io.*;
import java.util.*;

class User implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;

    User(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" + "name='" + name + '\'' + '}';
    }
}

public class SocialNetwork implements Serializable {
    private static final long serialVersionUID = 1L;
    Map<String, List<User>> friends = new HashMap<>();

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SocialNetwork network = new SocialNetwork();
        network.friends.put("alice", Arrays.asList(new User("bob"), new User("carol")));
        network.friends.put("bob", Collections.singletonList(new User("alice")));

        // 직렬화
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("network.ser"))) {
            out.writeObject(network);
        }

        // 역직렬화
        SocialNetwork loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
            loaded = (SocialNetwork) in.readObject();
        }

        System.out.println("복원된 네트워크: " + loaded.friends);
    }
}

핵심은 다음과 같습니다. 사용된 모든 타입 — HashMap, ArrayList, User 등 — 이 Serializable 인터페이스를 구현합니다. 직렬화 시 Java는 모든 중첩 컬렉션과 객체를 자동으로 순회하며 함께 기록합니다. 따라서 역직렬화 후에는 모든 내부 리스트를 포함한 구조 전체가 완전히 복원됩니다.

출력:

복원된 네트워크: {alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}

원하는 만큼의 중첩

원한다면 여러 단계로 중첩할 수 있습니다: List<List<User>>, Map<String, Map<Integer, List<User>>> 등 — Java는 (합리적인 범위에서) 재귀를 두려워하지 않습니다.

2. 계층적 객체: 상속이 있는 컬렉션 직렬화

컬렉션에 상속 구조를 가진 객체가 담겨 있다면 어떻게 될까요? 예를 들어 기본 클래스 Animal이 있고, 컬렉션에는 CatDog가 함께 들어 있는 경우입니다.

예시: 하위 클래스를 담은 컬렉션 직렬화

import java.io.*;
import java.util.*;

abstract class Animal implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;

    Animal(String name) {
        this.name = name;
    }

    public abstract String speak();
}

class Cat extends Animal {
    private static final long serialVersionUID = 1L;

    Cat(String name) {
        super(name);
    }

    @Override
    public String speak() {
        return "Meow!";
    }
}

class Dog extends Animal {
    private static final long serialVersionUID = 1L;

    Dog(String name) {
        super(name);
    }

    @Override
    public String speak() {
        return "Woof!";
    }
}

public class Zoo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<Animal> animals = new ArrayList<>();
        animals.add(new Cat("무르카"));
        animals.add(new Dog("샤릭"));

        // 직렬화
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("zoo.ser"))) {
            out.writeObject(animals);
        }

        // 역직렬화
        List<Animal> loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("zoo.ser"))) {
            loaded = (List<Animal>) in.readObject();
        }

        for (Animal animal : loaded) {
            System.out.println(animal.name + " 말합니다: " + animal.speak());
        }
    }
}

결과:

무르카 말합니다: Meow!
샤릭 말합니다: Woof!

중요한 점: Java는 기본 클래스의 필드뿐 아니라 객체의 실제 타입 정보도 함께 직렬화합니다. 따라서 역직렬화 이후에도 객체는 자신이 “고양이”인지 “개”인지의 정체성을 유지하므로, 해당 메서드를 안전하게 호출할 수 있습니다.

3. 객체 그래프 직렬화

이제 진짜 마법 — 서로를 참조할 수 있는 객체 그래프의 직렬화로 넘어가 봅시다. 이는 단순히 내부에 포함되는 것만이 아니라, 객체들이 서로를 가리키는 구조입니다. 먼저 객체 그래프가 무엇인지부터 짚고 가겠습니다.

객체 그래프란?

객체 그래프는 필드 참조를 통해 객체들이 서로 연결된 구조입니다. 예를 들어 가계도에서 각 사람은 부모, 자녀, 형제자매에 대한 참조를 가질 수 있습니다.

비유: 소셜 네트워크의 친구 관계를 떠올려 보세요. 각 사용자에게 친구 목록이 있고, 그 친구들 역시 사용자이며, 그들만의 친구들이 계속 이어집니다. 이것이 바로 객체 그래프입니다.

예시: 이중 연결 리스트 직렬화

import java.io.*;

class Node implements Serializable {
    private static final long serialVersionUID = 1L;
    String value;
    Node next;
    Node prev;

    Node(String value) {
        this.value = value;
    }
}

public class DoublyLinkedListDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // 연결된 두 노드를 생성
        Node first = new Node("A");
        Node second = new Node("B");
        first.next = second;
        second.prev = first;

        // 직렬화
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
            out.writeObject(first);
        }

        // 역직렬화
        Node loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
            loaded = (Node) in.readObject();
        }

        System.out.println("첫 번째 노드의 값: " + loaded.value); // "A"
        System.out.println("다음 노드: " + loaded.next.value);   // "B"
        System.out.println("다음 노드의 이전: " + loaded.next.prev.value); // "A"
    }
}

주의할 점은, 하나의 시작 참조(first)만 직렬화했더라도 Java가 재귀적으로 모든 연결된 객체를 따라간다는 것입니다. 역직렬화 시 참조 구조는 완전히 복원됩니다. 즉, loaded.next.prev == loadedtrue가 됩니다! 또한 그래프에 순환이 있어도(예: 노드들이 서로를 가리키는 경우) 표준 Java 직렬화는 올바르게 동작하며 무한 루프에 빠지지 않습니다.

4. 중첩 및 계층 컬렉션: 실제 클래스 예시

모델: 도서 카탈로그

Book 클래스가 있고, 종이책 또는 전자책(상속)일 수 있다고 합시다. 그리고 장르에 따른 카탈로그(Map<String, List<Book>>)를 보관하는 Library 클래스가 있습니다. 각 장르는 책 목록입니다.

import java.io.*;
import java.util.*;

abstract class Book implements Serializable {
    private static final long serialVersionUID = 1L;
    String title;

    Book(String title) {
        this.title = title;
    }
}

class PaperBook extends Book {
    private static final long serialVersionUID = 1L;
    int pages;

    PaperBook(String title, int pages) {
        super(title);
        this.pages = pages;
    }
}

class EBook extends Book {
    private static final long serialVersionUID = 1L;
    String format;

    EBook(String title, String format) {
        super(title);
        this.format = format;
    }
}

class Library implements Serializable {
    private static final long serialVersionUID = 1L;
    Map<String, List<Book>> catalog = new HashMap<>();
}

public class CatalogDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Library library = new Library();
        library.catalog.put("공상과학", Arrays.asList(
                new PaperBook("듄", 800),
                new EBook("마션", "epub")
        ));
        library.catalog.put("고전", Collections.singletonList(
                new PaperBook("전쟁과 평화", 1200)
        ));

        // 직렬화
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("library.ser"))) {
            out.writeObject(library);
        }

        // 역직렬화
        Library loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("library.ser"))) {
            loaded = (Library) in.readObject();
        }

        for (Map.Entry<String, List<Book>> entry : loaded.catalog.entrySet()) {
            System.out.println("장르: " + entry.getKey());
            for (Book book : entry.getValue()) {
                System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
            }
        }
    }
}

출력:

장르: 공상과학
 - 듄 (PaperBook)
 - 마션 (EBook)
장르: 고전
 - 전쟁과 평화 (PaperBook)

정리:

  • 중첩 컬렉션(Map<String, List<Book>>)의 직렬화는 즉시 동작합니다.
  • 객체의 타입(PaperBook, EBook)이 보존됩니다.
  • 역직렬화 후 구조가 완전히 복원됩니다.

5. 객체 그래프 직렬화: “후드 아래”에서는 무엇이 일어나나?

객체를 직렬화할 때 Java는 그 객체의 모든 필드(그리고 그 필드의 필드, …)를 따라가며 각 객체를 단 한 번만 직렬화합니다. 동일한 객체가 다시 등장하면(예: 순환 참조) Java는 그 객체를 다시 직렬화하지 않고, 특별한 참조를 기록합니다.

시각화(블록 다이어그램)

graph TD
    A[객체 A] -- 필드 --> B[객체 B]
    B -- 필드 --> C[객체 C]
    C -- 필드 --> A

Java는 먼저 A를, 그다음 B, 그다음 C를 직렬화하고, 다시 A를 만나면 “이미 직렬화된 객체 A에 대한 참조”를 기록합니다. 역직렬화 시 모든 연결을 보존한 채 구조가 복원됩니다.

6. 그래프 직렬화의 특징

  • 순환은 문제되지 않습니다: 표준 Java 직렬화는 순환 참조를 지원하며, 무한 루프에 빠지지 않고 StackOverflow도 발생시키지 않습니다.
  • 모든 객체가 직렬화 가능해야 합니다: 그래프의 객체 중 하나라도 Serializable을 구현하지 않으면, 해당 지점에서 직렬화가 실패합니다.
  • 같은 객체는 중복되지 않습니다: 동일한 객체가 그래프의 여러 곳에서 참조되더라도, 역직렬화 후에는 같은 객체(참조 동일)가 유지됩니다.
  • 객체 타입이 보존됩니다: 컬렉션이 List<Animal>로 선언되어 있어도, 역직렬화 후에는 실제 클래스(Cat, Dog 등)의 객체를 얻게 됩니다.

7. 중첩·계층 객체 직렬화 시 흔한 실수

오류 №1: 모든 클래스가 직렬화 가능하지 않음.
중첩된 객체나 컬렉션 내부에 있는 사용자 정의 클래스 중 하나에 implements Serializable를 빠뜨리는 경우가 매우 잦습니다. 결과는 NotSerializableException과 실망입니다. 중첩 체인을 반드시 점검하세요!

오류 №2: 수동 직렬화 시 참조 손실.
writeObject/readObject를 직접 구현하면서 어떤 필드(예: 부모에 대한 참조나 중첩 컬렉션)를 직렬화하지 않으면, 역직렬화 후 구조가 손상됩니다. 복원을 항상 테스트하세요.

오류 №3: 필요한 필드에 transient 사용.
필드를 transient로 표시하면 직렬화 스트림에 포함되지 않아, 복원 후 null이 되거나 기본값을 갖게 됩니다. 이는 객체 그래프의 무결성을 해칠 수 있습니다.

오류 №4: 직렬화와 역직렬화 사이 클래스 구조 변경.
객체를 직렬화한 뒤 클래스 구조(예: 필드 추가)를 변경하면, 역직렬화 시 오류가 발생하거나 데이터가 유실될 수 있습니다. serialVersionUID를 사용하고 호환성을 유지하세요.

오류 №5: 거대한 그래프 직렬화.
복잡하게 얽힌 구조는 파일이 매우 커지고 직렬화/역직렬화 시간이 길어질 수 있습니다. 크기를 모니터링하고 가능하면 분할을 고려하세요.

오류 №6: “raw” 컬렉션 직렬화.
generic 파라미터 없이 컬렉션을 선언했다면(예: 그냥 List), 역직렬화 후 명시적 캐스팅이 필요하고, 이는 ClassCastException을 유발할 위험이 큽니다. 제네릭을 사용하고 타입을 점검하세요.

1
과제
JAVA 25 SELF, 레벨 44, 레슨 1
잠금
순환 경로를 가진 지하 터널 매핑 🚇
순환 경로를 가진 지하 터널 매핑 🚇
1
과제
JAVA 25 SELF, 레벨 44, 레슨 1
잠금
하이퍼마켓 상품 관리: 계층 구조와 컬렉션 🏪
하이퍼마켓 상품 관리: 계층 구조와 컬렉션 🏪
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION