자바를 배운 지 얼마 안 됐을 때, “멀티스레드 환경에서 안전한 리스트가 필요하면 Vector를 쓰면 된다”는 말을 들어본 적 있으신가요? 그런데 막상 현업에 나와보면 Vector를 쓰는 코드를 거의 볼 수 없습니다. Java Vector 동기화 방식이 처음에는 훌륭해 보이지만, 실제 서비스 환경에서는 심각한 성능 문제와 구조적 결함을 드러내기 때문입니다. 이 글에서는 왜 Vector가 외면받는지, 그리고 지금 당장 무엇으로 교체해야 하는지를 단계별로 풀어드립니다.
목차
- Java Vector 클래스란 무엇인가 — 기초 개념 정리
- Vector의 동기화 원리 — 메커니즘 해부
- Vector를 쓰면 어떤 이점이 있었나 — 등장 배경과 긍정적 역할
- 왜 실무에서 사라졌나 — 동기화 이슈와 성능 함정
- Vector를 대체하는 실전 단계별 선택법
- 전문가 권고 및 검증된 대안 도구 총정리
1. Java Vector 클래스란 무엇인가 — 기초 개념 정리
Java의 Vector는 1998년 Java 1.0 시절부터 존재해 온 아주 오래된 컬렉션 클래스입니다. java.util 패키지에 속하며, 내부적으로 배열(array)을 기반으로 요소를 동적으로 저장할 수 있는 가변 크기 리스트입니다. 쉽게 말해 “크기가 자동으로 늘어나는 배열”이라고 이해하면 됩니다.
Vector의 기본 구조
Vector는 AbstractList를 상속하고 List 인터페이스를 구현합니다. 그래서 ArrayList와 외형적으로 거의 동일한 API를 제공합니다. 요소 추가(add), 제거(remove), 조회(get) 등 기본적인 리스트 연산을 모두 지원하며, 내부 배열이 꽉 차면 자동으로 두 배 크기로 확장(기본 capacity 증가 방식)합니다.
javaVector<String> vector = new Vector<>(); vector.add("사과"); vector.add("바나나"); vector.add("체리"); System.out.println(vector.get(0)); // 사과
위 코드는 ArrayList로 작성한 것과 거의 차이가 없어 보입니다. 그러나 내부 동작 방식에서 결정적인 차이가 있습니다.
ArrayList와의 표면적 차이
항목VectorArrayList최초 도입Java 1.0 (1998)Java 1.2 (1998년 말)동기화 여부모든 메서드에 기본 동기화동기화 없음크기 확장 방식현재 크기 × 2현재 크기 × 1.5상위 인터페이스ListList현재 권장 여부✗ 비권장 (레거시)✓ 권장
Vector가 ArrayList보다 6개월 정도 먼저 나왔지만, 자바 팀은 컬렉션 프레임워크를 설계하면서 Vector의 구조적 문제를 인식하고 ArrayList를 새롭게 설계했습니다. 두 클래스가 공존하게 된 것은 하위 호환성 때문이지, Vector가 더 좋아서가 아닙니다.
2. Vector의 동기화 원리 — 메커니즘 해부
Vector의 가장 큰 특징은 모든 공개 메서드(public method)에 synchronized 키워드가 붙어 있다는 점입니다. synchronized는 자바에서 “한 번에 하나의 스레드만 이 코드 블록을 실행할 수 있다”는 것을 보장하는 장치입니다. 이를 모니터 락(monitor lock) 또는 인트린식 락(intrinsic lock) 이라고 부릅니다.
메서드 레벨 동기화의 실제 동작
Vector의 add() 메서드 소스를 보면 다음과 같습니다.
java// java.util.Vector 내부 구현 (단순화) public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; } public synchronized E get(int index) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); return elementData(index); }
add()와 get() 모두 synchronized가 붙어 있습니다. 즉, 스레드 A가 add()를 실행하는 동안 스레드 B는 get()도 실행하지 못하고 대기해야 합니다. 읽기 작업조차도 락을 요구한다는 점이 중요한 포인트입니다.
락의 범위와 한계
여기서 결정적인 문제가 발생합니다. synchronized는 메서드 단위로만 걸립니다. 즉, 개별 메서드 호출은 원자적(atomic)이지만, 여러 메서드를 조합한 복합 연산은 원자적이지 않습니다.
java// 위험한 코드 예시 — 멀티스레드 환경에서 안전하지 않음 if (!vector.isEmpty()) { // 락 획득 → 해제 vector.remove(0); // 락 획득 → 해제 }
스레드 A가 isEmpty()를 실행해서 “비어있지 않다”는 결과를 받고 remove(0)을 호출하려는 그 찰나에, 스레드 B가 마지막 요소를 지워버리면? ArrayIndexOutOfBoundsException이 발생합니다. Vector가 동기화를 제공함에도 불구하고 복합 연산에서는 여전히 경쟁 조건(race condition)이 발생합니다.
3. Vector를 쓰면 어떤 이점이 있었나 — 등장 배경과 긍정적 역할
Vector가 처음 등장했을 때는 분명히 혁신적인 클래스였습니다. 1990년대 후반, 자바가 막 산업 현장에 도입되던 시기에는 멀티스레드 프로그래밍 자체가 매우 어려운 영역이었습니다. 개발자가 직접 락을 관리하고 스레드 안전성을 보장하는 코드를 작성하는 것은 전문가에게도 버거운 일이었습니다.
당시 Vector가 해결한 문제들
당시 Vector는 다음 세 가지 문제를 한꺼번에 해결해주었습니다.
첫째, 배열 크기 자동 관리. 기존의 정적 배열(int[])은 크기를 미리 선언해야 했고, 이를 초과하면 직접 더 큰 배열을 만들어 복사해야 했습니다. Vector는 이 과정을 자동화했습니다.
둘째, 스레드 안전성 제공. 개발자가 별도로 synchronized 블록을 작성하지 않아도, 메서드 호출만으로 기본적인 스레드 안전성이 보장되었습니다. 초보자도 큰 실수 없이 멀티스레드 코드를 작성할 수 있게 해준 셈입니다.
셋째, 표준 API 제공. Enumeration 인터페이스를 통해 요소를 순회할 수 있었고, 이는 당시로서는 꽤 진보된 설계였습니다.
교육적 가치로서의 Vector
오늘날에도 Vector는 동기화 개념을 가르치기 위한 교보재로 종종 활용됩니다. “모든 메서드에 락을 걸면 어떤 일이 벌어지는가”를 실습하기에 이보다 명확한 예제가 없기 때문입니다. 그러나 교육 목적 이외의 실무 코드에서는 등장해서는 안 됩니다.
4. 왜 실무에서 사라졌나 — 동기화 이슈와 성능 함정
이제 핵심입니다. Java Vector 동기화 방식은 왜 실무에서 독이 되었을까요? 크게 세 가지 이유가 있습니다.
이유 1 — 불필요한 락으로 인한 성능 저하
Vector는 단일 스레드 환경에서도 모든 메서드에 락을 겁니다. 락을 획득하고 해제하는 것 자체가 CPU 자원을 소비하는 작업입니다. 이를 락 오버헤드(lock overhead) 라고 합니다.
실제 벤치마크(JMH, Java Microbenchmark Harness 기준)에서 단일 스레드 환경의 ArrayList와 Vector를 비교하면, 단순 add() 연산만으로도 ArrayList가 30~50% 이상 빠른 결과를 보입니다. 동기화가 전혀 필요 없는 상황에서도 락 비용을 지불해야 하는 것입니다.[단순 벤치마크 결과 예시 — 1,000만 요소 add()] ArrayList: ~220ms Vector: ~370ms → Vector가 약 68% 더 느림
이유 2 — 복합 연산에서 안전하지 않음 (앞에서 설명한 race condition)
2절에서 살펴봤듯이, 메서드 단위 동기화는 복합 연산의 원자성을 보장하지 않습니다. 이는 Vector를 쓰면 스레드 안전하다는 착각을 심어주어, 오히려 더 위험한 코드를 작성하게 만드는 원인이 됩니다.
안전하게 사용하려면 결국 외부에서 추가로 synchronized 블록을 작성해야 합니다.
java// Vector를 안전하게 쓰려면 결국 이렇게 해야 함 synchronized (vector) { if (!vector.isEmpty()) { vector.remove(0); } }
이 상황이 되면, Vector가 제공하는 내부 동기화는 완전히 무의미해집니다. 이미 외부 락으로 보호하고 있으니까요. 이중 락(double locking) 이 발생하여 성능은 더 나빠지고, 코드 복잡도는 올라갑니다.
이유 3 — 상속 구조의 설계 결함
Vector를 상속한 Stack 클래스도 같은 문제를 안고 있으며, Vector 자체가 AbstractList를 상속하면서 상속을 잘못 활용한 설계의 전형적인 예시가 되었습니다. 자바 창시자 중 한 명인 조슈아 블로크(Joshua Bloch)는 저서 Effective Java에서 이 설계를 명시적으로 비판하며, Vector와 Stack 모두 사용을 피할 것을 권고했습니다.
또한 Vector의 Enumeration 기반 순회는 Iterator와 달리 fail-fast 동작을 완전히 지원하지 않아, 순회 중 컬렉션이 변경될 때 예상치 못한 버그가 생길 수 있습니다.
5. Vector를 대체하는 실전 단계별 선택법
그렇다면 지금 당장 어떤 클래스를 써야 할까요? 상황에 따라 최적의 선택이 다릅니다.
STEP 1 — 단일 스레드 환경이라면: ArrayList
멀티스레드를 전혀 고려할 필요가 없다면, 무조건 ArrayList가 정답입니다. Vector와 동일한 API를 제공하면서 락 오버헤드가 없어 훨씬 빠릅니다. 기존 Vector 코드를 ArrayList로 바꾸는 것은 클래스 이름만 변경하면 될 정도로 단순합니다.
java// Before (레거시) Vector<String> list = new Vector<>(); // After (권장) List<String> list = new ArrayList<>();
STEP 2 — 멀티스레드 + 읽기 많고 쓰기 적다면: CopyOnWriteArrayList
java.util.concurrent 패키지의 CopyOnWriteArrayList는 쓰기 발생 시 내부 배열 전체를 복사하는 방식으로 스레드 안전성을 구현합니다. 읽기 작업에는 락이 전혀 없어 매우 빠릅니다. 이벤트 리스너 목록, 설정 값 목록처럼 “거의 변경되지 않는 리스트”에 최적입니다.
javaList<String> list = new CopyOnWriteArrayList<>(); list.add("항목1"); // 쓰기 시 배열 복사 발생 String item = list.get(0); // 락 없이 빠른 읽기
단점: 쓰기가 빈번하면 배열 복사 비용이 매우 커집니다. 대용량 데이터에 쓰기가 많은 경우는 적합하지 않습니다.
STEP 3 — 멀티스레드 + 쓰기도 빈번하다면: Collections.synchronizedList 또는 ConcurrentLinkedQueue
java// 방법 1: 외부 동기화 래핑 List<String> syncList = Collections.synchronizedList(new ArrayList<>()); // 방법 2: 큐 구조가 필요하다면 Queue<String> queue = new ConcurrentLinkedQueue<>();
Collections.synchronizedList()는 ArrayList를 감싸서 동기화를 추가합니다. Vector와 달리 복합 연산 시 명시적으로 외부 락을 사용해야 한다는 것을 코드 구조상 강제하여, 개발자가 락의 필요성을 인식하게 만든다는 장점이 있습니다.
STEP 4 — 생산자-소비자 패턴이라면: BlockingQueue
멀티스레드 환경에서 데이터를 주고받는 생산자-소비자 패턴에는 LinkedBlockingQueue 또는 ArrayBlockingQueue가 최선입니다.
javaBlockingQueue<String> queue = new LinkedBlockingQueue<>(100); // 생산자 스레드 queue.put("데이터"); // 큐가 가득 차면 자동 대기 // 소비자 스레드 String data = queue.take(); // 큐가 비면 자동 대기
6. 전문가 권고 및 검증된 대안 도구 총정리
공식 문서와 업계 표준의 일관된 메시지
Oracle 공식 Java 문서는 Vector를 “레거시 클래스(legacy class)”로 분류하고, 동기화가 필요하지 않은 경우에는 ArrayList를, 스레드 안전이 필요한 경우에는 java.util.concurrent 패키지를 활용할 것을 명시적으로 권고합니다.
조슈아 블로크(Joshua Bloch) 의 Effective Java (3판, 아이템 81 등)에서는 “불필요한 동기화를 피하라”고 강조하며, Vector처럼 모든 메서드에 동기화를 거는 방식이 얼마나 비효율적인지를 구체적인 예와 함께 설명합니다.
Google의 Java 스타일 가이드와 SonarQube (정적 코드 분석 도구)에서도 Vector 사용 시 코드 스멜(code smell) 또는 경고(warning)로 분류합니다.
상황별 최적 클래스 요약표
상황권장 클래스이유단일 스레드, 일반 용도ArrayList가장 빠름, 락 없음멀티스레드, 읽기 위주CopyOnWriteArrayList읽기 무락, 안전멀티스레드, 읽기+쓰기 혼합Collections.synchronizedList명시적 제어 가능생산자-소비자 패턴LinkedBlockingQueue대기/알림 자동화고성능 비차단 큐ConcurrentLinkedQueue락프리 알고리즘❌ 어떤 상황에서도Vector레거시, 비권장
추천 학습 및 점검 도구
- IntelliJ IDEA 내장 인스펙션: Vector 사용 시 자동 경고 및 리팩토링 제안 제공
- SonarQube / SonarLint: 코드 품질 분석, Vector 사용을 코드 스멜로 탐지
- JMH (Java Microbenchmark Harness): 컬렉션 성능 직접 측정 가능
- 공식 레퍼런스: Java SE 공식 문서 — java.util.concurrent
결론
Java Vector 동기화 방식은 1990년대에는 혁신이었지만, 오늘날에는 성능 저하와 복합 연산의 안전성 미보장이라는 두 가지 치명적인 한계로 인해 실무에서 완전히 퇴출되었습니다. 단일 스레드라면 ArrayList, 멀티스레드라면 java.util.concurrent패키지의 클래스를 용도에 맞게 선택하는 것이 현대 자바 개발의 표준입니다. 지금 운영 중인 프로젝트에 Vector가 남아 있다면, 오늘 바로 리팩토링 계획을 세워보세요.
답글 남기기