본문으로 건너뛰기

11.1 컬렉션 프레임웍 소개 (Collections Framework Overview)

안내: 이 문서는 Java 21 버전을 기준으로 작성되었습니다.

배열(Array)은 여러 개의 데이터를 한 곳에 모아 관리하는 가장 기본적인 도구입니다. 하지만 배열에는 치명적인 단점이 있습니다. 크기를 처음에 반드시 고정해야 한다 는 점입니다. 만약 10명의 학생 이름을 저장하려고 배열을 만들었는데, 나중에 11번째 학생이 들어온다면? 배열을 통째로 새로 만들어야 하는 불편함이 생깁니다.

자바는 이런 불편함을 해결하기 위해 크기가 자유롭게 늘어나고 줄어드는 동적(Dynamic) 데이터 구조들 을 표준 라이브러리로 제공합니다. 이것을 컬렉션 프레임웍(Collections Framework) 이라고 부릅니다.

1. 컬렉션 프레임웍이란?

컬렉션 프레임웍은 데이터를 저장, 조회, 수정, 삭제하는 공통 기능을 인터페이스와 클래스들로 표준화해둔 거대한 라이브러리 체계입니다.

java.util 패키지 안에 있으며, 실제로 자바 개발에서 가장 많이 사용하는 도구들 중 하나입니다.

컬렉션 프레임웍의 핵심 가치
  • 표준화: 모든 컬렉션이 공통 인터페이스를 구현하므로 코드 일관성이 유지됩니다.
  • 재사용성: 검증된 자료구조를 직접 구현할 필요 없이 바로 사용합니다.
  • 성능: 각 구현체는 해당 자료구조에 최적화된 알고리즘을 내장하고 있습니다.
  • 타입 안전성: 제네릭(Generics)과 결합하여 컴파일 시점에 타입 오류를 잡아냅니다.

2. 컬렉션 계층 구조

컬렉션 프레임웍의 인터페이스 계층 구조를 이해하는 것이 핵심입니다.

Iterable<E>
└── Collection<E>
├── List<E> 순서 있음, 중복 허용
│ ├── ArrayList<E>
│ ├── LinkedList<E>
│ └── Vector<E>
├── Set<E> 순서 없음, 중복 불허
│ ├── HashSet<E>
│ ├── LinkedHashSet<E>
│ └── TreeSet<E>
└── Queue<E> FIFO(선입선출) 구조
├── LinkedList<E>
├── PriorityQueue<E>
└── Deque<E>

Map<K,V> (Collection을 상속하지 않는 별도 계층)
├── HashMap<K,V>
├── LinkedHashMap<K,V>
└── TreeMap<K,V>

MapCollection을 상속하지 않는 독립 계층이라는 점을 주목하세요.

3. 핵심 인터페이스 비교

컬렉션 프레임웍의 최상위에는 4가지 핵심 인터페이스가 있습니다.

인터페이스순서중복특징대표 구현 클래스
ListOO인덱스로 접근 가능ArrayList, LinkedList
SetXX고유한 값만 저장HashSet, TreeSet
QueueOOFIFO(선입선출)LinkedList, PriorityQueue
MapX키X/값O키-값 쌍으로 저장HashMap, TreeMap

4. 언제 무엇을 쓸까?

목록처럼 순서를 지켜 데이터를 쭉 저장하고 싶다    → List (ArrayList)
중복 없이 유일한 값들만 모은 집합이 필요하다 → Set (HashSet)
딕셔너리처럼 키(Key)로 값(Value)을 바로 찾고 싶다 → Map (HashMap)
먼저 들어온 것이 먼저 나가야 하는 대기열이 필요하다 → Queue (LinkedList)
자동으로 정렬된 상태를 유지해야 한다 → TreeSet, TreeMap

5. 제네릭(Generics)과 타입 안전성

컬렉션은 항상 제네릭과 함께 사용합니다. <> 꺾쇠 괄호 안에 저장할 타입을 지정하면 컴파일러가 잘못된 타입을 넣는 실수를 미리 잡아줍니다.

import java.util.ArrayList;

public class GenericSafety {
public static void main(String[] args) {
// 제네릭 없이 사용 (Raw Type) - 위험!
ArrayList rawList = new ArrayList();
rawList.add("문자열");
rawList.add(123); // 다른 타입도 들어가 버림
rawList.add(3.14);
// 꺼낼 때 형변환 필요 → ClassCastException 위험
String s = (String) rawList.get(1); // 런타임 에러!

// 제네릭 사용 - 안전!
ArrayList<String> safeList = new ArrayList<>();
safeList.add("자바");
safeList.add("스프링");
// safeList.add(123); // 컴파일 에러! String만 허용
String result = safeList.get(0); // 형변환 불필요
System.out.println(result); // 자바
}
}
Raw Type 사용 금지

제네릭 타입을 지정하지 않는 Raw Type(ArrayList, HashMap 등)은 Java 5 이전 코드와의 호환성을 위해 남아 있지만, 현대 코드에서는 절대 사용하지 마세요. 항상 ArrayList<String> 처럼 타입을 명시하세요.

6. 배열 vs 컬렉션 비교

배열과 컬렉션의 차이를 실제 코드로 비교해봅니다.

import java.util.ArrayList;
import java.util.Arrays;

public class ArrayVsCollection {
public static void main(String[] args) {
// ========== 배열 ==========
String[] arr = new String[3]; // 크기를 반드시 먼저 지정
arr[0] = "사과";
arr[1] = "바나나";
arr[2] = "딸기";
// arr[3] = "포도"; // ArrayIndexOutOfBoundsException!
System.out.println("배열 크기: " + arr.length); // 3
System.out.println("배열 내용: " + Arrays.toString(arr)); // [사과, 바나나, 딸기]

// ========== ArrayList ==========
ArrayList<String> list = new ArrayList<>(); // 크기 지정 불필요
list.add("사과");
list.add("바나나");
list.add("딸기");
list.add("포도"); // 자유롭게 추가 가능!
list.remove("바나나"); // 값으로 삭제
System.out.println("리스트 크기: " + list.size()); // 3
System.out.println("리스트 내용: " + list); // [사과, 딸기, 포도]

// ========== 배열 → 리스트 변환 ==========
String[] fruitsArr = {"망고", "키위", "복숭아"};
ArrayList<String> fromArray = new ArrayList<>(Arrays.asList(fruitsArr));
System.out.println("변환된 리스트: " + fromArray); // [망고, 키위, 복숭아]

// ========== 리스트 → 배열 변환 ==========
String[] toArray = fromArray.toArray(new String[0]);
System.out.println("변환된 배열: " + Arrays.toString(toArray)); // [망고, 키위, 복숭아]
}
}
비교 항목배열 (Array)컬렉션 (ArrayList 등)
크기고정 (변경 불가)동적 (자동 조절)
타입원시 타입 포함 가능객체 타입만 (래퍼 클래스 사용)
성능빠름약간 느림 (오버헤드)
기능기본 기능만풍부한 메서드 제공
다차원지원 (int[][])중첩 컬렉션으로 구현

7. Comparable vs Comparator

컬렉션의 정렬 기능을 사용할 때 두 가지 인터페이스가 등장합니다.

Comparable (자기 자신과 비교)

Comparable<T> 인터페이스를 구현하면 객체의 기본 정렬 기준 을 정의합니다.

import java.util.ArrayList;
import java.util.Collections;

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

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

@Override
public int compareTo(Student other) {
// 점수 오름차순 정렬 (음수: 나 < other, 양수: 나 > other)
return this.score - other.score;
}

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

public class ComparableExample {
public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>();
students.add(new Student("김철수", 85));
students.add(new Student("이영희", 92));
students.add(new Student("박민준", 78));

Collections.sort(students); // Comparable의 compareTo 사용
System.out.println(students); // [박민준(78), 김철수(85), 이영희(92)]
}
}

Comparator (외부에서 비교 기준 제공)

Comparator<T> 인터페이스를 구현하면 다양한 정렬 기준 을 외부에서 유연하게 제공할 수 있습니다.

import java.util.ArrayList;
import java.util.Comparator;

public class ComparatorExample {
public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>();
students.add(new Student("김철수", 85));
students.add(new Student("이영희", 92));
students.add(new Student("박민준", 78));

// 이름 기준 오름차순 정렬
students.sort(Comparator.comparing(s -> s.name));
System.out.println(students); // [김철수(85), 박민준(78), 이영희(92)]

// 점수 기준 내림차순 정렬
students.sort((a, b) -> b.score - a.score);
System.out.println(students); // [이영희(92), 김철수(85), 박민준(78)]
}
}
Comparable vs Comparator 선택 기준
  • Comparable: 클래스에 자연스러운 기본 정렬 순서가 있을 때 (예: 점수가 높을수록 우선)
  • Comparator: 상황에 따라 여러 정렬 기준이 필요할 때, 또는 수정할 수 없는 외부 클래스를 정렬할 때

8. Iterator와 for-each 순회

모든 컬렉션은 Iterable<E> 인터페이스를 구현하므로 for-each 루프 로 순회할 수 있습니다.

import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class IterationExample {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("사과");
list.add("바나나");
list.add("딸기");

// 1. for-each 루프 (가장 간결, 권장)
System.out.println("=== for-each ===");
for (String fruit : list) {
System.out.println(fruit);
}

// 2. Iterator 사용 (순회 중 삭제가 필요할 때 안전)
System.out.println("=== Iterator ===");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String fruit = it.next();
if (fruit.equals("바나나")) {
it.remove(); // 순회 중 안전하게 삭제
} else {
System.out.println(fruit);
}
}
System.out.println("삭제 후: " + list); // [사과, 딸기]

// 3. 인덱스 기반 for 루프 (List만 가능)
System.out.println("=== 인덱스 for ===");
for (int i = 0; i < list.size(); i++) {
System.out.println(i + ": " + list.get(i));
}

// 4. Set 순회 (순서 보장 없음)
HashSet<String> set = new HashSet<>();
set.add("Java");
set.add("Python");
set.add("Go");
for (String lang : set) {
System.out.println(lang); // 순서는 매번 다를 수 있음
}

// 5. Map 순회
HashMap<String, Integer> map = new HashMap<>();
map.put("국어", 95);
map.put("수학", 88);
map.put("영어", 92);

// entrySet으로 키-값 쌍 순회
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
순회 중 삭제 주의

for-each 루프 안에서 직접 list.remove()를 호출하면 ConcurrentModificationException이 발생합니다. 순회 중 삭제는 반드시 Iterator.remove()를 사용하거나, removeIf() 메서드를 활용하세요.

9. 실전 예제: 배열 vs ArrayList 학생 명단 관리

import java.util.ArrayList;
import java.util.Collections;

public class StudentManagement {

// 배열을 사용한 방식 (구식, 불편)
static void withArray() {
System.out.println("=== 배열 방식 ===");
String[] students = new String[5]; // 최대 5명 고정
int count = 0;

students[count++] = "김철수";
students[count++] = "이영희";
students[count++] = "박민준";

// 삭제 후 앞으로 당기는 작업 필요
String target = "이영희";
for (int i = 0; i < count; i++) {
if (students[i].equals(target)) {
for (int j = i; j < count - 1; j++) {
students[j] = students[j + 1];
}
students[--count] = null;
break;
}
}

System.out.print("학생 목록: ");
for (int i = 0; i < count; i++) {
System.out.print(students[i] + " ");
}
System.out.println();
}

// ArrayList를 사용한 방식 (현대적, 간결)
static void withArrayList() {
System.out.println("=== ArrayList 방식 ===");
ArrayList<String> students = new ArrayList<>();

students.add("김철수");
students.add("이영희");
students.add("박민준");
students.add("최지은"); // 마음껏 추가

System.out.println("초기 목록: " + students);

// 삭제: 한 줄로 끝
students.remove("이영희");
System.out.println("삭제 후: " + students);

// 정렬: 한 줄로 끝
Collections.sort(students);
System.out.println("정렬 후: " + students);

// 검색
System.out.println("박민준 있음? " + students.contains("박민준")); // true
System.out.println("이영희 있음? " + students.contains("이영희")); // false

// 총 학생 수
System.out.println("총 학생 수: " + students.size());
}

public static void main(String[] args) {
withArray();
System.out.println();
withArrayList();
}
}

출력 결과:

=== 배열 방식 ===
학생 목록: 김철수 박민준

=== ArrayList 방식 ===
초기 목록: [김철수, 이영희, 박민준, 최지은]
삭제 후: [김철수, 박민준, 최지은]
정렬 후: [김철수, 박민준, 최지은]
박민준 있음? true
이영희 있음? false
총 학생 수: 3

10. 불변 컬렉션 (Java 9+)

Java 9부터 List.of(), Set.of(), Map.of()수정 불가능한 불변 컬렉션 을 간단하게 만들 수 있습니다.

import java.util.List;
import java.util.Set;
import java.util.Map;

public class ImmutableCollections {
public static void main(String[] args) {
// 불변 List
List<String> immutableList = List.of("사과", "바나나", "딸기");
System.out.println(immutableList); // [사과, 바나나, 딸기]
// immutableList.add("포도"); // UnsupportedOperationException!

// 불변 Set
Set<Integer> immutableSet = Set.of(1, 2, 3, 4, 5);
System.out.println(immutableSet.contains(3)); // true

// 불변 Map
Map<String, Integer> immutableMap = Map.of("국어", 95, "수학", 88);
System.out.println(immutableMap.get("국어")); // 95

// 불변 컬렉션으로 가변 컬렉션 초기화
java.util.ArrayList<String> mutableList = new java.util.ArrayList<>(immutableList);
mutableList.add("포도"); // 이건 가능
System.out.println(mutableList); // [사과, 바나나, 딸기, 포도]
}
}
불변 컬렉션 활용
  • 상수로 정의하는 목록(예: 요일 이름, 국가 코드 등)
  • 메서드에서 내부 데이터를 외부에 안전하게 공개할 때
  • 멀티스레드 환경에서 데이터 무결성 보장

정리: 컬렉션 선택 가이드

앞으로의 챕터에서 각 인터페이스의 대표 구현 클래스(ArrayList, HashSet, HashMap 등)를 하나씩 자세히 배워보겠습니다.

상황선택
순서 있는 목록, 인덱스 접근 필요ArrayList
앞뒤로 추가/삭제가 잦은 목록LinkedList
중복 제거, 존재 여부 확인HashSet
중복 제거 + 삽입 순서 유지LinkedHashSet
중복 제거 + 자동 정렬TreeSet
키-값 쌍, 빠른 조회HashMap
키-값 쌍 + 삽입 순서 유지LinkedHashMap
키-값 쌍 + 키 자동 정렬TreeMap
선입선출(FIFO) 대기열LinkedList (Queue로 사용)
우선순위 기반 정렬 대기열PriorityQueue