자바에서 스택은 이렇게 생겼다.
package java.util;
public class Stack<E> extends Vector<E> {
public Stack() {
}
public E push(E item) {
addElement(item);
return item;
}
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
public boolean empty() {
return size() == 0;
}
public synchronized int search(Object o) {
int i = lastIndexOf(o);
if (i >= 0) {
return size() - i;
}
return -1;
}
@java.io.Serial
private static final long serialVersionUID = 1224463164541339165L;
}
주석을 제거한 클래스가 단촐하다.
이때 Stack위에는 이런 주석이 달려있다.

Stack 클래스는 후입선출(LIFO, Last-In-First-Out) 구조의 객체 스택을 나타냅니다.
이 클래스는 Vector 클래스를 상속하며, 벡터를 스택처럼 다룰 수 있도록 5가지 연산을 추가로 제공합니다.
• 제공 메서드:
• push : 스택의 맨 위에 요소 추가
• pop : 스택의 맨 위 요소 꺼내기
• peek : 스택 맨 위 요소를 꺼내지 않고 확인
• empty : 스택이 비어 있는지 여부 확인
• search : 스택에서 지정한 요소를 찾아, 맨 위에서 몇 번째에 있는지 반환
스택이 처음 생성되면 아무 요소도 포함하지 않습니다.
Stack 클래스 대신, Deque 인터페이스와 그 구현체들이 더 완전하고 일관된 LIFO 스택 연산을 제공합니다.
따라서 가능하다면 Stack 대신 Deque를 사용하는 것이 권장됩니다.
Stack이란 자료구조보다는 Deque을 사용하길 권장한다고 한다.
그럼 Stack은 언제쓸까?
우선 Stack<E>은 Vector<E>를 확장하고 있고, Vector는 AbstractList<E>를 확장하고 List<E>를 구현하고 있다.
지난 포스팅 에서 ArrayList를 보고 온 터라 Vetor와의 차이가 궁금하니 LLM에 질문을 던져서 아래 표를 받아왔다.
Vector / ArrayList
| 도입 시기 | Java 1.0 (원래는 Collections Framework 이전 클래스) | Java 1.2 (Collections Framework의 일부로 추가) |
| 동기화 여부 | Synchronized (스레드 안전) → 멀티스레드 환경에서 안전 | 비동기화 (스레드 안전 X) → 단일 스레드 환경에서 빠름 |
| 성능 | 동기화 때문에 ArrayList보다 느림 | 동기화 비용이 없어서 빠름 |
| 용량 증가 방식 | capacityIncrement 값만큼 증가 (기본은 100% 증가, 즉 2배) | 기존 용량의 50% 증가 |
| Fail-Fast 지원 | Iterator/ListIterator는 지원 (Enumeration은 X) | Iterator/ListIterator 모두 지원 |
| 권장 사용 환경 | 오래된 코드 유지보수, 멀티스레드 환경 | 새로운 개발, 단일 스레드 환경 |
| 예제 코드 | Vector<Integer> v = new Vector<>(); | ArrayList<Integer> list = new ArrayList<>(); |
Vector에는 총 53개의 메서드에 synchronized 구문이 붙어 있다.
Stack 클래스는 이를 확장하여 LIFO를 구현할 뿐, 결국 배열을 래핑한 클래스일 뿐이다.
그렇다면 Stack 주석에서 대체를 권장하는 Deque는 어떨까?
Deque<E>는 인터페이스로 Queue<E>와 SequencedCollection<E>를 확장하고 있다.
주석을 제외한 메서드는 아래와 같이 간결하다. 재밌는 점은 각 메서드를 용도에 따라 주석으로 구분을 해주고 있다는 점이다.
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E getFirst();
E getLast();
E peekFirst();
E peekLast();
boolean removeFirstOccurrence(Object o);
boolean removeLastOccurrence(Object o);
// *** Queue methods ***
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
boolean addAll(Collection<? extends E> c);
// *** Stack methods ***
void push(E e);
E pop();
// *** Collection methods ***
boolean remove(Object o);
boolean contains(Object o);
int size();
Iterator<E> iterator();
Iterator<E> descendingIterator();
default Deque<E> reversed() {
return ReverseOrderDequeView.of(this);
}
이번에도 LLM에 비교 표를 받아와보자.
Stack vs Deque 비교
구분StackDeque (예: ArrayDeque)
| 도입 시기 | Java 1.0 (오래된 클래스) | Java 1.6 (Deque 인터페이스), 구현체 ArrayDeque는 Java 1.6 |
| 상속 구조 | java.util.Vector 상속 (Legacy Collection) | java.util.Queue 인터페이스 확장 |
| 스레드 안전성 | Synchronized (스레드 안전) → 멀티스레드 환경에서 안전 | 비동기화 (스레드 안전 X) → 단일 스레드 환경에서 빠름 |
| LIFO 연산 메서드 | push(), pop(), peek() | push(), pop(), peek() (Deque 인터페이스 기본 제공) |
| 추가 기능 | search()로 요소의 위치 반환 | 양방향 큐이므로, LIFO + FIFO 모두 지원 |
| 성능 | 동기화 때문에 느림 | 동기화 비용이 없어 빠름 |
| 메모리 사용 | Vector 기반이라 메모리 오버헤드 존재 | 내부 배열 크기 자동 조절, 효율적 |
| 권장 사용 여부 | 권장되지 않음 (레거시) | 권장됨 (최신 코드) |
| 예제 코드 | Stack<Integer> stack = new Stack<>(); | Deque<Integer> stack = new ArrayDeque<>(); |
즉 Stack은 여기저기서 호출하는 syncronized 키워드로 인한 동기화 비용이 있어서 멀티 스레드 환경에서는 스레드 세이프하다는 장점이 잇는 반면 그로 인한 오버헤드가 있다.
반면 Deque, 그리고 대표적인 구현체인 ArryaDeque에는 syncronized 키워드가 한군데도 사용되지 않았다.
때문에 스레드세이프 하다고 보긴 어렵지만 비용 효율적이고, 양방향 큐로 동작하기 때문에 Stack의 동작도 구현이 가능하다.
대표적인 Deque의 구현체인 ArrayDeque의 메서드에는 아래와 같이 Stack으로 활용하기 위해 필요한 메서드도 지원한다.
public void push(E e) { addFirst(e); }
public E pop() { return removeFirst(); }
public boolean isEmpty() { return head == tail; }
다만, Deque는 클래스 주석에 나와있듯이 길이 조절형이기 때문에 isFull() 메서드나 top과 같은 값은 제공하지 않는다.
Deque 인터페이스의 크기 조절이 가능한 배열(resizable array) 기반 구현체입니다.
•용량 제한 없음
ArrayDeque는 용량 제한이 없으며, 사용량에 맞춰 필요할 때 자동으로 크기가 늘어납니다.
•스레드 안전성 없음
ArrayDeque는 스레드 안전하지 않으므로, 외부에서 동기화를 하지 않으면 여러 스레드가 동시에 접근하는 것을 지원하지 않습니다.
•Null 요소 금지
null 값을 요소로 저장할 수 없습니다.
•성능 특성
•스택(Stack)처럼 사용할 경우, Stack 클래스보다 빠를 가능성이 높습니다.
•큐(Queue)로 사용할 경우, LinkedList보다 빠를 가능성이 높습니다.
jdk21 기준으로 코드를 보고 있다지만, 찾아보니 이미 자바 1.6이 나온 2006년 말부터 Stack 대신 Deque를 쓰라고 권장하고 있었다.
스택이라는 자료구조 자체는 여전히 중요하고 네이티브 영역에서도 많이 쓰이지만, 정작 자바에서는 대체된 지 오래라는 게 좀 재밌다.
자료구조랑 알고리즘 공부를 하면 스택이랑 큐는 항상 나오는데, 막상 실제 개발에서 Stack을 언제 쓰나 싶었다.
Stack을 찾아보다 보니까 Deque가 나왔고, Deque는 Queue<E>를 구현하고 있어서 스택 모드랑 큐 모드를 하나로 다 구현할 수 있었다.
결국, Stack은 LIFO의 자료구조, Queue는 FIFO의 자료구조, Deque은 양방향 IO 자료 구조이다.
내부에서는 array를 이용하여 값을 관리하고, array의 크기를 개발자가 신경쓰지 않더라도 알아서 조절해주는 것이 핵심인 것이다.
오버헤드를 감수하더라도 스레드 세이프하게 Stack구조를 사용해야 하거나, 다른 인터페이스 없이 사용성을 제한하고 싶다면 Stack 클래스를, 스레드 세이프하지 않거나 조금 더 빠른 성능과 사용성을 원한다면 Deque를 사용하면 될것 같다.
참고자료
graalvm-jdk-21
'공부 기록' 카테고리의 다른 글
| 자바에서 HashSet에 값 넣고 꺼내기 (5) | 2025.08.10 |
|---|---|
| 자바에서 HashMap에 값 넣고 꺼내기 (5) | 2025.08.06 |
| 자바에서 배열은 클래스인가? (7) | 2025.08.04 |
| [NEXTSTEP] ATDD 과정 4주차 피드백, 5주차 시작[끝] (0) | 2023.08.19 |
| [NEXTSTEP] ATDD 과정 3주차 피드백, 4주차 시작 (0) | 2023.07.29 |
댓글