Java Collections Framework의 약어로
다수의 데이터를 쉽고 효과적으로 처리할 수 있는 표준화된 방법을 제공하는 클래스의 집합을 의미
즉, 데이터를 저장하는 자료 구조와 데이터를 처리하는 알고리즘을 구조화하여 클래스로 구현해 놓은 것
객체지향적 설계(인터페이스, 다형성)를 통해 표준화되어 있어서
사용에 편리하고 재사용성이 높다.
JVM에 최적화된 고성능 구현으로 프로그램 성능 향상
상호 운용성 (상위 인터페이스 타입으로 업캐스팅해서 사용)
새로운 API를 익히고 설계하는 시간이 줄어든다
소프트웨어 재사용을 촉진한다.
JCF를 사용하는 새로운 데이터 구조가 재사용 가능하기 때문 !
같은 이유로 JCF를 사용하는 객체를 활용하여 새로운 알고리즘을 만들어낼 수 있다.
JCF는 크게 Collection 인터페이스와 Map 인터페이스로 나눌 수 있다.
최상위 인터페이스 - Collection
Collection<E>은 모든 컬렉션 클래스의 공통된 기능을 정의하는 최상위 인터페이스
이를 구현하는 주요 인터페이스는 List, Set, Queue 등이 있다.

Queue 인터페이스
FIFO(First-In, First-Out) 방식의 컬렉션으로, 주로 대기열 구조에 사용
■ 구현 클래스
PriorityQueue → 우선순위 큐, 요소의 우선순위에 따라 정렬됨
LinkedList → Queue 인터페이스를 구현하여 큐로 활용 가능
List 인터페이스
Collection인터페이스를 상속받으며, 순서가 있는 데이터의 집합을 다룰 때 사용된다.
중복을 허용하며, 각 요소는 인덱스(index) 로 접근할 수 있다.
List 인터페이스의 특징
✅ 순서 유지 → 삽입한 순서대로 요소가 저장됨
✅ 중복 허용 → 같은 요소를 여러 번 추가 가능
✅ 인덱스를 통한 요소 접근 → get(int index) 사용
✅ 요소 추가, 수정, 삭제 가능
구현체의 종류
ArrayList → 배열 기반, 빠른 검색
LinkedList → 연결 리스트 기반, 빠른 삽입/삭제
Vector → 동기화 지원(쓰레드 안전)
Stack → 후입선출(LIFO) 구조
ArrayList는 Java의 동적 배열(Dynamic Array) 클래스로,List인터페이스를 구현한 컬렉션이다.
내부적으로 배열(Array) 기반이지만, 크기가 자동으로 조절되며 요소를 추가/삭제할 수 있다.
ArrayList의 주요 특징
• 배열 기반 (Random Access 가능)
→ 인덱스(index)로 요소에 빠르게 접근 가능 (O(1))
• 크기가 자동 조절됨 (Dynamic Resizing)
→ 배열 크기를 초과하면 1.5배씩 자동 증가
• 중복 요소 허용
→ 같은 값의 요소를 여러 번 저장 가능
• 삽입/삭제가 느릴 수 있음
→ 중간에 삽입/삭제하면 요소 이동이 필요하여 O(n) 발생
• 비동기 (Thread-Unsafe)
→ 여러 스레드에서 동시에 접근하면 문제 발생 가능 (Vector와 차이점)
| 요소 추가 | add() |
| 요소 접근 | get() |
| 요소 수정 | set() |
| 요소 삭제 | remove() |
| 리스트 크기 확인 | size() |
| 리스트 순회 | for-each, Iterator |
✔ ArrayList는 내부적으로 배열 기반으로 동작하며, 자동으로 크기를 1.5배 증가시킴
✔ 크기가 부족하면 새 배열을 생성 후 기존 데이터를 복사
✔ add()는 일반적으로 O(1) 이지만, 배열 크기 증가 시 O(n) 비용 발생
✔ 초기 크기를 지정하면 grow() 발생을 줄여 성능을 향상시킬 수 있음
크기 증가 방식
초기 크기
- 기본적으로 ArrayList는 초기 크기 10으로 시작합니다.
- ex) new ArrayList<>() 하면 Object[10] 크기의 배열 생성
크기가 부족할 때 증가 방식
- 기본적으로 1.5배(= 기존 크기의 3/2 + 1)씩 증가
- 기존 크기가 n일 때, 새로운 크기 = n * 3/2 + 1
- ex) 요소 개수: 10 | 기존 크기:10 | 새 크기: 10 * 3/2 + 1 = 16
크기 증가 과정의 비용 (Time Complexity)
- 일반적인 add() 연산 → O(1)
- But! 배열이 가득 찰 경우, grow()로 인해 O(n) 발생 (새 배열 생성 및 복사)
- 평균적으로 O(1)에 가까운 성능을 제공하지만, 일부 경우 O(n)이 발생할 수 있음
크기 증가 방식 최적화
1. 초기 용량 지정 (ArrayList(int initialCapacity))
- 초기 크기를 지정하면 grow() 발생 횟수를 줄일 수 있음
- 많은 데이터를 다룰 때 new ArrayList<>(1000)처럼 크기를 미리 설정하면 효율적
ArrayList<String> list = new ArrayList<>(1000); // 1000개의 요소를 저장할 공간 확보
2. trimToSize() 메서드 사용
- 배열 크기를 size()와 동일하게 줄여 메모리 낭비 방지
ArrayList<Integer> list = new ArrayList<>(100);
list.add(10);
list.trimToSize(); // 내부 배열 크기를 요소 개수(1)만큼 줄임
LinkedList는 이중 연결 리스트(Doubly Linked List) 기반으로 동작하는 Java의 List 구현체
ArrayList와 달리 배열 기반이 아니라 연결된 노드(Node) 구조를 사용하여 데이터를 저장
LinkedList의 주요 특징
• 이중 연결 리스트(Doubly Linked List) 기반
→ 각 노드가 이전(previous)과 다음(next) 노드를 가리키는 방식으로 연결됨
• 중간 삽입/삭제가 빠름 (O(1) ~ O(n))
→ 기존 요소를 이동할 필요 없이 참조(포인터)만 변경하면 됨
• 검색(조회)이 느림 (O(n))
→ ArrayList처럼 인덱스로 바로 접근할 수 없고, 앞에서부터 순차적으로 탐색해야 함
• 메모리 사용량이 많음
→ 각 요소가 이전/다음 요소의 참조(포인터)를 저장해야 하므로 메모리 오버헤드 발생
• Queue, Stack으로 활용 가능
→ Deque 인터페이스를 구현하여 양방향 대기열(Queue) 및 스택(Stack)으로 사용 가능
| 요소 추가 | add() |
| 요소 접근 | get() |
| 요소 수정 | set() |
| 요소 삭제 | remove() |
| 리스트 크기 확인 | size() |
| 리스트 검색 | contains(), indexOf() |
ArrayList를 사용하는 경우
- 읽기(검색) 작업이 많을 때 (빠른 검색 속도)
- 요소 추가/삭제가 적을 때 (삽입/삭제 비용이 높음)
- 데이터를 순차적으로 추가/삭제할 때 (add()가 빠름)
- 멀티스레드 환경이 아닐 때 (Vector보다 빠름)
LinkedList를 사용할 때
- 삽입/삭제 작업이 많을 때
- 요소를 중간에 자주 추가/삭제할 때

| 비교 항목 | ArrayList | Vector |
|---|---|---|
| 동기화(Synchronization) | ❌ 지원하지 않음 (비동기) | ✅ 지원 (Thread-Safe) |
| 속도 | 빠름 (멀티스레드 환경에서는 느려질 수 있음) | 느림 (동기화 때문에 오버헤드 발생) |
| 성능 | 멀티스레드 환경이 아니라면 더 빠름 | 동기화로 인해 속도가 느려짐 |
| 데이터 증가 방식 | 기존 크기의 1.5배 증가 | 기존 크기의 2배 증가 |
| 사용 추천 | 단일 스레드 환경 (일반적인 리스트 저장) | 멀티스레드 환경 (동기화가 필요할 때) |
Stack(스택)과 Queue(큐)는 대표적인 선형 자료구조
둘 다 데이터를 순서대로 저장하지만, 요소를 추가(삽입)하고 제거(삭제)하는 방식이 다르다.
Stack(스택) - LIFO (Last In, First Out)
마지막에 들어온 데이터가 가장 먼저 나가는 구조
즉, "후입선출(LIFO, Last In First Out)" 방식
Stack의 특징 및 사용 예시
LIFO(후입선출) 방식 → 가장 최근 데이터가 먼저 제거됨
재귀 함수 호출 기록 저장
웹 브라우저의 "뒤로 가기" 기능
수식 계산(괄호 검사, 후위 표기법 연산 등)
| 연산 | 설명 |
|---|---|
| push(E e) | 스택의 맨 위에 요소 추가 |
| pop() | 스택의 맨 위 요소 제거 및 반환 |
| peek() | 스택의 맨 위 요소 확인 (삭제 X) |
| isEmpty() | 스택이 비어있는지 확인 |
Queue(큐) - FIFO (First In, First Out)
먼저 들어온 데이터가 먼저 나가는 구조
즉, "선입선출(FIFO, First In First Out)" 방식
Queue의 특징 및 사용 예시
FIFO(선입선출) 방식 → 먼저 들어온 데이터가 먼저 나감
프린터 작업 대기열
은행 창구 대기 시스템
네트워크 요청 처리 (패킷 처리, 메시지 큐 등)
| 연산 | 설명 |
|---|---|
| offer(E e) | 큐의 맨 뒤에 요소 추가 |
| poll() | 큐의 맨 앞 요소 제거 및 반환 |
| peek() | 큐의 맨 앞 요소 확인 (삭제 X) |
| isEmpty() | 큐가 비어있는지 확인 |
Set인터페이스
Set은 중복을 허용하지 않고, 순서를 보장하지 않는 컬렉션입니다.
Java의Collection인터페이스를 확장한 인터페이스이며, 집합(Set) 개념을 구현한 자료구조입니다.
Set의 주요 특징
✅ 중복 요소를 허용하지 않음 → 같은 요소를 여러 번 추가해도 하나만 저장됨
✅ 요소의 순서를 유지하지 않음 → (단, LinkedHashSet은 삽입 순서를 유지)
✅ 빠른 검색 속도 (HashSet 사용 시 O(1))
✅ null 값을 하나만 저장 가능
구현체의 종류
HashSet → 해시 기반, 빠른 검색
LinkedHashSet → 삽입 순서 유지
TreeSet → 정렬된 상태 유지 (이진 탐색 트리 기반)
set은 기본적으로 중복을 허용하지 않는 자료구조이기 때문에, 중복된 요소가 자동으로 제거된다.
중복 요소가 걸러지는 원리
set은 내부적으로 해시 테이블을 사용하여 요소를 저장한다.
각 요소는 고유한 해시 값(hash value)을 가지며, 같은 해시 값을 가진 요소는 하나만 유지된다.
따라서 중복된 값이 추가되더라도 기존 값과 동일한 해시 값을 가지므로 한 번만 저장된다.
Map 계열 (Collection을 직접 구현하지 않음)
Map은 키(Key)와 값(Value) 쌍을 저장하는 컬렉션으로,
데이터를 효율적으로 저장하고 검색할 수 있는 인터페이스이다.
Collection인터페이스를 직접 상속하지 않지만 JCF의 중요한 부분이다.
List나Set과 달리 키(Key)를 기준으로 값을 저장하며, 키는 중복될 수 없고 값은 중복 가능하다.
Java의Map은java.util.Map인터페이스를 기반으로 여러 가지 구현체를 제공한다.
■ 구현 클래스
HashMap → 가장 많이 사용됨, 키의 순서를 보장하지 않음, 검색 속도가 빠름 (O(1))
LinkedHashMap → 입력 순서를 유지
TreeMap → 키를 정렬된 순서(오름차순)로 저장, 검색 성능이 O(log n)
Hashtable → HashMap과 비슷하지만, 스레드 안전(Thread-Safe)
HashMap은 해시 테이블(Hash Table) 기반의 자료구조로, 키-값을 저장하며 빠르게 데이터를 검색할 수 있다.
데이터를 저장할 때 해시 함수(Hash Function) 를 사용하여 키를 특정 위치(배열 인덱스)로 매핑하고,
이를 통해 O(1)의 시간 복잡도로 데이터를 검색할 수 있다.
HashMap의 동작 과정
1. 데이터 저장 과정 (put(K key, V value))
- 해시 값(hashCode) 계산
- 해시 버킷 결정
- 충돌 해결 (Chaining 방식 사용)
Map<String, Integer> map = new HashMap<>();
map.put("apple", 100); // "apple"의 해시값을 계산하여 적절한 위치에 저장
map.put("banana", 200); // 같은 방식으로 저장
2. 데이터 조회 과정 (get(K key))
- 해시 값(hashCode) 계산
- 해시 버킷(index) 찾기
- 키가 일치하는지 확인
System.out.println(map.get("apple")); // 100 (빠르게 검색)
3. 해시 충돌(Hash Collision) 해결
해시 충돌이 발생하면 연결 리스트(Linked List)로 데이터를 저장하고,
충돌이 심하면 트리(Tree)로 변환함.
HashMap의 최악의 경우 발생 조건
최악의 시간 복잡도가 발생하는 주요 이유는 해시 충돌(Hash Collision) 때문이다.
해시 충돌이 심하면 연결 리스트(Linked List)로 요소가 저장되면서
검색 및 삭제 성능이 O(n)까지 저하될 수 있다!!!
해시 충돌이 심한 경우
- HashMap은 내부적으로 키(key)의 hashCode() 값을 배열의 특정 인덱스(bucket)에
매핑해서 저장함.
- 하지만 여러 개의 키가 같은 해시 버킷(Index)에 충돌하면 하나의 버킷에 여러 개의
요소가 저장됨.
- 이 경우 연결 리스트로 저장되며, 리스트를 순차적으로 탐색해야 하므로
검색 및 삭제 연산이 O(n) 이 됨.
그치만 좋은 hashCode() 함수를 사용하고, 적절한 초기 크기 및 Load Factor를 설정하면
성능이 개선된다 ^__^
Map의 동작 방식과 Collection의 구조가 근본적으로 다르기 때문 !!
Collection과 Map의 근본적인 차이점
1. Collection은 단순한 요소(Element)의 집합
Collection 인터페이스를 구현한 주요 컬렉션(List, Set, Queue 등)은 "값(value)"의 모음을 다루는 구조
- List: 순서가 있는 값의 집합 (중복 허용)
- Set: 중복을 허용하지 않는 값의 집합
- Queue: FIFO(선입선출) 방식으로 동작하는 값의 집합
즉, Collection을 구현하는 클래스들은 단일 요소를 저장하고 관리하는 것이 목적이다 !
2. Map은 키-값(Key-Value)의 쌍을 저장하는 자료구조
Map은 단순한 요소(Element)의 집합이 아니라, (key, value) 형태의 쌍을 저장
- 키(key)는 중복될 수 없음.
- 값(value)는 중복될 수 있지만, 키가 동일하면 값이 덮어씌워짐.
- 요소를 추가하거나 삭제할 때 key를 통해 접근함.
📌 즉, Map은 Collection과는 다른 방식으로 데이터를 저장하고 관리하므로,
Collection 인터페이스를 상속받지 않음.
Iterable
: 반복할 수 있는 객체(컬렉션)를 의미
for-each문에서 사용할 수 있도록Iterator를 생성할 수 있음
Iterable의 특징
- Iterable 인터페이스를 구현하면 해당 객체는 반복 가능한 객체가 됨
- Iterable을 구현한 객체는 Iterator를 생성할 수 있어야 함
- 대표적인 구현 클래스: List, Set, Queue 등
Iterable 인터페이스의 구조
public interface Iterable<T> {
Iterator<T> iterator(); // Iterator 객체를 반환
}
→ iterator() 메서드를 구현하면 for-each 문에서 사용할 수 있음.
Iterator
: 실제 반복을 수행하는 객체
요소를 하나씩 가져오고, 탐색을 진행할 수 있는 메서드를 제공
Iterator의 특징
- Iterator는 컬렉션의 요소를 하나씩 순차적으로 가져오는 역할
- hasNext(), next(), remove() 등의 메서드를 제공
Iterator 인터페이스의 구조
public interface Iterator<T> {
boolean hasNext(); // 다음 요소가 있는지 확인
T next(); // 다음 요소 반환
void remove(); // 현재 요소 삭제 (선택적)
}
🔹 차이점 정리

Collection (인터페이스)
Collection은 Java에서 모든 컬렉션(List, Set, Queue 등)의 상위 인터페이스이다.
컬렉션(데이터를 저장하는 자료구조)을 정의하는 기본 인터페이스로,
여러 컬렉션 클래스(ArrayList,HashSet등)가 이를 구현한다.
Collection의 주요 특징
- 인터페이스이므로 객체 생성 불가 (new Collection<>() ❌)
- 여러 컬렉션(List, Set, Queue)의 공통 기능을 정의하는 역할
- Collection을 구현한 대표적인 클래스:
List 계열 → ArrayList, LinkedListSet 계열 → HashSet, TreeSetQueue 계열 → PriorityQueue, LinkedListCollections (유틸리티 클래스)
Collections는 컬렉션을 다룰 때 유용한 정적(static) 메서드들을 모아놓은 클래스이다.
컬렉션을 정렬, 검색, 동기화, 변경하는 등의 기능을 제공한다.
Collections의 주요 특징
- 컬렉션을 다루는 유틸리티 메서드 모음 (객체 생성 불가)
- 모든 메서드는 static이므로 클래스 이름으로 직접 호출 가능
- sort(), reverse(), shuffle(), binarySearch() 같은 도움 메서드 제공
- synchronizedList(), unmodifiableList() 같은 동기화 및 불변 리스트 생성 기능 제공

1. Thread 클래스를 상속받아 스레드 생성
Thread 클래스를 상속하고 run() 메서드를 오버라이드하는 방법
class MyThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 실행 중: " + i);
try {
Thread.sleep(1000); // 1초 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 스레드 시작
}
}
// 실행 결과
Thread 실행 중: 0
Thread 실행 중: 1
Thread 실행 중: 2
...
🔹 장점
- Thread 클래스를 직접 확장하여 간단하게 구현 가능.
🔹 단점
- 이미 다른 클래스를 상속받고 있다면 Thread를 상속할 수 없음. (Java는 단일 상속만 지원)
2. Runnable 인터페이스를 구현하여 스레드 생성
Runnable 인터페이스를 구현하고 Thread 객체에 전달하는 방식
class MyRunnable implements Runnable {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runnable 실행 중: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 스레드 시작
}
}
// 실행 결과
Runnable 실행 중: 0
Runnable 실행 중: 1
Runnable 실행 중: 2
...
🔹 장점
- 다른 클래스를 상속받아도 Runnable을 구현할 수 있어서 유연함.
- Thread와 Runnable을 분리하여 객체 지향적으로 설계 가능.
🔹 단점
- Thread를 상속하는 방식보다 약간 더 코드가 길어질 수 있음.
스레드 풀(Thread Pool) 은 미리 일정 개수의 스레드를 생성해 두고,
작업이 들어오면 기존 스레드를 재사용하는 방식이다.
즉, 새로운 작업이 요청될 때마다 새로운 스레드를 생성하는 대신,
기존의 스레드를 재활용해서 성능을 최적화하는 기법이다.
✅ 핵심 개념
왜 스레드 풀을 사용할까?
1️⃣ 스레드 생성 비용 절감
→ new Thread()를 호출하면 새로운 스레드를 생성하는데,
이는 CPU, 메모리, OS 자원 소모가 큼
→ 스레드 풀이 있으면 기존 스레드를 재사용하므로 성능이 향상됨
2️⃣ 과도한 스레드 생성 방지
→ 스레드를 무제한 생성하면 메모리 부족(OutOfMemoryError) 또는 성능 저하 발생 가능
→ 스레드 풀은 최대 개수를 제한하여 이러한 문제를 방지함
3️⃣ 병렬 작업 효율적 관리
→ 대량의 작업을 동시에 처리해야 할 때, 스레드 풀이 효율적으로 스레드 분배를 수행함
4️⃣ 안정적인 스레드 관리
→ 스레드가 많아지면 관리가 어려워짐. 하지만 스레드 풀을 사용하면
중앙에서 스레드를 관리할 수 있어 유지보수가 편리함
Context Switching에도 불구하고 스프링 프레임워크에서 수백 개의 스레드를 운영하는 이유
→ 스레드가 많아지면 Context Switching(문맥 전환) 으로 인해 성능이 저하될 수 있음에도,
Spring과 같은 프레임워크는 수백 개 이상의 스레드 풀을 운영하는 경우가 많다.
이는 대부분의 웹 애플리케이션이 I/O 작업이 많은 비동기 시스템이기 때문이다.
즉, CPU 바운드 작업이 아닌, I/O 바운드 작업을 수행하기 때문에
많은 스레드를 두는 것이 더 효율적이다!
■ CPU 바운드 vs I/O 바운드
CPU 바운드 작업
CPU 연산을 많이 사용하는 작업 (예: 복잡한 수학 연산, 이미지 처리, AI 모델 학습)
보통 CPU 코어 개수만큼 스레드를 두는 것이 최적
ex) newFixedThreadPool(Runtime.getRuntime().availableProcessors())
I/O 바운드 작업
네트워크 통신, DB 쿼리, 파일 읽기/쓰기 등 I/O 대기 시간이 긴 작업
스레드가 CPU를 쓰지 않고 대기하는 시간이 많음
CPU가 놀지 않도록 많은 스레드를 만들어 다른 요청을 동시에 처리하는 것이 더 효율적
→ 웹 애플리케이션(서버)는 대부분 I/O 바운드이므로,
수백 개의 스레드를 운영하는 것이 효과적!
■ Context Switching 비용보다 스레드 활용도가 더 높다
스레드가 많아지면 Context Switching 비용이 증가할 수 있음
하지만 웹 애플리케이션에서는 한 스레드가 DB 응답을 기다리거나 API 호출을 기다리는 경우가 많음
즉, 대부분의 시간은 대기 상태이므로 CPU 리소스를 거의 사용하지 않음
이때 다른 스레드가 실행될 수 있음
📌 결론
CPU 바운드 작업 → Context Switching 비용이 크므로 적은 스레드 유지
I/O 바운드 작업 (웹 애플리케이션) → 스레드가 대기하는 시간이 많아 수백 개 스레드 운영 가능