본문으로 건너뛰기
김신건의 로그

[Java] Vector

· 수정 · 📖 약 3분 · 1,227자/단어 #java #collection #data-structure #list #legacy #synchronized
Vector, java.util.Vector, Java Vector, 벡터, Vector class

정의

java.util.VectorArrayList 와 거의 동일한 동적 배열 기반 List 이지만, 모든 public 메서드가 synchronized 로 묶여 있다. Java 1.0 (1996) 부터 존재하는 레거시 컬렉션.

JDK 1.2 의 Collections Framework 도입 시 List 인터페이스에 retrofit 됐으며, 같은 인터페이스를 구현하는 ArrayList 가 등장했다. 새 코드에서는 거의 쓰지 않는다.

사용 상황

Vector 는 새 코드에서 쓰지 않는다. 하지만 다음 상황에서 마주치게 된다.

상황비고
레거시 코드 유지보수JDK 1.0-1.1 시절 코드, 90년대 후반 ~ 2000년대 초
Stack<E> 사용 코드StackVector 를 상속
AWT/Swing API 내부JTable, JList 모델 일부가 Vector 사용
API 반환값서드파티 라이브러리가 반환하는 경우

이 파일은 Vector 를 왜 쓰지 않는지, 그리고 마주쳤을 때 어떻게 처리하는지를 이해하기 위한 것이다.

시각화

내부 구조

public class Vector<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    protected Object[] elementData;
    protected int elementCount;
    protected int capacityIncrement;   // 0 이면 두 배씩 증가

    public synchronized boolean add(E e) { ... }
    public synchronized E get(int index) { ... }
    public synchronized E remove(int index) { ... }
    public synchronized int size() { ... }
    // 거의 모든 메서드에 synchronized
}

ArrayList 와의 두 가지 구조적 차이.

  1. capacityIncrement: 0 일 때 두 배씩 확장 (ArrayList 는 1.5 배). 양수로 지정하면 그 값만큼 고정 증가.
  2. 모든 메서드가 synchronized: 메서드 호출마다 this 의 모니터 락을 획득. size() 조차 락을 건다.

주요 연산 비용

복잡도 자체는 ArrayList 와 동일. 매 호출마다 락 acquire/release 가 더해진다.

메서드시간추가 비용
get(int i)O(1)락 acquire/release
add(E e)amortized O(1)락 acquire/release
add(int i, E e)O(n)락 acquire/release
remove(int i)O(n)락 acquire/release
size()O(1)락 acquire/release

단일 스레드 환경이라면 락은 무경합 (uncontended) 이라 비용이 매우 작지만, ArrayList 대비 측정 가능한 오버헤드가 있다. 멀티스레드 경합 시 모니터 진입 비용이 크다.

synchronized 메서드 락 흐름

flowchart TD
    T1["스레드 1: v.add(x)"] --> L1{"this 락 획득"}
    T2["스레드 2: v.get(0)"] --> L2{"this 락 획득"}
    L1 -->|"성공"| Op1["add 실행"]
    L2 -->|"락 보유 중"| Wait["대기 (BLOCKED)"]
    Op1 --> Rel1["락 해제"]
    Rel1 --> L2Retry["스레드 2 재시도"]
    L2Retry -->|"성공"| Op2["get 실행"]
    Op2 --> Rel2["락 해제"]
    Wait -.-> L2Retry

각 메서드가 this 모니터를 잠근다. addget 도 동시에 실행 불가.

메서드별 동기화의 한계

각 메서드 호출은 atomic 하지만, 복합 연산 은 race condition 의 여지가 그대로 남는다.

Vector<Integer> v = new Vector<>();

// 각 메서드는 atomic 이지만 두 호출 사이에 다른 스레드가 끼어들 수 있음
if (!v.contains(42)) {
    v.add(42);   // 다른 스레드가 그 사이에 42 를 넣었을 수 있음
}

// 외부에서 명시적 락으로 묶어야 안전
synchronized (v) {
    if (!v.contains(42)) {
        v.add(42);
    }
}

CAUTION

“Vector 가 thread-safe 하다” 는 오해다. 단일 메서드는 thread-safe 지만, 두 개 이상의 메서드를 묶는 일관성은 호출자가 따로 보장해야 한다. 진정한 동시성 컬렉션은 CopyOnWriteArrayList 또는 ConcurrentHashMap 같은 java.util.concurrent 의 컬렉션.

iterator 의 fail-fast

Vector 의 iterator 도 fail-fast iterator. 순회 도중 다른 스레드가 add/remove 하면 ConcurrentModificationException. 락으로 외부 동기화를 해야 안전한 순회가 가능하다.

synchronized (v) {
    for (Integer x : v) {
        process(x);   // 이 블록 안에서는 다른 스레드 진입 불가
    }
}

락 없이 순회하면 CME 가 발생할 수 있다. v.get(i) 인덱스 순회도 size 가 변하면 AIOOBE.

Stack 과의 관계

java.util.Stack<E>Vector<E>상속 한다. 레거시 코드에서 Stack 을 발견하면 Vector 의 모든 동작이 함께 온다는 뜻이다.

// 레거시: Stack은 Vector를 상속, 모든 List 메서드 노출됨
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.pop();
stack.get(0);    // 인덱스 접근도 가능 (스택의 의도와 다름)
stack.add(0, 99);  // 스택 중간에 삽입도 가능!

// Java 현대 권장: Deque 인터페이스 + ArrayDeque 구현
Deque<Integer> stack2 = new ArrayDeque<>();
stack2.push(1);
stack2.pop();
// stack2.get(0);  컴파일 에러, Deque 는 인덱스 접근 없음

ArrayDeque 기반 Deque 가 Stack 역할을 더 명확하게 표현하고 성능도 우수하다.

ArrayList vs Vector

항목ArrayListVector
동기화없음모든 메서드에 synchronized
도입 시점JDK 1.2 (1998)JDK 1.0 (1996)
용량 증가1.5 배2 배 (또는 capacityIncrement)
메서드 호출 비용작음+ 락 비용
복합 연산 안전성외부 동기화 필요여전히 외부 동기화 필요
새 코드 권장

왜 새 코드에서 쓰지 않는가

  1. 단일 스레드 라면 락이 불필요한데도 비용을 낸다.
  2. 다중 스레드 라도 메서드별 락만으로는 복합 연산이 안전하지 않다. 결국 외부 동기화가 필요하다.
  3. 진짜 동시성 컬렉션 (CopyOnWriteArrayList, ConcurrentLinkedQueue 등) 이 훨씬 우수하다.
  4. 레거시 설계 잔재: JDK 가 retrofit 했으나 이상적인 컬렉션 설계가 아니다.

대안: 상황별 선택

flowchart TD
    Goal["List 가 필요"] --> MT{"멀티스레드?"}
    MT -->|"아니오"| AL["ArrayList"]
    MT -->|"예"| RW{"읽기/쓰기 비율?"}
    RW -->|"읽기 압도적"| CoW["CopyOnWriteArrayList"]
    RW -->|"균형 or 쓰기 多"| SL["Collections.synchronizedList\nor ConcurrentLinkedDeque"]
    MT -->|"스택 용도"| Deq["Deque + ArrayDeque"]
의도대안
단일 스레드 ListArrayList
외부 동기화 ListCollections.synchronizedList(new ArrayList<>())
읽기 多, 쓰기 거의 없음CopyOnWriteArrayList
동시 큐/덱ConcurrentLinkedDeque, LinkedBlockingDeque
스택Deque<E> stack = new ArrayDeque<>()

레거시 Vector 마이그레이션

레거시 코드에서 Vector 를 마주쳤을 때 점진 교체 전략.

// 기존 레거시
Vector<String> v = new Vector<>();
v.add("a");
String s = v.get(0);

// 1단계: 변수 타입을 List 로 추상화 (컴파일 체크)
List<String> list = new Vector<>();   // 구현체는 그대로

// 2단계: 실제 구현체 교체 (단일 스레드라면)
List<String> list2 = new ArrayList<>();

// 3단계: 멀티스레드라면 상황에 맞는 concurrent 컬렉션으로
// 읽기 多: CopyOnWriteArrayList
// 쓰기 多: Collections.synchronizedList(new ArrayList<>()) + 복합연산 외부 락

capacityIncrement 의 미묘한 차이

ArrayList 는 grow 할 때 현재 크기의 1.5 배 로 늘리지만, Vector 는 capacityIncrement 가 0 이면 두 배 로 늘린다.

// capacityIncrement = 0 (기본)
Vector<Integer> v1 = new Vector<>();          // 초기 capacity 10, 초과 시 20으로
Vector<Integer> v2 = new Vector<>(10, 5);     // 초기 10, 초과 시 5씩 증가

// ArrayList: 초기 10, 초과 시 15 -> 22 -> 33 ... (1.5배)
ArrayList<Integer> a = new ArrayList<>();

두 배 성장은 메모리 낭비가 클 수 있고, 고정 증가(capacityIncrement > 0)는 잦은 재할당이 발생할 수 있다. ArrayList 의 1.5 배 전략이 일반적으로 더 균형 잡혀 있다.

Enumeration: Vector 전용 레거시 API

Vector 는 iterator() 외에 레거시 Enumeration<E> 도 지원한다.

Vector<String> v = new Vector<>(List.of("a", "b", "c"));

// 레거시 API (JDK 1.0)
Enumeration<String> e = v.elements();
while (e.hasMoreElements()) {
    System.out.println(e.nextElement());
}

// 현대 방식
for (String s : v) { ... }
v.forEach(System.out::println);

Enumeration 은 fail-fast 가 아니다. 순회 중 수정해도 CME 가 발생하지 않고, 대신 정의되지 않은 동작이 일어날 수 있다. 레거시 Enumeration 코드를 유지보수할 때 주의.

관련 위키

이 글의 용어 (10개)
[Java] ArrayListjava
정의 는 동적 배열 (dynamic array) 로 구현된 . 내부적으로 를 들고 다니며, 용량이 부족해지면 새 배열로 옮긴다. Java 의 가장 흔한 컬렉션 구현체. 인덱스 기…
[Java] Collectionjava
정의 는 그룹으로 묶인 객체들을 표현하는 최상위 인터페이스. JCF (Java Collections Framework) 의 입구이자, / / / 모두 이를 확장한다. 자체는 직접…
[Java] ConcurrentHashMapjava
정의 는 고동시성 환경에서 사용 가능한 구현. JSR-166 (Java 5) 도입, Java 8 에서 내부 구조가 크게 재작성됐다. 과 같은 인터페이스를 제공하면서 thread-…
[Java] ConcurrentModificationExceptionjava
정의 (CME) 는 fail-fast iterator 가 컬렉션의 구조 변경을 탐지했을 때 던지는 unchecked 예외 ( ). 이름과 달리 반드시 동시성 (멀티스레드) 문제일…
[Java] CopyOnWriteArrayListjava
정의 는 쓰기 시 배열 전체를 복사 하는 thread-safe 구현. 읽기에는 lock 이 전혀 없고, 쓰기에는 으로 직렬화한다. (JSR-166) 의 컬렉션. 읽기 압도적 다,…
[Java] Fail-Fast Iteratorjava
정의 Fail-Fast Iterator 는 순회 도중 컬렉션의 구조가 변경되면 즉시 을 던지는 iterator. , , 등 대부분의 컬렉션의 iterator 가 이 방식. 목적은…
[Java] Iterablejava
정의 는 루프로 순회 가능한 모든 타입의 최상위 인터페이스. 단 하나의 추상 메서드, 를 정의한다. 인터페이스가 을 extends 하므로 , , , 등 모든 컬렉션이 자동으로 대…
[Java] Listjava
정의 는 순서가 있고 중복이 허용 되는 컬렉션을 표현하는 인터페이스. 각 원소는 0 부터 시작하는 정수 인덱스로 접근 가능. 인터페이스를 확장하며, 의 모든 성질을 물려받는다. …
[Java] Objectjava
정의 는 Java 의 모든 클래스의 최상위 부모 (root) 클래스. 가 명시되지 않은 클래스는 컴파일러가 자동으로 를 붙인다. 인터페이스는 클래스가 아니라 를 직접 상속하지는 …
[Java] ReentrantLockjava
정의 는 키워드와 같은 상호 배제 (mutual exclusion) 를 제공하는 클래스 기반 락. JSR-166 (Java 5) 에서 추가됐다. "재진입 (reentrant)" …

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기