이전에 Collection Framework에 대해 정리하였는데, 프로젝트 팀원으로부터 JDK21 버전 부터 Collection에 새로운 기능들이 추가되었다고 해서 해당 내용도 정리해 보고자 한다.
Sequenced Collectiond은 정렬을 제공하는 Collection 자료구조들의 첫, 마지막 데이터로의 접근, 그리고 정렬 및 역정렬에 대한 표준화된 API를 제공하여 기존 Collection의 문제점을 보완하기 위해 도입된 개념이다.
Oracle 공식 문서와 개인적인 주관을 섞어서 해석해 보았다.
개념 설명만으로는 와닿지 않을 수 있으니, 이러한 개념이 나타난 이유들을 알아 보자.
정렬된 자료구조를 사용할 경우 첫 번째 데이터와 마지막 데이터에 접근할 일이 많다. 조회를 하거나, 해당 위치에서 삽입 삭제가 빈번하게 일어나기 때문이다.
다만, 기존의 Collection Framework에서는 정렬된 Collection들에 대한 일관적인 인터페이스가 제공되지 않았다.
예를 들어, List의 경우 저장된 데이터들 중 첫 번째 데이터를 가져올 때에는 get(0)을 사용하고 마지막 데이터를 가져올 때에는 get(list.size() - 1)를 사용한다.
반면에 Deque같은 경우, getFirst()와 getLast() 메소드를 지원하기 때문에 보다 간편하게 데이터에 접근이 가능하다.
이런 일관적이고 직관적이지 못 한 사용 방식은 그리 좋은 현상은 아니었다.
데이터를 순회할 때, 정렬 상태 그대로 순회하기도 하지만 역방향으로 순회를 하는 경우가 생각 보다 많다. 이럴 때에 개발자가 직접 역순으로 조회하는 코드를 작성해야 했기 때문에 불필요하게 반복되는 코드들이 많았다.
정렬이 되어 있는 구현체를 상위 인터페이스로 변환하면 그 특성을 잃어버린다는 문제가 있다.
LinkedHashSet을 Set으로 변환하여 사용하는 경우, 추후 Set만 보고는 해당 구현체가 정렬을 제공하는 자료구조인지 아닌지 알 방법이 없어진다. 실제 동작할 때에는 정렬을 하겠지만, 개발자 입장에서 해당 구현체가 정렬을 제공하고 있다는 정보를 잃게 되는 것이다.
Sequenced Collection은 이러한 문제점을 해결하기 위해 상위 인터페이스로서 도입되어 일관적이고 공통된 인터페이스를 제공한다.
Sequenced Collection이 도입되면서 Collection은 아래와 같은 구조를 띄게 되었다.
Sequenced Collection을 잘 보면 List는 Sequenced Collection를 상속받아 하위에 위치하지만 Set, Qeueu, Map는 그렇지 않은 것을 볼 수 있다.
List는 순서가 보장되는 선형 자료구조이기 때문에 어떤 구현체이든 순서가 존재한다. 따라서 SequencedCollection이 List보다 상위의 인터페이스에 해당하게 된다.
Set은 애초에 순서를 저장하지 않는 자료구조이기 때문에 SequencedCollection의 상속 대상이 아니다. 다만 SortedSet과 같은 Set이지만 정렬을 지원하는 구현체들은 SequencedCollection을 상속받는 SequencedSet인터페이스를 구현하도록 되어 있다.
Map은 Set과 마찬가지로 순서를 저장하지 않는 자료구조이고, 특정 구현체만 정렬을 지원하기 때문에 Map이 최상위 인터페이스이고 그 하위에 SequencedMap이 존재한다.
Queue은 삽입된 데이터가 순서대로 나오는 구조이기 때문에 특정 데이터(Sequenced Collection에서는 첫, 마지막 데이터)에 임의로 접근하는 기능은 지원하지 않는다. 따라서 Queue자체는 SequencedCollection을 상속받지 않지만, Deque처럼 양방향에서 데이터 접근을 가능하도록 하는 자료구조는 Sequenced Collection을 적용하기 적절한 형태이기 때문에 구현하도록 되어 있다.
그렇다면 SequencedCollection은 정렬된 자료구조로부터 어떤 형태로 일관된 인터페이스를 제공할까?
가장 최상위인 SequencedCollection인터페이스부터 살펴 보면
interface SequencedCollection<E> extends Collection<E> {
SequencedCollection<E> reversed();
// methods promoted from Deque
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
가장 먼저 눈에 띄는 건 reversed()메소드이다.
해당 메소드는 정렬된 자료구조가 역으로 정렬된 Collection을 return해 주는 메소드이다.
이 메소드로 반환된 데이터는 기존의 원본 데이터의 변경을 그대로 따라가며, 반대로 이 데이터의 변경이 원본 데이터에도 반영된다.
var list = new ArrayList<>(Arrays.asList("a", "b", "c", "d", "e"))
list ==> [a, b, c, d, e]
var rev = list.reversed()
rev ==> [e, d, c, b, a]
rev.add(1, "f") // 역방향 데이터에 "f" 추가
rev ==> [e, f, d, c, b, a]
list ==> [a, b, c, d, f, e]
위의 코드처럼 역으로 정렬된 데이터의 변경이 원본에 영향을 준다. 반대의 경우도 마찬가지이다.
그외의 메소드들은 직관적이기 때문에 쉽게 이해할 수 있다.
SequencedMap은 정렬을 제공하는 Map 구현체들이 구현하는 인터페이스이다.
reversed()
putFirst()
putLast()
sequencedEntrySet()
sequencedKeySet()
sequencedValues()
위와 같은 메소드들을 제공한다. 크게 어려운 내용 없이 정렬된 Key나 Value를 제공하고, 구현체들은 putFirst나 putLast와 같은 메소드를 통해 SequencedCollection의 addFirst나 addLast를 구현하고 있을 것으로 추정된다.
( reversed()는 SequencedMap을 반환하는 메소드이다. SequencedCollection을 반환하는 SequencedCollection의 reversed()와 반환 타입을 제외하면 갖다. )
SequencedSet은 SequencedSet을 반환하는 reversed() 메소드가 전부이다.
이렇게 JDK21부터 새롭게 도입된 Sequenced Collection을 알아 보았다.
덕분에 일관적인 인터페이스를 통해 데이터 접근이 용이해졌고, 개발자들에게 역정렬 편의성을 제공해 주니 좋은 기능인 것 같다.
또한 정렬된 자료구조에 대해서는 Sequenced인터페이스를 제공하니, 구현체의 정렬 유무에 따라 사용 가능한 인터페이스도 확장되었다.
예를 들어, 정렬된 Map구현체를 사용한다면 Map인터페이스가 아닌 SequencedMap인터페이스를 사용하여 개발자에게 더 여러 기능을 제공하고, 정렬된 상태임을 인지시킬 수 있을 것 같다.