본문으로 건너뛰기

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>

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

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