본문 바로가기
Back-end & 알고리즘

스택과 큐, 무심코 썼다간 장애로 돌아옵니다: 실무자가 반드시 알아야 할 함정

by CodeByJin 2026. 6. 28.
반응형

많은 주니어 개발자들이 코딩 테스트를 통해 스택(Stack)과 큐(Queue)를 '그저 데이터를 넣고 빼는 도구'로 배웁니다. 하지만 실무에서 이 둘을 선택하는 기준은 '자료구조의 정의'가 아니라 운영 환경에서 발생할 비용과 장애 전파 가능성에 있습니다. 잘못된 선택은 단순한 성능 저하를 넘어, 서비스 전체의 메모리 누수나 데이터 불일치라는 치명적인 결과로 이어집니다.

 

이 글에서는 기술적 정의를 나열하는 대신, 왜 특정 상황에서 스택과 큐를 선택하면 안 되는지, 그리고 실무 아키텍트가 의사결정을 내릴 때 고려해야 할 트레이드오프를 짚어봅니다.

스택(Stack): 재귀의 함정과 스택 오버플로우

스택은 '후입선출(LIFO)'의 미학을 가졌지만, 실무에서 이를 직접 구현하거나 남용할 때 마주하는 가장 큰 적은 메모리 관리입니다. 특히 재귀(Recursion) 기반의 로직에서 스택을 잘못 활용하면 프로세스의 런타임 환경을 순식간에 마비시킬 수 있습니다.

 

많은 팀이 겪는 대표적인 실패 사례는 대규모 트리 데이터를 탐색할 때 스택 메모리를 과도하게 점유하는 경우입니다. 운영 환경에서 함수 호출 스택이 깊어지면, JVM이나 파이썬 런타임의 스택 사이즈 제한(Stack Size Limit)에 걸려 `StackOverflowError`가 발생합니다. 이는 단일 스레드를 죽이는 것에 그치지 않고, 서킷 브레이커가 작동하지 않을 경우 장애가 전파되는 병목 지점이 됩니다.

스택 큐 장애 방지 코드
스택 큐 장애 방지 코드

큐(Queue): 비동기 처리의 양날의 검

큐는 비동기 처리의 핵심입니다. 하지만 '언제든 큐에 넣으면 된다'는 믿음은 위험합니다. 특히 메모리 기반의 큐(예: Java의 `LinkedBlockingQueue`)를 무한정 사용할 때 발생하는 메모리 사용량 급증과 GC(Garbage Collection) 영향은 실무에서 가장 빈번하게 발생하는 이슈입니다.

 

데이터 생성 속도가 소비 속도를 따라가지 못하는 상황(Backpressure 문제)에서, 적절한 크기 제한(Capacity) 없는 큐는 힙 메모리를 잠식합니다. 이는 결국 Stop-the-world 시간을 길게 만들어 API 응답 속도 전체를 저하시키는 원인이 됩니다.

구분 스택(Stack) 선택 시 고려 큐(Queue) 선택 시 고려
핵심 리스크 Stack Overflow, 재귀 깊이 OOM, Backpressure, GC 영향
운영 관점 스레드당 스택 사이즈 모니터링 큐 사이즈 제한(Bounded Queue) 필수
대안 재귀를 반복문으로 치환 외부 메시지 브로커(Kafka 등)
반응형

실무 코드 예시: Bounded Queue의 중요성

아래는 실무에서 흔히 발생하는 '잘못된 큐 활용'과 이를 개선한 패턴입니다.


// 잘못된 구현 사례: 무제한 큐 사용
// 운영 환경에서 트래픽 급증 시, 큐가 무한히 커지며 OOM 유발
private final BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>();

public void handleRequest(Task task) {
    taskQueue.offer(task); // 사이즈 제한이 없음
}

// 개선된 구현 사례: Bounded Queue와 거절 정책 도입
private final BlockingQueue<Task> taskQueue = new LinkedBlockingQueue<>(1000);

public void handleRequest(Task task) {
    // 큐가 가득 찼을 때의 정책: 예외 발생 또는 드롭
    boolean accepted = taskQueue.offer(task, 500, TimeUnit.MILLISECONDS);
    if (!accepted) {
        // 모니터링 시스템에 알림 전송 (예: Prometheus Counter 증가)
        log.error("Queue full, dropping task. task_id: {}", task.getId());
        throw new RuntimeException("Service Unavailable: Queue full");
    }
}

 

코드 리뷰 및 운영 관점
위의 개선 코드에서 500ms의 타임아웃을 둔 이유는, 무한 대기로 인한 사용자 요청의 Inference Latency가 길어지는 것을 방지하기 위함입니다. 시스템이 느려질 때 큐에 요청을 쌓아두는 것은 문제를 해결하는 것이 아니라, 장애를 지연시킬 뿐입니다. 적절한 Circuit Breaker를 병행하여 시스템 부하를 차단하는 것이 정석입니다.

AI 엔지니어링과 데이터 구조

LLM 기반의 서비스를 구축할 때도 이 개념은 적용됩니다. 예를 들어, 프롬프트 체이닝이나 대화 내역(Context Window)을 관리할 때 큐를 활용하여 가장 오래된 메시지를 삭제하는 구조를 설계합니다. 이때 큐의 크기가 너무 작으면 Hallucination이 발생할 확률이 커지고, 너무 크면 Token 비용Inference Latency가 기하급수적으로 증가합니다.

 

실무에서 LLM 파이프라인의 성능을 평가(Evaluation)할 때, 단순히 모델의 성능만 보지 마세요. 큐에 데이터가 쌓이는 시간과 그에 따른 GPU 활용률의 상관관계를 모니터링해야 합니다. 이것이 진짜 성능 병목 지점을 찾는 방법입니다.

언제 무엇을 써야 하는가

이 글의 관점은 명확합니다. 기술은 '사용하는 것'보다 '사용하지 않을 때를 아는 것'이 중요합니다. 스택과 큐를 선택할 때 다음 체크리스트를 활용해 보세요.

  • 데이터 처리의 가시성: 현재 시스템의 메모리 사용량이 감당 가능한가? (OOM 발생 가능성)
  • 장애의 파급력: 특정 데이터 처리가 지연될 때 전체 서비스가 멈추는가? (Circuit Breaker 필요성)
  • 운영 복잡도: 인 메모리 큐로 충분한가, 아니면 외부 분산 큐(Kafka/SQS)로 넘겨야 하는가? (유지보수 비용)

많은 개발자가 큐와 스택을 고민할 때 알고리즘 복잡도만 따집니다. 하지만 진정한 실무 설계자는 "이 자료구조가 우리 서비스의 운영 지표(응답 속도, 메모리 점유율)에 어떤 영향을 미치는가"를 먼저 고민합니다. 알고리즘은 도구일 뿐, 책임은 아키텍처에 있다는 점을 잊지 마세요.

혹시 현재 프로젝트에서 메모리 이슈나 처리 지연으로 고생하고 계신 부분이 있나요? 자료구조 선택이 아닌 다른 병목 지점이 의심되는 상황이라면 무엇인가요?

 

 

Java 동기화, 당신의 서비스가 멈추고 있는 진짜 이유

Java 개발자라면 누구나 한 번쯤 마주하는 ‘동기화 문제’. 우리는 흔히 synchronized 키워드를 붙이거나 ReentrantLock을 거는 것으로 문제를 해결하려 합니다. 하지만 실무에서 정말 고민해야 할 지점

byteandbit.tistory.com

 

* 본 포스팅에 사용된 이미지는 생성형 AI를 통해 생성된 이미지입니다. *

 

반응형