Optimal Solution
[자료구조] ArrayList와 Vector 본문
ArrayList와 Vector를 학습하기 전에
https://optimal-solution.tistory.com/entry/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-Array%EC%99%80-List
[자료구조] Array와 List
Array, List를 학습하기 전에 일반적으로 '선형 자료구조'를 떠올리면, Array(배열)와 List(리스트)가 있다. 컴퓨터공학의 자료구조 과목에서 가장 먼저 배우는 자료구조 두 가지가 바로 '배열'과 '링크
optimal-solution.tistory.com
이전 포스트에서 Array와 List를 다루면서, Array와 LinkedList, ArrayList라는 3가지 자료구조를 다뤘다. 그런데, 나는 이전에 객체지향 프로그래밍을 학습하면서 (뜬금없게 객체지향 프로그래밍 언급을 하고 있네) Java의 Stack과 Vector의 관계를 통해 상속이 항상 좋은 것만은 아니라는 것을 깨달은 경험이 있다(이와 관련해서는 추후 포스트를 한 번 작성 ... 할 수 있다면 해보겠다 ?). 그 때 당시의 나는 Java에 익숙한 사람이 아니었고, 코딩테스트는 그 때나 지금이나 C++을 사용하는 사람으로서 Java에도 C++처럼 Vector가 존재한다는 사실을 알게 되었다 !!!
사실 내가 ArrayList를 학습하면서, Java의 ArrayList는 C++의 vector와 비슷하다고 느꼈다. 그렇다면 Java에서는 ArrayList와 Vector가 어떻게 다른 것일까 ? 라는 의문이 생겼고, 마침 옛날에 학과 친구들과 함께 진행했던 스터디에서 이 주제를 다루기도 했기 때문에 ArrayList와 Vector를 비교해보는 포스트를 작성해보겠다.
ArrayList
저번 포스트에서 설명했듯이, ArrayList는 내부적으로 Array를 가지고 있어 인덱스를 통해 데이터에 접근할 수 있으면서도 size와 capacity라는 특성을 활용해 List처럼 그 크기를 가변적으로 활용할 수 있는 장점을 가진 자료구조이다.
capacity 이상으로 데이터를 저장할 경우에는 capacity를 1.5배로 만들어 내부 Array의 크기를 증가시키고, 그 빈 공간에 데이터를 추가하는 방식으로 동작한다. 그리고, 저번 포스트에서 다루지 않은 내용이 있는데 ArrayList는 synchronized를 지원하지 않는다.
synchronized
OS를 공부하거나 Java, Kotlin과 같은 언어를 공부하다 보면, '동기화' 라는 키워드를 굉장히 자주 접할 수 있다. 멀티 스레드 환경에서는 한 데이터에 여러 스레드가 동시에 접근해 조작하게 될 경우 개발자의 의도와 다르게 동작할 수 있는데, Java에서는 이를 보완하고자 다양한 동기화 기법을 제공한다(동기화에 관한 내용 역시 언젠가 블로그에서 다루게 될 주제 ... 가 될 것이라고 생각한다). 그 중에서도 synchronized 키워드는, 해당 키워드가 붙은 메서드나 블록에 한 번에 하나의 스레드만 진입하도록 보장하는 키워드이다.
예시를 들어보도록 하겠다. 내 은행 계좌 잔고에 100만원이 있는데, 엄마가 10만원을 나에게 용돈으로 보내줌과 동시에 내가 15만원짜리 키보드를 구매했다. 그렇다면 아래 둘 중 하나의 과정으로 수행이 될 것이다.
100만원 -> 10만원 입금 -> 110만원
110만원 -> 15만원 출금 -> 95만원
100만원 -> 15만원 출금 -> 85만원
85만원 -> 10만원 입금 -> 95만원
아무리 두 가지 일이 동시에 일어난다 한들, 2번의 거래 이후 내 계좌 잔고는 95만원이다. 그래야 정상적이다. 하지만 스레드, 비동기의 세계에서는 이런 문제가 발생할 수 있다.
100만원 -> 10만원 입금 -> 15만원 출금 -> 110만원으로 업데이트 -> 85만원으로 업데이트
10만원이 입금된 후, 110만원으로 계좌 잔고를 업데이트해야 한 후 15만원을 출금해 95만원이 되어야 하는데, 동시에 두 가지 일이 진행되면서 한 쪽의 처리 결과가 반영되지 않은 것이다 !!! 원래대로라면 10만원 입금을 처리한 후에 15만원 출금을 처리하던가, 15만원 출금을 처리한 후 10만원 입금을 처리해야 하는데 '동기화' 가 되지 않은 것이다.
따라서 이러한 상황을 생각해보면, 우리는 이런 생각을 해볼 수 있다.
'계좌 잔고를 변동시킬 때, 값을 읽어와서 -> 조작하고 -> 값을 쓰는 과정은 하나의 작업으로 인식해 이 사이에 다른 작업을 동시에 수행하지 않도록 하면 문제가 해결되지 않을까 ?'
이 개념이 바로 synchronized이다. 동기화 개념은 단순히 Java에만 국한된 것이 아니고, OS에서도, DB에서도, Android 개발에서도 많은 개발자들의 머리를 아프게 하는 문제이다. 그래서 분명 별도의 글로 정리해야 하지만, 일단 우리의 관심사는 ArrayList와 Vector이니까 대강 이 정도만 이해해도 충분하다.
ArrayList는 synchronized를 지원하지 않는다
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<Integer>();
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
arrayList.add(i);
}
}).start();
new Thread(() -> {
for (int i = 10000; i < 20000; i++) {
arrayList.add(i);
}
}).start();
new Thread(() -> {
try {
Thread.sleep(2000);
for (int i = 0; i < arrayList.size(); i++) {
System.out.println(i + " : " + arrayList.get(i));
}
System.out.println("ArrayList size() : " + arrayList.size());
} catch (Exception ignored) {
}
}).start();
}
}
위 코드는 ArrayList에 동시에 2개의 스레드가 접근해 0부터 19,999까지의 값을 삽입하는 코드이다. 처음 2개의 스레드가 각각 1만 개의 데이터를 ArrayList에 추가하고, 마지막 스레드에서는 두 스레드의 동작을 보장하기 위해 2초간 sleep한 후 ArrayList의 모든 값과 size를 출력한다.
당연히 size 2만이 나올 것이라 예상하지만, 놀랍게도 (안 놀랍다면 죄송) 그렇지 않다.
이러한 결과가 나온 이유는, 바로 ArrayList는 synchronized를 지원하지 않기 때문이다. 앞서 설명한 은행 계좌 잔고 업데이트 이슈와 같은 문제가 발생한 것이다.
Vector는 synchronized를 지원한다
Vector는 ArrayList와 달리 synchronized를 지원한다.
import java.util.Vector;
public class Test {
public static void main(String[] args) {
Vector<Integer> vector = new Vector<Integer>();
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
vector.add(i);
}
}).start();
new Thread(() -> {
for (int i = 10000; i < 20000; i++) {
vector.add(i);
}
}).start();
new Thread(() -> {
try {
Thread.sleep(2000);
for (int i = 0; i < vector.size(); i++) {
System.out.println(i + " : " + vector.get(i));
}
System.out.println("Vector size() : " + vector.size());
} catch (Exception ignored) {
}
}).start();
}
}
분명 ArrayList와 같은 코드인데, 다른 결과가 나온다.
ArrayList와 Vector의 또 다른 차이점
ArrayList는 capacity를 초과해 데이터를 삽입하면, 1.5배씩 capacity를 증가시킨다. 하지만 Vector는 capacity를 2배씩 증가시킨다.
또한 Vector는 레거시로 분류되지만, ArrayList는 그렇지 않고, ArrayList는 Iterator만 지원하지만 Vector는 Iterator와 Enumeration 둘 다 지원한다.
이런 자잘한 차이도 있지만, 사실 가장 중요한 것은 synchronized의 차이라고 생각한다.
Iterator vs Enumeration
사실 다루지 말까 생각했지만, 이왕 하는 거 다뤄본다.
Iterator와 Enumeration 둘 다 Collection에서 데이터를 순차적으로 접근하기 위해 사용할 때 쓰인다.
하지만 Iterator는 읽고, 지우는 것이 가능하지만 Enumeration은 지우는 것은 불가능하고 읽는 것만 가능하다.
또한, Enumeration은 Collection에서도 레거시로 분류되는 Vector나 Hashtable에서만 사용 가능하다.
추가적으로 Fail-Fast와 Fail-Safe라는 개념도 존재하는데, 나는 우아한테크캠프를 할 때 이 문제를 경험해본 적이 있어 이 개념을 다룬 흥미로운 블로그를 읽었다. 그래서 링크를 첨부한다.
https://misoboy.tistory.com/25
Enumeration & Iterator
Enumeration : 자바 초기버젼에서 개발된 것으로, HashTable과 Vector에서 사용가능 Iterator : jdk 1.2버젼에서 개발된 것으로, Collection 인터페이스를 구현한 모든 클래스에서 사용가능. API를 확인해볼까요??
misoboy.tistory.com
마무리
ArrayList와 Vector에 대해 다루면서, synchronized나 Iterator vs Enumeration과 같은 흥미로운 주제들도 함께 탐구할 수 있었다. 가장 중요한 핵심적인 포인트는 synchronized라는 것을 다시 한 번 강조하면서도, 레거시로 분류되는 Vector에 대해 알아보면서 과거 내가 개발하면서 마주했던 에러의 원인을 지금에서야 찾은 것 같아 뭔가 굉장히 기분이 좋아진 상태로 글을 마무리한다. 그 에러가 뭐였는지는 딱히 여기다가 쓰진 않을 건데, 혹시 내 면접 들어오시는 면접관께서 이 글을 읽고 물어봐주신다면 신나서 20분은 떠들 수 있을 것 같긴 하다.
'CS' 카테고리의 다른 글
[자료구조] 스택(Stack)과 큐(Queue) (1) | 2023.10.21 |
---|---|
[자료구조] 해시(Hash)와 해시 테이블(Hash Table) (1) | 2023.10.15 |
[자료구조] Array와 List (0) | 2023.10.14 |