1. 전통적인 컬렉션에서의 순서 문제
Java에는 언제나 요소의 순서를 보장하는 컬렉션(예: ArrayList, LinkedList, LinkedHashSet, LinkedHashMap)과 순서를 보장하지 않는 컬렉션(예: HashSet, HashMap)이 있었습니다. 하지만 이러한 컬렉션에는 공통된 단점이 하나 있었는데, 일부 컬렉션이 특정 순서로 요소를 저장하더라도 표준 인터페이스(List, Set, Map)는 첫/마지막 요소에 접근하거나 순서를 뒤집는 범용 메서드를 제공하지 않았다는 점입니다.
예를 들어 List<String>이 있다면 list.get(0)으로 첫 요소를 얻을 수 있지만, Set이나 Map에서는 이런 요령이 통하지 않습니다. 반복자를 사용하거나 불필요한 코드를 작성해야 하죠. 그리 편리하지도 않고 가독성에도 도움이 되지 않습니다.
마치 서랍장이 있는데 첫 번째나 마지막 서랍을 꺼내려면 매번 모든 서랍을 손으로 세어야 하는 것과 같습니다! "첫 번째"와 "마지막"이라는 전용 손잡이가 있으면 얼마나 좋을까요?
SequencedCollection 및 관련 인터페이스의 등장
Java 21에는 새로운 컬렉션 인터페이스가 추가되었습니다:
- SequencedCollection<E>
- SequencedSet<E>
- SequencedMap<K, V>
이 인터페이스들은 표준 컬렉션을 확장하여 요소 순서를 다루는 일관된 방식을 도입합니다. 이제 순서가 중요한 어떤 컬렉션이든 구체적인 구현에 신경 쓰지 않고 범용 코드를 작성할 수 있습니다.
이게 무엇인가요?
- SequencedCollection은 요소에 명확한 순서가 있으며, 첫 요소와 마지막 요소를 쉽게 얻고 순서를 뒤집어 볼 수 있는 컬렉션입니다.
- SequencedSet은 고유한 요소를 갖는 Set에 동일한 개념을 적용한 것입니다.
- SequencedMap은 키-값 쌍을 갖는 Map에 동일한 개념을 적용한 것입니다.
어떤 컬렉션이 이를 구현하나요?
Java 21에서 새로운 인터페이스를 구현하는 표준 컬렉션은 다음과 같습니다:
- ArrayList, LinkedList → SequencedCollection
- LinkedHashSet, TreeSet → SequencedSet
- LinkedHashMap, TreeMap → SequencedMap
즉, 이미 이러한 컬렉션을 사용 중이라면 자동으로 새로운 기능을 얻게 됩니다!
2. SequencedCollection, SequencedSet, SequencedMap의 주요 메서드
SequencedCollection의 메서드
E getFirst(); // 첫 번째 요소 가져오기
E getLast(); // 마지막 요소 가져오기
SequencedCollection<E> reversed(); // 역순 뷰 가져오기
SequencedSet의 메서드
SequencedCollection과 동일한 메서드에, Set이 제공하는 모든 기능이 더해집니다.
SequencedMap의 메서드
Map.Entry<K, V> firstEntry(); // 첫 엔트리(키-값)
Map.Entry<K, V> lastEntry(); // 마지막 엔트리(키-값)
SequencedMap<K, V> reversed(); // 역순 Map 뷰
3. 새로운 인터페이스 사용 예시
예제 1: 첫 번째와 마지막 요소 가져오기
import java.util.*;
public class SequencedDemo {
public static void main(String[] args) {
SequencedCollection<String> sc = new ArrayList<>();
sc.add("Java");
sc.add("Python");
sc.add("Kotlin");
// 첫 번째와 마지막 요소 가져오기
String first = sc.getFirst(); // "Java"
String last = sc.getLast(); // "Kotlin"
System.out.println("첫 번째: " + first);
System.out.println("마지막: " + last);
}
}
출력:
첫 번째: Java
마지막: Kotlin
예제 2: 컬렉션 역순
SequencedCollection<String> sc = new LinkedList<>();
sc.add("A");
sc.add("B");
sc.add("C");
SequencedCollection<String> reversed = sc.reversed();
System.out.println(reversed); // [C, B, A]
주의: reversed()는 컬렉션을 역순으로 표현한 뷰를 반환합니다. 원본 컬렉션을 변경하면 reversed 뷰도 함께 변경됩니다!
예제 3: SequencedSet 사용
SequencedSet<Integer> set = new LinkedHashSet<>();
set.add(100);
set.add(200);
set.add(300);
System.out.println("첫 요소: " + set.getFirst()); // 100
System.out.println("마지막 요소: " + set.getLast()); // 300
SequencedSet<Integer> reversedSet = set.reversed();
System.out.println(reversedSet); // [300, 200, 100]
예제 4: SequencedMap 사용
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("apple", 5);
map.put("banana", 3);
map.put("cherry", 7);
Map.Entry<String, Integer> first = map.firstEntry();
Map.Entry<String, Integer> last = map.lastEntry();
System.out.println("첫 번째: " + first.getKey() + " = " + first.getValue()); // apple = 5
System.out.println("마지막: " + last.getKey() + " = " + last.getValue()); // cherry = 7
SequencedMap<String, Integer> reversedMap = map.reversed();
System.out.println(reversedMap); // {cherry=7, banana=3, apple=5}
4. 내 애플리케이션과 어떤 관련이 있나요?
학습용 애플리케이션에서 로그인한 사용자 목록을 저장하고, 첫 번째와 마지막 사용자를 빠르게 가져오고 싶다고 가정해 봅시다(예: "누가 가장 먼저 로그인했는지"와 "누가 마지막인지" 표시). 예전에는 다음과 같이 작성해야 했습니다:
List<String> users = new ArrayList<>();
// ... 사용자 추가
String first = users.get(0);
String last = users.get(users.size() - 1);
하지만 컬렉션이 리스트가 아니라 예를 들어 LinkedHashSet(요소가 고유하고 순서가 유지됨)이라면, 이런 요령은 통하지 않습니다:
Set<String> users = new LinkedHashSet<>();
// ... 사용자 추가
// 첫 사용자를 어떻게 가져오나? 반복자만 사용 가능:
String first = users.iterator().next();
// 마지막 사용자는? 전부 순회해야 함!
이제는 더 간단하고 범용적으로 처리할 수 있습니다:
SequencedSet<String> users = new LinkedHashSet<>();
// ... 사용자 추가
String first = users.getFirst();
String last = users.getLast();
이는 코드량을 줄일 뿐 아니라 더 읽기 쉽고 안전하게 만듭니다.
5. 새 인터페이스 개요
classDiagram
Collection <|-- SequencedCollection
List <|-- SequencedCollection
Set <|-- SequencedSet
Map <|-- SequencedMap
SequencedCollection <|-- SequencedSet
SequencedSet <|-- LinkedHashSet
SequencedSet <|-- TreeSet
SequencedCollection <|-- ArrayList
SequencedCollection <|-- LinkedList
SequencedMap <|-- LinkedHashMap
SequencedMap <|-- TreeMap
6. 유용한 팁과 주의점
SequencedCollection의 실용적 장점
- 순서를 다루기 위한 단일 인터페이스: 어디서는 get(0)을 쓰고, 어디서는 반복자가 필요하고, 어디서는 첫 요소를 아예 얻을 수 없는지 더 이상 외울 필요가 없습니다.
- 큐와 스택 작업의 편의성: 첫(head) 요소와 마지막(tail) 요소를 쉽게 가져올 수 있습니다.
- 안전성과 가독성: 컬렉션 오용에서 비롯되는 오류가 줄고, 코드가 자체적으로 의도를 드러냅니다.
- 컬렉션을 빠르게 역순으로 보기: reversed()로 수작업 없이 손쉽게 역순 뷰를 얻을 수 있습니다.
- 코드 유지보수와 확장의 용이성: 앞으로 예를 들어 ArrayList를 LinkedHashSet으로 바꾸고 싶어도, SequencedCollection을 사용하는 코드는 다시 작성할 필요가 없습니다.
구현상의 특징
- 모든 컬렉션이 SequencedCollection을 구현하는 것은 아닙니다: 예를 들어 HashSet과 HashMap은 순서를 보장하지 않으므로 새로운 인터페이스를 구현하지 않습니다.
- 메서드가 예외를 던질 수 있습니다: 컬렉션이 비어 있으면 getFirst()나 getLast() 호출 시 NoSuchElementException이 발생합니다. 컬렉션이 비어 있지 않은지 확인하세요!
- reversed()는 복사가 아니라 "뷰"입니다: 원본 컬렉션의 변경 사항은 역순 뷰에도 반영되고 그 반대도 마찬가지입니다.
- 호환성: 새로운 인터페이스는 Java 21부터 사용할 수 있습니다. 더 낮은 버전의 Java를 사용 중이라면 아직 이용할 수 없습니다(업데이트를 고려해 보세요!).
- 제네릭(Generics): 모든 새로운 인터페이스는 제네릭을 완전히 지원하므로 어떤 데이터 타입과도 함께 사용할 수 있습니다.
7. SequencedCollection 사용 시 흔한 실수
오류 1: 순서를 보장하지 않는 컬렉션에서 순서를 기대함. HashSet을 SequencedSet으로 캐스팅하려고 하면 컴파일 오류가 발생합니다. HashSet에는 순서가 없으며 이 인터페이스를 구현하지 않습니다.
오류 2: 비어 있는 컬렉션을 무시함. 비어 있는 컬렉션에서 getFirst()나 getLast()를 호출하면 예외가 발생합니다. 이 메서드를 호출하기 전에 컬렉션이 비어 있지 않은지 확인하세요:
if (!sc.isEmpty()) {
String first = sc.getFirst();
}
오류 3: reversed()에 대한 오해. reversed()는 복사가 아니라 뷰를 반환합니다. reversed 뷰를 변경하면 원본 컬렉션도 변경되고(그 반대도 마찬가지), 예상치 못한 결과를 초래할 수 있습니다.
오류 4: 오래된 Java 버전에서 새로운 인터페이스 사용. 프로젝트가 Java 21 미만 버전으로 컴파일된다면 컴파일러가 이 인터페이스들을 찾지 못합니다. 프로젝트 설정에서 JDK 버전을 확인하세요!
GO TO FULL VERSION