Computer Science

스택과 큐: Swift에 Stack 타입이 없는 이유

스택은 LIFO, 큐는 FIFO 순서로 데이터를 꺼내는 자료구조입니다. Swift에 전용 Stack 타입이 없는 이유, Array로 구현하는 법과 Queue에서 removeFirst가 만드는 성능 함정을 정리합니다.

5분 읽기
스택과 큐: Swift에 Stack 타입이 없는 이유 대표 이미지

자료구조 공부를 시작하면 가장 먼저 만나는 두 형제가 스택(Stack)과 큐(Queue)입니다.

개념 자체는 5분이면 끝납니다. 그런데 Swift로 넘어오면 이상한 점이 하나 보입니다. 표준 라이브러리에 Array, Dictionary, Set은 있는데 StackQueue 타입은 없습니다. 왜 없는지, 그리고 없는 채로 어떻게 써야 하는지까지가 이 글의 범위입니다.

특히 큐를 배열로 흉내 낼 때 밟는 성능 지뢰(removeFirst())는 코딩 테스트에서 시간 초과의 단골 원인이라, 따로 자세히 다룹니다.

이 글의 스택은 자료구조이며 메모리 할당 영역과는 다릅니다. 같은 이름의 차이는 메모리 스택 vs 힙에서, 큐가 우선순위를 갖는 다음 단계는 힙과 우선순위 큐에서 이어집니다.


개념은 접시와 줄서기로 끝

스택은 LIFO(Last In, First Out). 마지막에 넣은 게 먼저 나옵니다. 접시 더미를 떠올리면 됩니다. 위에 쌓고(push), 위에서 꺼냅니다(pop). 중간에서 빼는 건 반칙입니다.

큐는 FIFO(First In, First Out). 먼저 넣은 게 먼저 나옵니다. 계산대 줄서기입니다. 뒤로 들어와서(enqueue) 앞으로 나갑니다(dequeue).

연산은 각각 두 개가 전부입니다.

넣기 꺼내기 엿보기
스택 push pop peek(top)
enqueue dequeue front

이게 전부인데 왜 중요할까요? 이 단순한 규칙이 iOS 개발 곳곳의 뼈대이기 때문입니다.

  • 콜 스택: 함수 호출과 리턴이 정확히 push/pop입니다. 재귀가 깊어지면 나는 크래시 이름이 괜히 stack overflow가 아닙니다
  • UINavigationController: pushViewController / popViewController — 화면 전환이 문자 그대로 스택입니다
  • 실행취소(undo): 커맨드를 스택에 쌓고 역순으로 되돌립니다
  • GCD(Grand Central Dispatch)의 DispatchQueue: 이름 그대로 큐입니다. serial queue에 넣은 작업은 넣은 순서대로 실행됩니다
  • BFS/DFS: 그래프 탐색에서 큐를 쓰면 BFS(Breadth-First Search, 너비 우선 탐색), 스택을 쓰면 DFS(Depth-First Search, 깊이 우선 탐색)가 됩니다. 자료구조 선택이 곧 알고리즘 선택입니다

Swift에 Stack이 없는 이유

결론부터 말하면, Array가 이미 완벽한 스택이기 때문입니다.

var stack: [Int] = []
stack.append(3)      // push — O(1)
stack.append(7)
let top = stack.last // peek — O(1)
let popped = stack.popLast() // pop — O(1), 비어 있으면 nil

배열의 끝에서 넣고 빼는 연산은 전부 O(1)입니다(정확히는 amortized O(1) — 내부 버퍼가 꽉 찼을 때만 확장 비용이 듭니다). 별도 타입을 만들어봐야 Array를 감싼 껍데기라서 표준 라이브러리는 타입을 추가하지 않는 쪽을 택했습니다.

그래도 의도를 드러내고 싶다면 얇게 감싸는 게 관례입니다.

struct Stack<Element> {
    private var storage: [Element] = []
    var isEmpty: Bool { storage.isEmpty }
    var top: Element? { storage.last }
    mutating func push(_ element: Element) { storage.append(element) }
    mutating func pop() -> Element? { storage.popLast() }
}

subscript를 막아서 “중간 접근 금지”라는 스택의 규칙을 타입으로 강제하는 효과가 있습니다.

스택 push pop과 큐 enqueue dequeue 동작 구조를 나란히 그린 LIFO FIFO 다이어그램
넣고 빼는 방향만 기억하면 헷갈릴 일이 없습니다

큐는 배열로 만들면 함정에 빠진다

스택과 같은 논리로 큐를 만들면 이렇게 됩니다.

var queue: [Int] = []
queue.append(1)              // enqueue — O(1), 문제없음
let first = queue.removeFirst() // dequeue — O(n), 여기가 함정

removeFirst()는 맨 앞 요소를 빼고 나머지 전부를 한 칸씩 앞으로 당깁니다. 요소가 10만 개면 dequeue 한 번에 10만 번의 이동이 일어납니다. BFS처럼 dequeue를 수만 번 반복하는 알고리즘에서 이걸 쓰면 전체가 O(n²)이 되고 코딩 테스트라면 시간 초과가 납니다.

해결책은 세 가지입니다.

1. 인덱스로 앞을 밀기 (코딩 테스트 정석)

var queue: [Int] = []
var head = 0
// enqueue
queue.append(5)
// dequeue
let value = queue[head]
head += 1

실제로 빼지 않고 읽는 위치만 옮깁니다. dequeue가 O(1)이 되고, 다 쓴 앞부분 메모리는 그대로 남지만 코딩 테스트에서는 거의 항상 이걸로 충분합니다.

2. 스택 두 개로 큐 만들기 (면접 단골)

넣을 때는 in 스택에 쌓고 뺄 때는 out 스택이 비어 있으면 in을 통째로 뒤집어 옮긴 뒤 pop합니다. 요소 하나당 이동이 최대 두 번이라 amortized O(1)입니다. “스택으로 큐를 구현하라”는 면접 문제의 정답이기도 합니다.

3. swift-collections의 Deque (실무 정답)

애플이 관리하는 swift-collections 패키지의 Deque는 링 버퍼 기반이라 **양쪽 끝 삽입·삭제가 전부 O(1)**입니다.

import DequeModule

var queue: Deque<Int> = []
queue.append(1)            // enqueue
let v = queue.popFirst()   // dequeue — O(1)

Array와 API가 거의 같아서 교체 비용도 낮습니다. 실무에서 큐가 필요하면 직접 만들지 말고 이걸 쓰는 게 맞습니다.


어느 쪽을 쓸지 판단하는 기준

헷갈릴 때는 질문 하나면 정리됩니다. “나중에 온 걸 먼저 처리해야 하나, 먼저 온 걸 먼저 처리해야 하나?”

  • 되돌리기, 괄호 짝 검사, 방문 경로 역추적, 깊이 우선 탐색 → 최근 것 우선 → 스택
  • 작업 대기열, 이벤트 처리, 프린터 출력, 너비 우선 탐색 → 도착 순서 보장 →

괄호 짝 검사는 스택의 대표 활용이라 한 줄만 짚고 갑니다. 여는 괄호를 만나면 push, 닫는 괄호를 만나면 pop해서 짝이 맞는지 확인 — 끝났을 때 스택이 비어 있으면 유효한 수식입니다. 컴파일러가 코드의 중괄호를 검사하는 원리도 같습니다.

Swift 배열 removeFirst O(n) 성능 함정을 컨베이어 벨트로 표현한 큐 일러스트
하나 뺄 때마다 뒤에 있는 전부가 한 칸씩 움직입니다

정리

  • 스택은 LIFO, 큐는 FIFO — 접시 더미와 계산대 줄이면 개념은 끝입니다
  • 콜 스택·내비게이션 스택·undo가 스택, DispatchQueue·이벤트 처리·BFS가 큐 — iOS 개발 곳곳에 이미 깔려 있습니다
  • Swift에 Stack 타입이 없는 건 Array의 append/popLast가 이미 O(1) 스택이기 때문입니다
  • 큐를 배열로 만들 때 removeFirst()는 O(n) — 코딩 테스트 시간 초과의 단골 원인입니다
  • 해결책은 head 인덱스 방식, 스택 두 개 방식, 그리고 실무라면 swift-collections의 Deque입니다
  • 스택이냐 큐냐는 “최근 것 우선이냐, 도착 순서 우선이냐”라는 질문 하나로 갈립니다

다음 글에서는 이 둘의 응용판인 힙(Heap)과 우선순위 큐, 그리고 Dictionary 뒤에 숨은 해시 테이블로 이어가겠습니다.