Vector와 Stack — 레거시 스레드 안전 컬렉션
Vector와 Stack의 역사·내부 구조·synchronized 문제, Stack이 List를 상속하는 설계 결함, 그리고 현대 Java에서 ArrayDeque와 Collections.synchronizedList로 대체하는 실전 마이그레이션 가이드
지난 글에서 LinkedList의 이중 연결 리스트 구조를 살펴봤다. 이번에는 Java 1.0 시절부터 존재하는 **Vector와 Stack**을 다룬다. 두 클래스는 지금도 표준 라이브러리에 남아 있어 레거시 코드에서 종종 만날 수 있지만, 현대 Java 개발에서는 더 나은 대안으로 교체하는 것이 권장된다.
Vector — 최초의 동기화된 List
Vector는 Java 1.0에서 도입된 동적 배열 구현체다. Java 1.2에서 컬렉션 프레임워크가 도입될 때 AbstractList를 구현하도록 개조되어 List 인터페이스를 만족하게 됐다.
import java.util.Vector;
Vector<String> v = new Vector<>();
v.add("apple");
v.add("banana");
v.add("cherry");
System.out.println(v.get(0)); // "apple"
System.out.println(v.size()); // 3
System.out.println(v.capacity()); // 기본 10, 꽉 차면 2배 확장
ArrayList와 달리 Vector는 모든 공개 메서드에 synchronized가 붙어 있다. 단일 스레드 환경에서도 매 호출마다 락 획득/해제 비용이 발생한다.
// Vector 내부 (간략화)
public synchronized boolean add(E e) { ... }
public synchronized E get(int index) { ... }
public synchronized int size() { ... }
public synchronized void clear() { ... }
용량(capacity) 확장 전략도 ArrayList와 다르다. ArrayList는 현재 크기의 1.5배로 확장하지만, Vector는 capacityIncrement 파라미터가 0이면 현재 용량의 2배로 확장한다. 메모리를 더 많이 낭비하는 경향이 있다.
Stack — Vector를 상속하는 설계 실수
Stack은 Vector를 상속(extends Vector)하는 방식으로 구현되어 있다. 이 설계는 심각한 결함이다.
import java.util.Stack;
Stack<Integer> stack = new Stack<>();
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println(stack.peek()); // 30 (제거 없이 최상단 확인)
System.out.println(stack.pop()); // 30 (제거)
System.out.println(stack.empty()); // false (10, 20 남음)
System.out.println(stack.search(10)); // 2 (1이 top, 2가 그 아래)
Stack이 Vector를 상속하기 때문에 List의 모든 메서드가 그대로 노출된다. 인덱스로 중간 원소에 접근하거나 add(index, e)로 스택 중간에 삽입하는 것도 문법적으로 가능하다 — 스택의 LIFO 캡슐화가 완전히 깨진다.
Stack<Integer> stack = new Stack<>();
stack.push(1); stack.push(2); stack.push(3);
// 스택임에도 List 메서드 전부 노출
stack.add(1, 99); // 인덱스 1에 삽입 — 스택 의미 위반
stack.remove(0); // 바닥 원소 제거 — 스택 의미 위반
System.out.println(stack.get(0)); // List 방식 접근
현대적 대안
단일 스레드 스택: ArrayDeque
import java.util.ArrayDeque;
import java.util.Deque;
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // addFirst
stack.push(2);
stack.push(3);
System.out.println(stack.peek()); // 3
System.out.println(stack.pop()); // 3
// Deque 인터페이스 타입을 쓰므로 get(0) 등 List 메서드가 없음
ArrayDeque는 동기화가 없어 단일 스레드에서 더 빠르다. 또한 Deque 인터페이스 타입으로 선언하면 LIFO 연산만 노출되어 캡슐화가 보장된다.
스레드 안전 List: Collections.synchronizedList
Vector를 다중 스레드 환경에서 List로 쓰던 코드를 마이그레이션할 때:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
// Vector 대체: 동기화 래퍼
List<String> syncList =
Collections.synchronizedList(new ArrayList<>());
// 주의: 복합 연산은 별도로 동기화해야 함
synchronized (syncList) {
if (!syncList.contains("x")) {
syncList.add("x");
}
}
더 높은 동시성이 필요하다면 CopyOnWriteArrayList(읽기 다수·쓰기 드문 경우) 또는 ConcurrentLinkedQueue를 고려한다.
Vector의 Enumeration
레거시 코드에서 Vector를 Enumeration으로 순회하는 패턴을 볼 수 있다:
Vector<String> v = new Vector<>(List.of("a", "b", "c"));
// 레거시 방식
java.util.Enumeration<String> e = v.elements();
while (e.hasMoreElements()) {
System.out.println(e.nextElement());
}
// 현대 방식 (Iterator 또는 향상 for문)
for (String s : v) {
System.out.println(s);
}
마이그레이션 요약
| 레거시 | 현대 대체 | 이유 |
|---|---|---|
new Vector<>() | new ArrayList<>() | 단일 스레드, 더 빠름 |
new Vector<>() (멀티스레드) | Collections.synchronizedList(new ArrayList<>()) | 명시적 동기화 |
new Stack<>() | new ArrayDeque<>() | LIFO 캡슐화, 더 빠름 |
v.elements() | for-each / Iterator | 현대 순회 API |
Vector와 Stack은 Java 표준 라이브러리에서 공식적으로 deprecated 처리되어 있지 않지만, Javadoc에 “use ArrayList instead”, “use ArrayDeque instead”라는 권고가 명시되어 있다.
지난 글: LinkedList — 이중 연결 리스트의 구조와 실전 활용
다음 글: HashSet — 해시 기반 중복 없는 컬렉션
읽어주셔서 감사합니다. 😊