프로젝트를 진행하다가 Last In First Out과 같은 자료구조를 활용한 경험이 있다.
그래서 Stack을 사용하려고 했는데, 얼핏 누군가가 나에게 "Java Stack은 지양해주세요." 라는 말을 했던 것 같다..
Java에서 Stack 사용을 왜 지양해야하는지, 공부했던 내용을 공유하고자 한다.
Stack은 무엇인가요?
스택에 대한 내용은 이미 잘 아시겠지만~ 리마인드 차원에서 간단하게 짚고 넘어가보자!
스택은 한 쪽 끝에서만 자료를 넣거나 뺄 수 있는 선형 구조이며 Last In First Out 매커니즘을 가지고 있다.
Java에서 간단한 Stack 사용법을 보자면..
import java.util.Stack;
// init
Stack<Integer> st = new Stack<>();
// push
st.push(1);
st.push(2);
// pop & print
while(!st.empty()){
System.out.printf("%d ", st.pop());
}
요런식으로 사용할 수 있다. (이것만 알아도 stack 완전 정복~)
Stack Class의 문제점
그래서 왜 Why Java에서 Stack 사용을 지양해야할까? 필자는 크게 2가지 문제가 있다고 생각한다.
1. LIFO 구조를 장담할 수 있나?
Stack은 Vector의 특징을 그대로 받아서 사용하고 있다. 그러므로 중간 삽입/삭제, set을 활용하여 임의로 값을 조작할 수도 있다.
// init
Stack<Integer> st = new Stack<>();
// push
st.push(1);
st.push(2);
// vector add
st.add(0, 3);
// vector set
st.set(0, 999);
while(!st.empty()){
int t = st.pop();
System.out.printf("%d ", t);
}
(사실 이 문제는 대안으로 소개할 Deque도 동일하다..)
2. Vector로부터 상속받은 구조 ⭐⭐
Stack class를 살펴보면
package java.util;
/**
* The {@code Stack} class represents a last-in-first-out
* (LIFO) stack of objects. It extends class {@code Vector} with five
* operations that allow a vector to be treated as a stack.
*/
public class Stack<E> extends Vector<E> {
public Stack() {
}
Vector를 통해 상속받은 것을 확인할 수 있다. 여기서 의문인 것은 Vector를 통해 상속받은 것이 왜 문제가 될까?
이를 알기 위해서는 우리의 주적인 Vector에 대해 알고 있어야 한다.
Step1) Vector란?
고대유물과 같은 Vector는 List 역할을 수행할 수 있는데 List 기능을 가지고 있는 Vector가 무엇이 문제일까..?
Step2) Vector 파해치기
Vector class를 살펴보자.
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
/**
* Replaces the element at the specified position in this Vector with the
* specified element.
*
* @param index index of the element to replace
* @throws ArrayIndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= size()})
* @since 1.2
*/
public synchronized E set(int index, E element) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
get, set 역할을 하는 모든 메서드에 synchronized 키워드가 선언되어 잇는 것을 확인할 수 있다. Vector의 장점이자 단점인데,, 당연하게도 Single Thread 환경에서는 가장 큰 단점이 될 것이다.
동기화를 통해 멀티 쓰레드 환경에서 안전하게 사용할 수 있다. (완전히 Thread-Safety라고 할 수 없지만..)
⇒ 단일쓰레드 환경에서?
Vector<Integer> vector = new Vector<>();
for (int i = 0; i < 10000000; i++) {
vector.add(i);
}
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 10000000; i++) {
arrayList.add(i);
}
결과는~~
이처럼 Vector는 항상 동기화를 진행하기 때문에 오버헤드와 시간 소모가 발생할 수 있다.
그러므로 Vector의 특징을 그대로 물려받은 Stack을 많이 썼다가.. Oops~
Stack의 대안을 내놓으시오
이제 stack의 문제점을 파악했을 것이라고 생각한다! 그래서 Stack을 못쓰면 어떤걸 쓰라는 것일까?
https://docs.oracle.com/javase/8/docs/api/java/util/Stack.html
위 주소와 Stack class의 주석을 해석해보자면, Stack보다 Deque 사용을 우선적으로 고려하라는 뜻이다.
그런데.. 왜 Deque가 대안일까?
일단, Deque는 Duoble-Ended Queue을 줄임말로 큐의 양쪽에 데이터를 넣고 뺄 수 있는 자료구조로 Stack 용도로 충~~분히 활용할 수 있다. 필자가 생각할 때, 공식문서에서도 deque를 사용하라는 이유는 크게 2가지가 있다.
1. Interface
//stack
public class Stack<E> extends Vector<E> { ...}
//Deque
public interface Deque<E> extends Queue<E>, SequencedCollection<E>
Stack의 경우 class로 되어있지만 Deque의 경우 interface로 선언이 되어있는데 그 덕분에 ArrayDeque, LinkedList에서 Deque 인터페이스를 구현하여 유연하게 사용하고 있다. Stack의 경우 단일상속만 가능하기 때문에 유연성 측면에서 현저히 떨어진다.
2. 성능
Deque의 경우 동기화를 수행하지 않기 때문에 오버헤드, 속도 측면에서 더 나은 성능을 보여줄 수 있다.
오늘의 한줄평
Stack은 지양하고 Deque 사용을 지향하자.
'JAVA' 카테고리의 다른 글
[JAVA] Enum "equals" vs "==" (2) | 2024.08.21 |
---|---|
Java 8 Functional Interface에 대해 알아보자. (0) | 2023.10.31 |