CodeGym /행동 /JAVA 25 SELF /표준 라이브러리의 인터페이스: Comparable, Serializable 등

표준 라이브러리의 인터페이스: Comparable, Serializable 등

JAVA 25 SELF
레벨 21 , 레슨 4
사용 가능

1. 인터페이스 Comparable<T>

숫자나 문자열 리스트를 정렬해 본 적 있나요? 물론 있겠죠! 이제 여러분만의 객체 리스트 — 예를 들어 학생, 상품, 고양이 리스트가 있다고 상상해 봅시다. Java는 이들을 어떤 순서로 정렬해야 할지 어떻게 알까요? 바로 이때 필요한 것이 Comparable<T> 인터페이스입니다.

이 인터페이스는 객체의 “자연 순서”를 정의합니다 — 즉 해당 데이터 타입에 대해 논리적인 순서입니다. 예를 들어 숫자는 오름차순, 문자열은 사전식, 학생은 성이나 나이(선택)에 따라 정렬할 수 있습니다.

Comparable의 구조

아주 단순한 인터페이스입니다. 메서드는 하나뿐입니다:

public interface Comparable<T> {
    int compareTo(T o);
}

compareTo 메서드는 다음을 반환해야 합니다:

  • 현재 객체가 다른 객체보다 “작으면” 음수;
  • 0이면 “같음”;
  • “크면” 양수.

예제: 학생을 나이로 정렬

Student 클래스를 추가하고 Comparable<Student>를 구현해 봅시다:

public class Student implements Comparable<Student> {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 예제를 위한 getter
    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public int compareTo(Student other) {
        // 나이 기준(오름차순)으로 정렬
        return Integer.compare(this.age, other.age);
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

이제 학생 배열이나 리스트를 쉽게 정렬할 수 있습니다:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("Vasya", 20));
        students.add(new Student("Petya", 18));
        students.add(new Student("Masha", 22));

        Collections.sort(students); // Comparable 덕분에 동작!

        System.out.println("정렬된 학생들:");
        for (Student s : students) {
            System.out.println(s);
        }
    }
}

결과:

Petya (18)
Vasya (20)
Masha (22)

중요한 뉘앙스

Comparable을 구현한다면 compareToequals와 일관되게 동작하도록 하세요. 즉 a.compareTo(b) == 0이면 a.equals(b)true여야 합니다. 그렇지 않으면 정렬과 컬렉션이 예측 불가능하게 동작할 수 있고, 프로그래머의 삶의 의미에 대한 철학적 사색거리가 생길지도 모릅니다.

2. 인터페이스 Serializable

직렬화란 객체가 바이트 시퀀스로 변환되었다가(예: 파일에 저장하거나 네트워크로 전송하기 위해) 다시 복원될 수 있는 능력을 말합니다. 게임 상태를 저장하거나 객체를 서버로 보내고 싶다면 직렬화가 필수입니다.

Java에는 이를 위한 마커 인터페이스 Serializable가 있습니다. 마커 인터페이스란 메서드는 없고, 단지 클래스를 직렬화 가능하다고 “표시”하기만 합니다.

import java.io.Serializable;

public class Student implements Serializable {
    private String name;
    private int age;

    // ... 나머지 코드
}

객체를 직렬화하는 방법

직렬화와 역직렬화에는 ObjectOutputStreamObjectInputStream 클래스를 사용합니다. 예시 — 객체를 파일에 저장하고 다시 읽어 봅니다:

import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Student s = new Student("Katya", 19);

        // 객체를 파일에 저장
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("student.dat"))) {
            out.writeObject(s);
        }

        // 파일에서 객체를 읽기
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("student.dat"))) {
            Student loaded = (Student) in.readObject();
            System.out.println("로드됨: " + loaded);
        }
    }
}

참고: 객체의 모든 필드(중첩 객체 포함)도 직렬화 가능해야 하며, 그렇지 않으면 오류가 발생합니다.

마커 인터페이스가 필요한 이유

Serializable 인터페이스는 구현해야 할 메서드가 없습니다 — 단지 JVM에 “이 객체는 직렬화할 수 있다”고 알립니다. 이를 구현하는 것을 잊으면 직렬화를 시도할 때 NotSerializableException 예외가 발생합니다.

3. 표준 라이브러리의 다른 중요한 인터페이스

인터페이스 Cloneable

또 하나의 마커 인터페이스입니다. Object.clone() 메서드로 객체를 복제할 수 있음을 JVM에 알리는 역할을 합니다. 이것이 없으면 clone() 호출 시 예외가 발생합니다.

하지만 Java에서의 복제는 함정이 있습니다. 기본 복제는 표면적 복사(shallow copy)이며, 종종 그 대신 자체 복사 메서드를 작성하는 편이 좋습니다.

public class Student implements Cloneable {
    private String name;
    private int age;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

인터페이스 AutoCloseable

이 인터페이스에는 close()라는 메서드 하나만 있습니다. 이를 구현한 클래스는 try-with-resources 구문에서 사용할 수 있으며(예: 파일, 스트림), 자원이 자동으로 닫힙니다:

public class MyResource implements AutoCloseable {
    @Override
    public void close() {
        System.out.println("리소스가 닫혔습니다!");
    }
}

public class Main {
    public static void main(String[] args) {
        try (MyResource res = new MyResource()) {
            System.out.println("리소스를 사용 중");
        }
        // 여기에서는 res.close()가 자동으로 호출됩니다
    }
}

인터페이스 Iterable<T>

이 인터페이스는 객체가 for-each 루프에서 “순회 가능”하도록 합니다. iterator() 하나의 메서드를 가지며, Iterator<T> 객체를 반환합니다.

public class MyList implements Iterable<String> {
    // ... 내부 저장소

    @Override
    public java.util.Iterator<String> iterator() {
        // 요소 순회를 위한 이터레이터 반환
        return ...;
    }
}

모든 표준 컬렉션(ArrayList, HashSet 등)은 Iterable을 구현하므로 for-each에서 순회할 수 있습니다.

인터페이스 Comparator<T>

이 인터페이스는 객체를 여러 규칙에 따라 비교할 수 있게 해 주며, 객체 자체는 변경하지 않습니다. 예를 들어 학생을 나이가 아니라 이름으로 정렬할 수 있습니다.

import java.util.Comparator;

Comparator<Student> byName = new Comparator<Student>() {
    @Override
    public int compare(Student a, Student b) {
        return a.getName().compareTo(b.getName());
    }
};

현대 Java에서는 보통 람다 표현식으로 작성합니다:

Comparator<Student> byName = (a, b) -> a.getName().compareTo(b.getName());

Observer, EventListener

이 인터페이스들은 “옵저버”와 “이벤트 리스너” 패턴을 구현할 때 사용됩니다 — 한 객체에서 발생하는 사건에 다른 객체가 반응하는 구조입니다. 예를 들어 그래픽 UI(Swing, JavaFX)에서 버튼 핸들러는 ActionListener 인터페이스를 구현합니다.

4. 실습: Comparable을 구현하고 객체를 직렬화해 보기

예제 1. 사용자 정의 클래스에 Comparable 구현

Book 클래스를 작성하고, 출간 연도로 정렬 가능하게 해 봅시다:

public class Book implements Comparable<Book> {
    private String title;
    private int year;

    public Book(String title, int year) {
        this.title = title;
        this.year = year;
    }

    @Override
    public int compareTo(Book other) {
        return Integer.compare(this.year, other.year);
    }

    @Override
    public String toString() {
        return title + " (" + year + ")";
    }
}
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Book> books = Arrays.asList(
            new Book("Java dlya chaynikov", 2018),
            new Book("Voyna i mir", 1869),
            new Book("Garri Potter", 1997)
        );
        Collections.sort(books);
        System.out.println(books);
    }
}

결과:

[Voyna i mir (1869), Garri Potter (1997), Java dlya chaynikov (2018)]

예제 2. 객체 직렬화

import java.io.*;

public class Book implements Serializable {
    private String title;
    private int year;
    // ... 생성자, getter, toString

    public Book(String title, int year) {
        this.title = title;
        this.year = year;
    }

    @Override
    public String toString() {
        return title + " (" + year + ")";
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Book book = new Book("Java dlya chaynikov", 2018);

        // 객체를 파일에 저장
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("book.dat"))) {
            out.writeObject(book);
        }

        // 파일에서 객체를 읽기
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("book.dat"))) {
            Book loaded = (Book) in.readObject();
            System.out.println("로드됨: " + loaded);
        }
    }
}

5. 표: 표준 라이브러리의 주요 인터페이스

인터페이스 용도 핵심 메서드 사용 예
Comparable<T>
객체의 자연 순서
int compareTo(T o)
리스트 정렬
Comparator<T>
사용자 정의 객체 비교
int compare(T a, T b)
여러 규칙에 따른 정렬
Serializable
객체 직렬화 — (마커형) 객체 저장/로드
Cloneable
객체 복제 — (마커형) 객체 사본 생성
AutoCloseable
리소스 자동 닫기
void close()
try-with-resources
Iterable<T>
컬렉션의 요소 순회
Iterator<T> iterator()
for-each 루프
Observer / EventListener 이벤트에 대한 반응
update(), actionPerformed
UI의 이벤트 처리, 패턴

6. 표준 인터페이스를 사용할 때의 흔한 실수

오류 № 1: 인터페이스를 구현하지 않았는데 기능을 사용하려 함.
예를 들어 Serializable 구현을 잊었는데 객체를 직렬화하려 하면 NotSerializableException이 발생합니다. Cloneable 없이 clone()을 호출하는 경우도 마찬가지입니다.

오류 № 2: Comparable과 equals 계약 위반.
a.compareTo(b)== 0인데 그럼에도 a.equals(b)가 만족되지 않으면 컬렉션이 이상하게 동작할 수 있습니다. 예를 들어 TreeSet은 객체를 “잃어버릴” 수 있습니다.

오류 № 3: 복제 시 얕은 복사.
기본 clone()은 객체의 “겉층”만 복사합니다. 다른 객체를 참조하는 필드가 있다면 깊은 복사가 되지 않습니다. 이는 알 수 없는 버그로 이어질 수 있습니다.

오류 № 4: try-with-resources를 지키지 않음.
클래스가 AutoCloseable을 구현했는데 try-with-resources를 사용하지 않으면 리소스 닫기를 잊고 메모리 누수나 파일 잠금을 유발할 수 있습니다.

오류 № 5: compareTo 또는 compare를 잘못 구현.
음수/0/양수가 아닌 01만 반환한다면 정렬이 올바르게 동작하지 않습니다.

1
과제
JAVA 25 SELF, 레벨 21, 레슨 4
잠금
숫자 "컨테이너" 비교 📦
숫자 "컨테이너" 비교 📦
1
과제
JAVA 25 SELF, 레벨 21, 레슨 4
잠금
도시를 인구 수로 정렬하기 🏙️
도시를 인구 수로 정렬하기 🏙️
1
과제
JAVA 25 SELF, 레벨 21, 레슨 4
잠금
매장의 대체 상품 정렬 🏷️
매장의 대체 상품 정렬 🏷️
1
과제
JAVA 25 SELF, 레벨 21, 레슨 4
잠금
보안 리소스 자동 닫기 🔑
보안 리소스 자동 닫기 🔑
1
설문조사/퀴즈
고급 인터페이스, 레벨 21, 레슨 4
사용 불가능
고급 인터페이스
고급 인터페이스와 функциональные интерфейсы
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION