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을 구현한다면 compareTo가 equals와 일관되게 동작하도록 하세요. 즉 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;
// ... 나머지 코드
}
객체를 직렬화하는 방법
직렬화와 역직렬화에는 ObjectOutputStream과 ObjectInputStream 클래스를 사용합니다. 예시 — 객체를 파일에 저장하고 다시 읽어 봅니다:
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. 표: 표준 라이브러리의 주요 인터페이스
| 인터페이스 | 용도 | 핵심 메서드 | 사용 예 |
|---|---|---|---|
|
객체의 자연 순서 | |
리스트 정렬 |
|
사용자 정의 객체 비교 | |
여러 규칙에 따른 정렬 |
|
객체 직렬화 | — (마커형) | 객체 저장/로드 |
|
객체 복제 | — (마커형) | 객체 사본 생성 |
|
리소스 자동 닫기 | |
try-with-resources |
|
컬렉션의 요소 순회 | |
for-each 루프 |
| Observer / EventListener | 이벤트에 대한 반응 | |
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/양수가 아닌 0과 1만 반환한다면 정렬이 올바르게 동작하지 않습니다.
GO TO FULL VERSION