개요
프로그래머스에서 스택 관련 문제를 풀다가 익명의 분께서 Java의 Stack 클래스는 사용을 지양하는 게 좋다는 글을 읽었다.
Stack 자료구조는 매우 유명한 자료구조이고, 그걸 구현한 Stack 클래스를 사용하지 말라니?
의문점이 들어 Stack 클래스에 대해 자세히 조사하게 되었다.
Stack 자료구조에 대해서
스택(Stack) 자료구조는 LIFO(Last In First Out)의 자료구조이다.
이 말의 의미는 가장 나중에 삽입(push)된 데이터가 가장 먼저 삭제(pop)된다는 뜻이다.
또한 Stack 자료구조는 정해진 방향으로만 데이터를 쌓을 수 있으며,
맨 위의 데이터(top)에 대해서만 접근이 가능해야 한다.
Java의 Stack 클래스의 문제점
Java의 Stack 클래스는 Vector 클래스를 상속받은 클래스이다.
바로 그 점이 Stack 클래스의 문제점이다.
엥? 단순히 상속받은 게 문제가 된다고?
그렇다.
이유는 Vector는 Array와 같이 인덱스(index)로 접근하는 걸 허용하는 클래스이기 때문이다.
1. LIFO 위반, 데이터 쌓는 방향 위반
우리가 위에서 잠깐 본 Stack 자료구조에 대한 설명으로는
분명 정해진 방향으로만 데이터를 쌓을 수 있고, 맨 위의 데이터(top)에 대해서만 접근이 가능해야 된다고 했다.
하지만 Vector를 상속받은 Stack은
add(), insertElementAt() 메서드 등을 통해서 특정 인덱스에 데이터를 삽입할 수 있을 뿐더러,
remove(), removeElementAt() 메서드 등을 통해서 특정 인덱스의 데이터를 삭제도 가능하다.
Stack<String> stack = new Stack<>();
stack.add("data1");
stack.add("data2");
stack.add(0, "data3"); //인덱스 위치에 삽입 가능
stack.insertElementAt("data4", 0); //인덱스 위치에 삽입 가능
for(String str: stack){
System.out.println(str);
}
System.out.println();
stack.remove(0); //인덱스 위치 삭제 가능
stack.removeElementAt(0); //인덱스 위치 삭제 가능
for(String str: stack){
System.out.println(str);
}
이는 LIFO를 명백히 위반하는 것이며, 정해진 방향으로 데이터를 쌓는 것도 아니다.
2. 맨 위의 데이터에만 접근 가능해야 한다는 규칙 위반
또한 get() 메서드를 통해서 특정 인덱스에 접근할 수 있으므로,
맨 위의 데이터(top)에만 접근가능해야 한다는 것도 위반하게 된다.
Stack<String> stack = new Stack<>();
stack.add("data1");
stack.add("data2");
stack.add("data3");
System.out.println(stack.get(0));
즉, Stack 클래스는 Vector 클래스를 상속받음으로써 Vector 클래스의 메서드를 사용할 수 있으며,
그로 인해 원래의 자료구조 Stack으로써 지켜야 할 많은 룰들을 위반하게 된다.
3. 성능 상의 문제 - 생성자
Stack 클래스는 심지어 하나의 생성자만 지원하는데, 이는 용량의 기본값을 10으로 설정한다.
만약 데이터가 많은 상황일경우, 용량을 늘리는 과정이 반복되므로 성능 상의 저하가 발생할 것이다.
왜 Java에서는 Stack 클래스를 개선하지 않을까?
사실 Stack 클래스는 Vector 클래스와 함께 Java에서 더 이상 성능 개선을 하지 않는 '레거스 클래스'이다.
Java의 하위 호환을 위해서 남아있는 것이며,
java docs에서도 Stack 대신 다른 클래스를 사용할 것을 권장한다.
Stack의 대안
만약 LIFO의 특징을 가진 자료구조를 사용하고 싶다면, Deque 인터페이스를 사용하는 것이 좋다.
push(e), pop(), peek() 등의 메서드로 이를 지원한다.
1. 단일 스레드 환경
Deque 인터페이스의 구현체인 ArrayDeque나 LinkedList를 사용하는 것이 좋다.
- ArrayDeque: 자체 메모리 소모량이 적으며, iterator의 효율이 좋음
- LinkdeList: 스택의 size의 변동성이 큰 경우 사용, 즉각적인 메모리 공간 확보가 가능
2. 멀티 스레드 환경
LinkedBlockingDeque, ConcurrentLinkedDeque를 사용하기를 권장한다.
- LinkedBlockingDeque:
- 스택의 size가 제한적일 경우 사용
- 큐가 가득 차거나 비어 있을 때 스레드를 차단
- 한 번의 단일 스레드에서만 데이터 접근 허용
- ConcurrentLinkedDeque:
- 여러 스레드에서의 접근이 가능(Non-blocking)하면서도
- 효율적인 lock-free 알고리즘(CAS, Compare-And-Swap)을 사용해 우선 순위가 낮은 스레드가 락을 점유하는 상황이나 데드락에 걸리는 상황을 막아준다.
Reference
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Vector.html
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Stack.html
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Deque.html
'Language > Java' 카테고리의 다른 글
[Java][Collections Framework 완전 정복] Map 인터페이스, Map.Entry 인터페이스 (0) | 2025.01.07 |
---|---|
[Java][Collections Framework 완전 정복] Set 인터페이스(HashSet) (0) | 2025.01.07 |
[Java][캐스팅] int[] <-> ArrayList (1) | 2025.01.07 |