1. 개요

자바의 컬렉션 프레임워크(Collection Framework) 는 다수의 데이터를 효율적으로 저장, 관리, 조작할 수 있도록 표준화된 데이터 구조와 알고리즘을 제공하는 클래스와 인터페이스의 집합이다. 배열(Array)이 고정된 크기만을 지원하는 한계를 극복하기 위해 가변 크기의 데이터 구조를 제공하며, 표준화된 인터페이스를 통해 일관된 방식으로 데이터를 다룰 수 있게 해준다.

2. 컬렉션 프레임워크의 핵심 인터페이스 구조

자바 컬렉션 프레임워크는 크게 Collection 인터페이스와 Map 인터페이스 두 개의 주요 줄기로 나뉜다.

                  Iterable
                     │
                 Collection
      ┌──────────────┼──────────────┐
     List           Set           Queue
      │              │              │
 ArrayList      HashSet        LinkedList
LinkedList      TreeSet         ArrayDeque
 Vector

(※ Map 인터페이스는 구조상의 이유로 Collection을 상속받지 않고 독립적으로 존재한다.)

3. List 인터페이스

순서가 있는 데이터의 집합이며, 데이터의 중복을 허용한다.

  • ArrayList: 내부적으로 가변 배열을 사용하여 데이터를 관리한다. 인덱스(Index)를 통한 데이터 조회(get) 속도가 빠르다($O(1)$). 반면, 중간에 데이터를 삽입하거나 삭제할 때는 요소들의 이동이 필요하여 성능이 저하된다.
  • LinkedList: 양방향 연결 리스트 구조로 구현되어 있다. 요소의 삽입과 삭제가 빈번하게 발생하는 상황에서 뛰어난 성능을 보인다. 인덱스 조회의 경우 처음부터 순회해야 하므로 $O(N)$의 시간 복잡도를 갖는다.

아래 예제는 ArrayList에 과일 이름을 넣고 조회·삭제하면서, 순서 유지와 중복 허용을 확인한다. 변수 타입을 구현체(ArrayList)가 아닌 인터페이스(List)로 선언해 두면 나중에 LinkedList로 바꿀 때 오른쪽만 수정하면 된다.

List<String> fruits = new ArrayList<>();
fruits.add("apple");
fruits.add("banana");
fruits.add("apple");                  // 중복 허용

System.out.println(fruits);           // [apple, banana, apple]
System.out.println(fruits.get(1));    // banana (인덱스로 조회)
System.out.println(fruits.size());    // 3

fruits.remove("apple");               // 앞에서부터 첫 번째 apple만 삭제
System.out.println(fruits);           // [banana, apple]

넣은 순서대로 저장되고 같은 값 "apple"이 두 번 들어간 것을 확인할 수 있다. remove(Object)는 일치하는 요소 중 첫 번째 하나만 지운다.

List에서 자주 쓰는 메서드는 다음과 같다. size(), isEmpty(), contains(), clear()는 Collection 인터페이스에 정의되어 있어 Set, Queue에서도 똑같이 쓸 수 있다.

메서드 설명
add(E e) 맨 뒤에 요소를 추가한다.
add(int index, E e) 지정한 위치에 요소를 끼워 넣는다. 뒤의 요소들은 한 칸씩 밀린다.
get(int index) 지정한 위치의 요소를 반환한다.
set(int index, E e) 지정한 위치의 요소를 새 값으로 바꾼다.
remove(int index) / remove(Object o) 위치 또는 값으로 요소를 삭제한다.
indexOf(Object o) 값이 처음 나오는 위치를 반환한다. 없으면 -1이다.
contains(Object o) 값이 들어 있는지 true/false로 알려 준다.
size() / isEmpty() 요소 개수 / 비어 있는지 여부를 반환한다.
sort(Comparator c) 정렬한다. null을 넘기면 기본 순서(오름차순)로 정렬한다.
clear() 모든 요소를 삭제한다.

List<Integer>에서 remove(1)을 호출하면 값 1이 아니라 인덱스 1의 요소가 삭제된다. 값을 지우려면 remove(Integer.valueOf(1))처럼 객체로 넘겨야 한다.

4. Set 인터페이스

순서를 유지하지 않는 데이터의 집합이며, 중복을 허용하지 않는다.

  • HashSet: 해시 테이블(Hash Table) 기반으로 구현되어 데이터를 저장하고 검색한다. 요소의 추가, 삭제, 검색 속도가 매우 빠르다($O(1)$). 중복 여부를 판단할 때 저장되는 객체의 hashCode()와 equals() 메서드를 활용한다.
  • TreeSet: 이진 탐색 트리(Red-Black Tree) 구조를 기반으로 구성된다. 데이터가 자동으로 오름차순 또는 지정한 정렬 기준에 따라 정렬되어 저장된다. 검색 및 정렬 연산에 $O(\log N)$의 시간이 소요된다.

같은 과일 이름을 HashSet과 TreeSet에 각각 넣어 중복 제거와 정렬 여부를 비교해 본다.

Set<String> hashSet = new HashSet<>();
hashSet.add("banana");
hashSet.add("apple");
System.out.println(hashSet.add("apple"));     // false (이미 있으므로 추가되지 않음)
System.out.println(hashSet.contains("apple")); // true
System.out.println(hashSet);                   // 예: [banana, apple] (순서 보장 X)

Set<String> treeSet = new TreeSet<>(List.of("cherry", "banana", "apple"));
System.out.println(treeSet);                   // [apple, banana, cherry] (자동 정렬)

add()는 이미 같은 값이 있으면 false를 반환하고 저장하지 않는다. HashSet의 출력 순서는 해시 값에 따라 달라지므로 실행 환경마다 다를 수 있고, TreeSet은 항상 오름차순으로 출력된다.

직접 만든 클래스를 HashSet에 넣을 때는 equals()와 hashCode()를 함께 재정의해야 같은 내용의 객체를 중복으로 인식한다.

Set은 인덱스가 없으므로 get(int) 같은 위치 기반 메서드가 없다. 자주 쓰는 메서드는 다음과 같다.

메서드 설명
add(E e) 요소를 추가한다. 이미 있으면 추가하지 않고 false를 반환한다.
remove(Object o) 요소를 삭제한다.
contains(Object o) 요소가 들어 있는지 확인한다. Set을 쓰는 가장 흔한 이유다.
size() / isEmpty() / clear() List와 동일하다.
first() / last() (TreeSet 전용) 가장 작은 값 / 가장 큰 값을 반환한다.

5. Map 인터페이스

키(Key)와 값(Value)의 쌍으로 데이터를 관리한다. 키는 중복될 수 없으나, 값은 중복을 허용한다.

  • HashMap: 키와 값을 해시 테이블 구조로 저장한다. 키의 중복을 허용하지 않으며, 순서를 보장하지 않는다. 가장 흔하게 사용되는 Map 구현체다.
  • TreeMap: 키를 기준으로 자동 정렬되는 Map 구조다. 정렬된 상태로 키-값 쌍을 조회하거나 범위 검색이 필요할 때 유용하다.
  • ConcurrentHashMap: 다중 스레드(Multi-thread) 환경에서 스레드 안정성(Thread-safety)을 제공하면서도, 기존의 Hashtable 대비 높은 성능을 발휘하도록 분할 Lock 메커니즘을 사용한다.

아래 예제는 과일 이름을 키로, 재고 수량을 값으로 저장한다.

Map<String, Integer> stock = new HashMap<>();
stock.put("apple", 3);
stock.put("banana", 5);
stock.put("apple", 10);                              // 같은 키 → 값 덮어쓰기

System.out.println(stock.get("apple"));              // 10
System.out.println(stock.get("cherry"));             // null (없는 키)
System.out.println(stock.getOrDefault("cherry", 0)); // 0
System.out.println(stock.containsKey("banana"));     // true

for (Map.Entry<String, Integer> entry : stock.entrySet()) {
    System.out.println(entry.getKey() + " = " + entry.getValue());
}

같은 키로 put()을 다시 호출하면 새 항목이 추가되지 않고 기존 값이 바뀐다. 없는 키를 get()하면 null이 반환되므로, 기본값이 필요할 때는 getOrDefault()를 쓰는 편이 안전하다. 키와 값을 함께 순회할 때는 entrySet()을 사용한다.

Map에서 자주 쓰는 메서드는 다음과 같다.

메서드 설명
put(K key, V value) 키-값 쌍을 저장한다. 같은 키가 있으면 값을 덮어쓴다.
get(Object key) 키에 해당하는 값을 반환한다. 없으면 null이다.
getOrDefault(Object key, V defaultValue) 키가 없을 때 null 대신 지정한 기본값을 반환한다.
putIfAbsent(K key, V value) 키가 없을 때만 저장한다. 기존 값을 덮어쓰지 않는다.
containsKey(Object key) / containsValue(Object value) 해당 키 / 값이 있는지 확인한다.
remove(Object key) 키에 해당하는 쌍을 삭제한다.
keySet() / values() / entrySet() 키 목록 / 값 목록 / 키-값 쌍 목록을 반환한다. 반복문으로 순회할 때 쓴다.
size() / isEmpty() / clear() 쌍의 개수 / 비어 있는지 여부 / 전체 삭제.

6. Stack과 Queue, Deque

  • Stack: LIFO(Last In First Out, 후입선출) 구조를 갖는 자료구조다. 현재는 Stack 클래스 대신 Deque 인터페이스를 사용하는 것이 권장된다.
  • Queue: FIFO(First In First Out, 선입선출) 구조를 갖는다. 주로 LinkedList나 ArrayDeque로 구현하여 사용한다.
  • Deque (Double Ended Queue): 양쪽 끝에서 요소의 삽입과 삭제가 모두 가능한 구조다. Stack과 Queue의 기능을 모두 수행할 수 있다.

ArrayDeque 하나로 Stack과 Queue를 각각 사용하는 예제다. 같은 순서로 넣어도 꺼내는 메서드에 따라 결과 순서가 달라진다.

// Stack처럼 사용 (LIFO)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println(stack.pop());   // 3
System.out.println(stack.peek());  // 2 (꺼내지 않고 확인만)

// Queue처럼 사용 (FIFO)
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(1);
queue.offer(2);
queue.offer(3);
System.out.println(queue.poll());  // 1
System.out.println(queue.peek());  // 2

push()/pop()은 앞쪽에서 넣고 빼므로 마지막에 넣은 값이 먼저 나오고, offer()/poll()은 뒤에 넣고 앞에서 빼므로 먼저 넣은 값이 먼저 나온다. poll()과 peek()은 비어 있을 때 예외 대신 null을 반환한다.

용도별로 자주 쓰는 메서드를 정리하면 다음과 같다.

용도 넣기 꺼내기 확인만 하기
Stack (Deque) push(e) pop() peek()
Queue offer(e) poll() peek()
Deque 앞쪽 offerFirst(e) pollFirst() peekFirst()
Deque 뒤쪽 offerLast(e) pollLast() peekLast()

Queue에는 같은 역할의 add() / remove() / element()도 있다. 이 메서드들은 실패하거나 비어 있으면 null이나 false 대신 예외를 던진다는 점만 다르다. pop()도 비어 있으면 예외를 던진다.

7. 정리

자바 컬렉션 프레임워크는 상황에 맞는 최적의 데이터 구조를 선택할 수 있게 해준다. 순서가 중요하고 중복 조회가 빈번하다면 List(ArrayList), 중복을 제거해야 한다면 Set(HashSet), 키-값 형태의 매핑 데이터라면 Map(HashMap)을 선택하는 것이 기본 규칙이다.