💻 프로그래밍

Stack

스택

LIFO(Last-In-First-Out) 자료구조. 마지막에 삽입된 데이터가 먼저 나가는 구조로, 함수 호출 스택, 실행 취소, DFS 등에 활용.

📖 상세 설명

스택(Stack)은 LIFO(Last-In-First-Out, 후입선출) 원칙을 따르는 선형 자료구조입니다. 접시를 쌓아 올리는 것처럼 마지막에 추가된 데이터가 가장 먼저 제거됩니다. 데이터는 top(꼭대기)에서만 삽입(push)과 삭제(pop)가 이루어집니다. 이는 FIFO(First-In-First-Out) 구조인 큐(Queue)와 대비되는 특성입니다. 스택이라는 용어는 '쌓다'라는 의미에서 유래했으며, 가장 기본적이면서도 강력한 자료구조 중 하나입니다.

스택의 핵심 연산들은 모두 O(1) 상수 시간복잡도를 가집니다. push(삽입)는 top에 요소를 추가하고, pop(삭제)은 top 요소를 제거 후 반환합니다. peek/top은 top 요소를 제거 없이 확인만 합니다. isEmpty(비어있는지 확인)와 size(크기 반환)도 O(1)입니다. 배열이나 연결 리스트로 구현할 수 있는데, 배열 구현은 메모리가 연속적이라 캐시 효율이 좋고, 연결 리스트 구현은 동적 크기 조절이 자유롭습니다.

컴퓨터 과학에서 스택은 두 가지 중요한 영역에서 핵심적으로 사용됩니다. 콜 스택(Call Stack)은 함수 호출 시마다 스택 프레임(지역 변수, 반환 주소 등)을 쌓아 함수 실행 흐름을 관리합니다. 함수가 끝나면 프레임이 pop되어 호출자로 돌아갑니다. 메모리 스택은 프로그램 실행 시 지역 변수와 함수 매개변수를 저장하는 메모리 영역입니다. 힙(Heap)과 달리 자동으로 할당/해제되어 빠르지만 크기가 제한적입니다. 재귀 호출이 깊어지면 스택 메모리가 가득 차서 Stack Overflow가 발생합니다.

실무에서 스택은 다양한 분야에서 핵심적으로 활용됩니다. 실행 취소(Undo) 기능은 작업을 스택에 쌓고, Ctrl+Z 시 pop하여 이전 상태로 복원합니다. 괄호 검사는 여는 괄호를 push하고 닫는 괄호가 나올 때 pop하여 짝이 맞는지 확인합니다. DFS(깊이 우선 탐색)는 스택을 사용해 그래프/트리를 탐색합니다. 브라우저의 뒤로가기/앞으로가기, 수식의 후위 표기법 계산, 문자열 역순 처리, 재귀 함수의 반복문 변환 등에도 스택이 필수적으로 사용됩니다.

📊 스택의 종류와 활용

콜 스택 (Call Stack)

함수 호출 관리. 스택 프레임에 지역 변수, 매개변수, 반환 주소 저장. 재귀/함수 중첩의 핵심.

표현식 스택

수식 계산에 사용. 중위(infix)를 후위(postfix)로 변환, 후위 표기법 계산에 활용.

메모리 스택

프로그램 실행 시 지역 변수 저장. 힙보다 빠르지만 크기 제한. 자동 메모리 관리.

Undo 스택

작업 이력 저장. 실행 취소/다시 실행 구현. 에디터, 그래픽 툴에서 필수.

⏱️ 시간복잡도

연산 시간복잡도 설명
push (삽입) O(1) top에 요소 추가
pop (삭제) O(1) top에서 요소 제거 및 반환
peek / top O(1) top 요소 확인 (제거 없음)
isEmpty O(1) 비어있는지 확인
size O(1) 요소 개수 반환
검색 O(n) 특정 요소 찾기 (순차 탐색)

💻 코드 예제

# Python 스택 구현 예제
from typing import Any, List, Optional


class Stack:
    """리스트 기반 스택 구현"""
    def __init__(self):
        self._items: List[Any] = []

    def push(self, item: Any) -> None:
        """스택의 top에 요소 추가 - O(1)"""
        self._items.append(item)

    def pop(self) -> Any:
        """스택의 top에서 요소 제거 및 반환 - O(1)"""
        if self.is_empty():
            raise IndexError("빈 스택에서 pop 시도")
        return self._items.pop()

    def peek(self) -> Any:
        """top 요소 확인 (제거 없음) - O(1)"""
        if self.is_empty():
            raise IndexError("빈 스택에서 peek 시도")
        return self._items[-1]

    def is_empty(self) -> bool:
        """스택이 비어있는지 확인 - O(1)"""
        return len(self._items) == 0

    def size(self) -> int:
        """스택의 크기 반환 - O(1)"""
        return len(self._items)

    def __len__(self) -> int:
        return self.size()

    def __repr__(self) -> str:
        return f"Stack({self._items})"


# 사용 예제
if __name__ == "__main__":
    stack = Stack()
    stack.push("A")
    stack.push("B")
    stack.push("C")

    print(f"스택: {stack}")           # Stack(['A', 'B', 'C'])
    print(f"top: {stack.peek()}")     # C
    print(f"크기: {len(stack)}")       # 3

    print(f"pop: {stack.pop()}")      # C
    print(f"pop: {stack.pop()}")      # B
    print(f"남은 스택: {stack}")        # Stack(['A'])


# ========== 실무 활용 예제 ==========

# 1. 괄호 유효성 검사
def is_valid_parentheses(s: str) -> bool:
    """괄호가 올바르게 짝지어졌는지 확인"""
    stack = []
    pairs = {')': '(', '}': '{', ']': '['}

    for char in s:
        if char in '({[':
            stack.append(char)
        elif char in ')}]':
            if not stack or stack[-1] != pairs[char]:
                return False
            stack.pop()

    return len(stack) == 0

print(is_valid_parentheses("({[]})"))  # True
print(is_valid_parentheses("([)]"))    # False


# 2. 문자열 뒤집기
def reverse_string(s: str) -> str:
    """스택을 사용해 문자열 뒤집기"""
    stack = list(s)
    result = []
    while stack:
        result.append(stack.pop())
    return ''.join(result)

print(reverse_string("Hello"))  # olleH


# 3. DFS (깊이 우선 탐색) - 스택 활용
def dfs_iterative(graph: dict, start: str) -> List[str]:
    """스택을 사용한 반복적 DFS"""
    visited = set()
    stack = [start]
    result = []

    while stack:
        node = stack.pop()
        if node not in visited:
            visited.add(node)
            result.append(node)
            # 역순으로 추가 (왼쪽 자식 먼저 방문하도록)
            for neighbor in reversed(graph.get(node, [])):
                if neighbor not in visited:
                    stack.append(neighbor)

    return result

graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F'],
    'D': [],
    'E': ['F'],
    'F': []
}
print(f"DFS 순회: {dfs_iterative(graph, 'A')}")  # ['A', 'B', 'D', 'E', 'F', 'C']


# 4. 실행 취소 (Undo) 시스템
class UndoSystem:
    """간단한 Undo/Redo 시스템"""
    def __init__(self):
        self.undo_stack = []
        self.redo_stack = []
        self.current_state = ""

    def execute(self, action: str):
        """작업 실행"""
        self.undo_stack.append(self.current_state)
        self.current_state = action
        self.redo_stack.clear()  # 새 작업 시 redo 스택 초기화
        print(f"실행: {action}")

    def undo(self) -> Optional[str]:
        """실행 취소"""
        if not self.undo_stack:
            print("더 이상 취소할 수 없습니다.")
            return None
        self.redo_stack.append(self.current_state)
        self.current_state = self.undo_stack.pop()
        print(f"취소 -> 현재 상태: {self.current_state or '(초기 상태)'}")
        return self.current_state

    def redo(self) -> Optional[str]:
        """다시 실행"""
        if not self.redo_stack:
            print("다시 실행할 작업이 없습니다.")
            return None
        self.undo_stack.append(self.current_state)
        self.current_state = self.redo_stack.pop()
        print(f"다시 실행 -> 현재 상태: {self.current_state}")
        return self.current_state

editor = UndoSystem()
editor.execute("Hello")
editor.execute("Hello World")
editor.execute("Hello World!")
editor.undo()  # Hello World
editor.undo()  # Hello
editor.redo()  # Hello World
// JavaScript 스택 구현 예제

class Stack {
    constructor() {
        this.items = [];
    }

    // 스택의 top에 요소 추가 - O(1)
    push(item) {
        this.items.push(item);
        return this;  // 메서드 체이닝 지원
    }

    // 스택의 top에서 요소 제거 및 반환 - O(1)
    pop() {
        if (this.isEmpty()) {
            throw new Error("빈 스택에서 pop 시도");
        }
        return this.items.pop();
    }

    // top 요소 확인 (제거 없음) - O(1)
    peek() {
        if (this.isEmpty()) {
            throw new Error("빈 스택에서 peek 시도");
        }
        return this.items[this.items.length - 1];
    }

    // 스택이 비어있는지 확인 - O(1)
    isEmpty() {
        return this.items.length === 0;
    }

    // 스택의 크기 - O(1)
    get size() {
        return this.items.length;
    }

    // 스택 초기화
    clear() {
        this.items = [];
    }

    // 배열로 변환
    toArray() {
        return [...this.items];
    }

    toString() {
        return `Stack([${this.items.join(', ')}])`;
    }
}


// 사용 예제
const stack = new Stack();
stack.push("A").push("B").push("C");

console.log(`스택: ${stack}`);            // Stack([A, B, C])
console.log(`top: ${stack.peek()}`);      // C
console.log(`크기: ${stack.size}`);        // 3

console.log(`pop: ${stack.pop()}`);       // C
console.log(`pop: ${stack.pop()}`);       // B
console.log(`남은 스택: ${stack}`);         // Stack([A])


// ========== 실무 활용 예제 ==========

// 1. 괄호 유효성 검사
function isValidParentheses(s) {
    const stack = [];
    const pairs = { ')': '(', '}': '{', ']': '[' };

    for (const char of s) {
        if ('({['.includes(char)) {
            stack.push(char);
        } else if (')}]'.includes(char)) {
            if (stack.length === 0 || stack[stack.length - 1] !== pairs[char]) {
                return false;
            }
            stack.pop();
        }
    }

    return stack.length === 0;
}

console.log(isValidParentheses("({[]})")); // true
console.log(isValidParentheses("([)]"));   // false


// 2. 브라우저 히스토리 (뒤로가기/앞으로가기)
class BrowserHistory {
    constructor(homepage) {
        this.backStack = [];
        this.forwardStack = [];
        this.current = homepage;
    }

    visit(url) {
        this.backStack.push(this.current);
        this.current = url;
        this.forwardStack = [];  // 새 페이지 방문 시 앞으로 가기 스택 초기화
        console.log(`방문: ${url}`);
    }

    back() {
        if (this.backStack.length === 0) {
            console.log("뒤로 갈 수 없습니다.");
            return this.current;
        }
        this.forwardStack.push(this.current);
        this.current = this.backStack.pop();
        console.log(`뒤로 -> ${this.current}`);
        return this.current;
    }

    forward() {
        if (this.forwardStack.length === 0) {
            console.log("앞으로 갈 수 없습니다.");
            return this.current;
        }
        this.backStack.push(this.current);
        this.current = this.forwardStack.pop();
        console.log(`앞으로 -> ${this.current}`);
        return this.current;
    }
}

const browser = new BrowserHistory("google.com");
browser.visit("youtube.com");
browser.visit("facebook.com");
browser.back();    // youtube.com
browser.back();    // google.com
browser.forward(); // youtube.com


// 3. DFS (깊이 우선 탐색) - 스택 활용
function dfsIterative(graph, start) {
    const visited = new Set();
    const stack = [start];
    const result = [];

    while (stack.length > 0) {
        const node = stack.pop();
        if (!visited.has(node)) {
            visited.add(node);
            result.push(node);
            // 역순으로 추가
            const neighbors = graph[node] || [];
            for (let i = neighbors.length - 1; i >= 0; i--) {
                if (!visited.has(neighbors[i])) {
                    stack.push(neighbors[i]);
                }
            }
        }
    }

    return result;
}

const graph = {
    'A': ['B', 'C'],
    'B': ['D', 'E'],
    'C': ['F'],
    'D': [],
    'E': ['F'],
    'F': []
};
console.log(`DFS 순회: ${dfsIterative(graph, 'A')}`); // A,B,D,E,F,C


// 4. 수식 계산기 (후위 표기법)
function evaluatePostfix(expression) {
    const stack = [];
    const tokens = expression.split(' ');

    for (const token of tokens) {
        if (!isNaN(token)) {
            stack.push(parseFloat(token));
        } else {
            const b = stack.pop();
            const a = stack.pop();
            switch (token) {
                case '+': stack.push(a + b); break;
                case '-': stack.push(a - b); break;
                case '*': stack.push(a * b); break;
                case '/': stack.push(a / b); break;
            }
        }
    }

    return stack.pop();
}

// "3 + 4 * 2" -> 후위: "3 4 2 * +"
console.log(evaluatePostfix("3 4 2 * +"));  // 11
// "( 5 - 3 ) * 4" -> 후위: "5 3 - 4 *"
console.log(evaluatePostfix("5 3 - 4 *"));  // 8


// 5. Min Stack (최솟값 O(1) 조회 스택)
class MinStack {
    constructor() {
        this.stack = [];
        this.minStack = [];  // 각 시점의 최솟값 저장
    }

    push(val) {
        this.stack.push(val);
        const minVal = this.minStack.length === 0
            ? val
            : Math.min(val, this.minStack[this.minStack.length - 1]);
        this.minStack.push(minVal);
    }

    pop() {
        this.stack.pop();
        this.minStack.pop();
    }

    top() {
        return this.stack[this.stack.length - 1];
    }

    getMin() {
        return this.minStack[this.minStack.length - 1];
    }
}

const minStack = new MinStack();
minStack.push(5);
minStack.push(2);
minStack.push(7);
console.log(`최솟값: ${minStack.getMin()}`);  // 2
minStack.pop();
minStack.pop();
console.log(`최솟값: ${minStack.getMin()}`);  // 5

🗣️ 실무 대화 예시

기술 면접에서 - Stack vs Queue 비교

"스택과 큐의 핵심 차이는 데이터 처리 순서입니다. 스택은 LIFO로 마지막에 들어온 게 먼저 나가서 함수 호출 스택이나 실행 취소(Undo)에 적합합니다. 큐는 FIFO로 먼저 들어온 게 먼저 나가서 작업 대기열이나 BFS 탐색에 씁니다. 예를 들어 브라우저 뒤로가기는 최근 페이지부터 돌아가니 스택을, 웹 서버 요청 처리는 들어온 순서대로 처리해야 하니 큐를 사용합니다. 시간복잡도는 둘 다 삽입/삭제 O(1)로 동일합니다."

디버깅 중 - Stack Overflow 해결

"Stack Overflow 에러가 나는 이유는 재귀 함수의 base case가 누락되었거나, 재귀 깊이가 너무 깊어서 콜 스택이 가득 찼기 때문입니다. 해결 방법은 세 가지예요. 첫째, base case가 올바르게 정의되었는지 확인하세요. 둘째, 재귀를 반복문으로 변환하여 명시적 스택을 사용하세요. 셋째, 꼬리 재귀 최적화를 지원하는 언어라면 꼬리 재귀 형태로 바꾸세요. Python에서는 sys.setrecursionlimit()으로 제한을 늘릴 수 있지만, 근본적인 해결책은 아닙니다."

코드 리뷰 중 - 재귀 vs 반복문

"이 DFS 함수가 재귀로 구현되어 있는데, 그래프 깊이가 예측 불가능하면 스택으로 바꾸는 게 안전합니다. 재귀는 코드가 깔끔하지만 호출마다 콜 스택에 프레임이 쌓여서 깊이가 수천을 넘으면 터질 수 있어요. 명시적 스택을 사용하면 힙 메모리를 쓰기 때문에 훨씬 깊게 탐색할 수 있습니다. 그리고 메모이제이션이 필요한 경우에도 명시적 스택이 관리하기 편합니다."

⚠️ 주의사항

1
Stack Overflow (스택 오버플로우)

콜 스택의 크기는 제한되어 있습니다. 재귀 호출이 너무 깊거나 무한 재귀에 빠지면 스택이 가득 차서 프로그램이 중단됩니다. JavaScript는 브라우저마다 다르지만 수천~수만 번, Python은 기본 1000번이 한계입니다. 깊이가 예측 불가능하거나 큰 경우, 재귀 대신 명시적 스택과 반복문을 사용하세요.

2
재귀 깊이 제한

많은 언어에서 재귀 깊이에 제한이 있습니다. Python에서는 sys.getrecursionlimit()으로 확인하고 sys.setrecursionlimit()으로 변경할 수 있지만, 시스템 스택 크기를 넘으면 여전히 크래시됩니다. 꼬리 재귀 최적화(TCO)를 지원하는 언어(Scheme, Scala 등)에서는 꼬리 재귀로 작성하면 스택 프레임을 재사용하여 이 문제를 해결할 수 있습니다.

3
메모리 관리와 스택 vs 힙

스택 메모리는 자동 할당/해제되어 빠르지만 크기가 작습니다(보통 1~8MB). 대용량 데이터는 힙에 할당하세요. 지역 변수로 큰 배열을 선언하면 스택 오버플로우가 발생할 수 있습니다. 또한 스택에 저장된 값의 참조가 함수 종료 후에도 남아있으면 dangling pointer 문제가 발생할 수 있으니 주의하세요.

🔗 관련 용어

📚 더 배우기