Java 자바 스택(Stack) 개념과 사용법

suover

java

Stack이란 무엇인가?

Java에서 Stack은 자료구조의 한 종류로, 데이터를 순서대로 쌓아 올리는 형태로 운영됩니다. 컴퓨터 과학에서 흔히 Last In, First Out (LIFO) 구조라 부르는 Stack은 가장 나중에 들어간 데이터가 가장 먼저 나오는 특징을 가지고 있습니다. 이 글에서는 Java에서 Stack을 어떻게 사용하고 구현하는지, 그 내부적인 작동 방식은 무엇인지, 그리고 실제로 사용되는 다양한 사례들을 깊이 있게 살펴보겠습니다.

Stack은 컴퓨터 메모리의 한 단위로 자주 사용됩니다. 그 이유는 후입선출(LIFO)의 작동 원리가 함수 호출과 같은 작업에 매우 적합하기 때문입니다. Stack은 다음과 같은 특징을 가집니다.

Java 자바 스택(Stack) 개념과 사용법

  • LIFO (Last In, First Out): 가장 마지막에 삽입된 항목이 가장 먼저 제거됩니다.

이러한 연산들로 Stack은 데이터를 효과적으로 관리할 수 있습니다. 하지만 중요한 점은 Stack은 메모리 제한을 받을 수 있다는 것입니다. Java에서 Stack은 메모리와 깊게 연관되어 있으며, 자바 기본 메모리 영역인 Call Stack과도 관련이 깊습니다.

Stack의 실제 활용 사례

Stack은 여러 가지 현실적인 문제 해결에 사용됩니다. 대표적인 예로는 다음과 같습니다.

  • 수식 계산기: 후위 표기법을 사용한 계산기 구현에서 스택을 사용해 연산 순서를 관리할 수 있습니다.
  • 괄호 검사기: 소스 코드나 문자열에서 괄호의 짝이 맞는지 검사할 때 스택을 활용하면 편리합니다. 여는 괄호는 스택에 쌓고, 닫는 괄호가 나올 때마다 스택에서 꺼내 비교하는 방식입니다.
  • Undo 기능: 텍스트 에디터 등에서 마지막 작업을 취소하는 Undo 기능을 구현할 때, 각 작업을 스택에 저장하고 필요할 때 이전 상태로 되돌리는 방식으로 구현할 수 있습니다.

주요 메서드

Java의 Stack 클래스에서 자주 사용되는 주요 메서드는 다음과 같습니다.

  • push(E item): 스택의 맨 위에 새로운 데이터를 삽입합니다.
    • 예: stack.push(1);은 스택의 맨 위에 숫자 1을 추가합니다.
  • pop(): 스택의 맨 위에 있는 데이터를 제거하고 그 값을 반환합니다.
    • 예: stack.pop();은 스택의 맨 위 요소를 제거하고 반환합니다.
  • peek(): 스택의 맨 위에 있는 데이터를 제거하지 않고 반환합니다.
    • 예: stack.peek();은 스택의 맨 위 요소를 확인만 합니다.
  • isEmpty(): 스택이 비어 있는지 여부를 확인합니다.
    • 예: stack.isEmpty();은 스택이 비어 있으면 true를 반환합니다.
  • search(Object o): 스택에서 특정 요소의 위치를 반환합니다. 스택의 맨 위에서부터 검색하며, 요소의 1-based 위치를 반환합니다. 요소가 없으면 -1을 반환합니다.
    • 예: stack.search(2);은 스택에 있는 숫자 2의 위치를 반환합니다.
  • size(): 스택에 있는 요소의 개수를 반환합니다.
    • 예: stack.size();는 스택에 현재 저장된 요소의 개수를 반환합니다.

이 메서드들을 사용하면 Stack의 기본적인 연산을 손쉽게 수행할 수 있습니다.

Java에서 Stack 클래스

Java에서 Stack은 java.util.Stack 클래스로 직접 사용할 수 있습니다. Stack 클래스는 Vector 클래스를 상속받아 구현되어 있으며, 사용하기 간편합니다.

Java
import java.util.Stack;

public class StackExample {
    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<>();
        
        // Stack에 데이터 추가
        stack.push(1);
        stack.push(2);
        stack.push(3);
        
        // Stack의 맨 위 요소 확인 (3)
        System.out.println("Peek: " + stack.peek());
        
        // Stack의 맨 위 요소 제거 (3)
        System.out.println("Pop: " + stack.pop());
        
        // Stack 상태 출력
        System.out.println("Stack after pop: " + stack);
        
        // Stack에서 요소 검색
        System.out.println("Position of element 2: " + stack.search(2));
        
        // Stack의 크기 확인
        System.out.println("Stack size: " + stack.size());
    }
}
Java

위 코드에서 push(), pop(), peek(), search(), size() 메서드를 통해 Stack의 기본적인 연산을 간단히 수행할 수 있습니다. Stack 클래스는 동기화된(synchronized) 구조를 가지고 있어서, 멀티스레딩 환경에서의 사용이 가능합니다. 그러나 동기화는 성능에 영향을 미치므로 대규모의 데이터 처리에서는 주의해야 합니다.

Stack의 내부 작동 원리

Java에서 Stack은 배열(Array) 혹은 연결 리스트(Linked List) 구조로 구현될 수 있습니다. java.util.Stack은 Vector를 기반으로 동작하며, 내부적으로 배열을 사용해 데이터를 관리합니다. 이로 인해 메모리 재할당이 발생할 수 있습니다. 만약 스택이 가득 차게 되면, Java는 새로운 더 큰 배열을 할당하고 기존 데이터를 복사하는 방식으로 용량을 확장합니다.

이러한 구조적 특징 때문에, Stack 사용 시 성능 문제가 발생할 수 있습니다. 예를 들어, 스택이 자주 확장되거나 데이터의 크기가 매우 클 경우, 메모리와 CPU 사용량이 급증할 수 있습니다. 이를 피하려면 스택의 용량을 사전에 예측하고 적절히 설정하는 것이 좋습니다.

Stack과 Call Stack

Java에서 Stack은 메서드 호출을 관리하기 위해 Call Stack에서 사용됩니다. 각 메서드 호출은 Call Stack에 프레임을 쌓아 올리는 방식으로 관리됩니다. 이렇게 하면 메서드가 끝날 때 자동으로 해당 프레임이 제거되고, 호출한 메서드로 돌아갑니다.

예를 들어, 재귀 함수 호출 시 각 호출은 Call Stack에 새로운 프레임을 추가하며, 이 프레임들은 후입선출 방식에 따라 처리됩니다. 이 때문에 재귀 함수가 너무 깊게 호출되면 Stack Overflow 오류가 발생할 수 있습니다.

Java
public class RecursionExample {
    public static void main(String[] args) {
        int result = factorial(5);
        System.out.println("Factorial: " + result);
    }

    public static int factorial(int n) {
        if (n == 1) {
            return 1;
        }
        return n * factorial(n - 1);
    }
}
Java

위 코드에서 factorial(5) 호출 시, 각 재귀 호출이 Call Stack에 쌓입니다. 이때 메서드 호출이 끝날 때마다 스택에서 제거됩니다. 이런 방식은 간단한 문제 해결에 효과적이지만, 재귀 깊이가 너무 깊어질 경우 StackOverflowError가 발생할 수 있습니다.

Stack을 사용하면 안되는 이유

Java에서 Stack 클래스를 사용할 때 몇 가지 한계와 문제점들이 있습니다. 이는 자바 공식 문서에서도 명확하게 지적된 부분입니다. 다음은 Java 공식 문서에서 발췌한 내용입니다.

“The Stack class represents a last-in-first-out (LIFO) stack of objects. It extends class Vector with five operations that allow a vector to be treated as a stack. The Stack class is synchronized, which means that it’s safe for use in a multithreaded environment, but at a cost of reduced performance. For better performance, it’s often recommended to use Deque as a stack instead.”

이 문장의 번역은 다음과 같습니다.

“Stack 클래스는 객체들의 후입선출(LIFO) 스택을 나타냅니다. 이 클래스는 Vector 클래스를 확장하여 스택으로 사용할 수 있는 다섯 가지 연산을 제공합니다. Stack 클래스는 동기화되어 있으므로 멀티스레드 환경에서 안전하게 사용할 수 있지만, 성능 저하의 대가가 따릅니다. 더 나은 성능을 위해 스택으로서 Deque를 사용하는 것이 종종 권장됩니다.”

이와 같은 이유로, Stack 클래스의 단점으로는 다음과 같은 것들이 있습니다.

  1. 동기화로 인한 성능 저하: java.util.Stack 클래스는 동기화된(synchronized) 구조로 설계되어 있습니다. 이는 멀티스레드 환경에서 데이터의 무결성을 보장할 수 있다는 장점이 있지만, 단일 스레드 환경에서는 불필요한 성능 저하를 초래할 수 있습니다. 동기화 때문에 발생하는 오버헤드는 대규모의 데이터 처리를 할 때 큰 문제가 될 수 있습니다. 실제로 많은 경우에 이러한 동기화는 필요하지 않으므로, 비동기적 자료구조인 ArrayDeque가 성능 면에서 더 유리합니다.
  2. 레거시 클래스: Stack 클래스는 자바의 초창기부터 존재한 레거시 클래스입니다. 이는 최신 자료구조나 모던한 코드 스타일을 반영하지 못하고 있다는 것을 의미합니다. Java에서는 이후에 더 나은 대안으로 Deque 인터페이스를 제공하고 있으며, ArrayDeque를 사용해 스택의 기능을 구현하는 것을 권장하고 있습니다. ArrayDeque는 비동기적이면서도 메모리 재할당을 효율적으로 수행하기 때문에 일반적인 경우 더 나은 선택입니다.
  3. Vector 상속: Stack은 Vector 클래스를 상속받아 구현되어 있습니다. 이는 Stack의 동작과는 관계없는 여러 메서드가 그대로 상속된다는 의미입니다. 예를 들어, add()와 같은 Vector의 메서드가 스택의 API로 노출되어 있어, 이를 잘못 사용하면 자료구조의 일관성이 깨질 수 있습니다. 이는 유지보수성과 사용성을 저하시킬 수 있는 요소입니다.
  4. 메모리 사용 문제: Stack 클래스는 내부적으로 배열을 사용하기 때문에, 스택의 크기가 계속해서 증가할 경우 새로운 배열을 할당하고 기존 데이터를 복사하는 메모리 재할당이 빈번하게 발생할 수 있습니다. 이 과정에서 불필요한 메모리 사용과 성능 저하가 발생할 수 있습니다. 특히, 데이터의 크기를 예측하기 어렵거나 빈번하게 스택의 크기를 변경해야 하는 경우에는 비효율적입니다.

Stack의 한계와 대안

Java의 Stack 클래스는 편리하지만 몇 가지 한계점이 존재합니다. 예를 들어, 동기화로 인한 성능 저하 문제로 인해 단일 스레드 환경에서는 오히려 불리할 수 있습니다. 이와 같은 이유로 Java에서는 Deque 인터페이스를 사용하는 것을 권장하기도 합니다. ArrayDeque는 비동기적이며 Stack의 대부분의 기능을 더욱 빠르게 수행할 수 있습니다.

Java
import java.util.ArrayDeque;
import java.util.Deque;

public class DequeAsStackExample {
    public static void main(String[] args) {
        Deque<Integer> stack = new ArrayDeque<>();
        
        // 데이터 추가
        stack.push(1);
        stack.push(2);
        stack.push(3);
        
        // 데이터 조회 및 제거
        System.out.println("Pop: " + stack.pop());
        System.out.println("Peek: " + stack.peek());
    }
}
Java

위 코드에서 ArrayDeque는 Stack과 유사한 역할을 수행하지만, 성능상 더 유리합니다. 실제로 Java 8 이후에는 Stack 대신 ArrayDeque를 사용하는 것이 성능 면에서 권장됩니다.

결론

Java의 Stack 자료구조는 간단하고 강력한 개념으로, 많은 프로그래밍 문제를 해결하는 데 유용합니다. 함수 호출 관리, 데이터의 순서 처리, 되돌리기 기능 등 다양한 곳에 Stack이 사용됩니다. 그러나 Java의 기본 Stack 클래스는 몇 가지 한계가 있으므로, 적절한 상황에서 ArrayDeque와 같은 대안을 선택하는 것이 중요합니다.

Leave a Comment