스택 오버플로우(Stack Overflow)

허성재's avatar
Aug 13, 2024
스택 오버플로우(Stack Overflow)
 
정말 유명한 개발자들의 사이트인 스택 오버플로우의 뜻을 알아보자.
 

1. Stack Overflow의 뜻

Stack Overflow는 프로그램 실행 중에 호출 스택(call stack)이 허용된 크기를 초과할 때 발생하는 오류입니다. 호출 스택은 함수나 메서드 호출 시 해당 함수의 매개변수, 복귀 주소 등을 저장하는 메모리 영역입니다. 메서드가 너무 깊게 중첩되거나 재귀 호출이 끝나지 않으면 스택 메모리가 가득 차서 더 이상 데이터를 저장할 수 없는 상황이 발생하며, 이때 StackOverflowError가 발생합니다.

2. Stack Overflow의 대표적 원인과 해결 방법

2.1 원인: 무한 재귀 호출 (Infinite Recursion)

무한 재귀 호출은 메서드가 끝없이 자기 자신을 호출하는 경우입니다. 이러한 경우, 호출 스택이 계속해서 쌓이다가 결국 스택 메모리가 가득 차서 StackOverflowError가 발생합니다.
Java 예제:
public class StackOverflowExample { public static void recursiveMethod() { recursiveMethod(); // 종료 조건 없이 자기 자신을 계속 호출 } public static void main(String[] args) { recursiveMethod(); // StackOverflowError 발생 } }
위 코드에서는 recursiveMethod()가 자기 자신을 계속 호출하면서 무한 루프에 빠집니다. 이는 스택 메모리를 고갈시켜 StackOverflowError를 일으킵니다.
 

해결 방법: 종료 조건 추가 (Base Case)

재귀 호출에 대한 종료 조건을 추가하여 스택 오버플로우를 방지할 수 있습니다. 종료 조건이 있으면 재귀 호출이 일정 조건에서 멈추기 때문에 호출 스택이 더 이상 쌓이지 않습니다.
Java 예제:
public class StackOverflowSolution { public static void recursiveMethod(int n) { if (n == 0) { // 종료 조건 설정 return; // 재귀 호출 종료 } recursiveMethod(n - 1); // 재귀 호출 } public static void main(String[] args) { recursiveMethod(5); // 안전한 재귀 호출 } }
이 코드에서는 n0이 되면 재귀 호출이 종료됩니다. 이렇게 종료 조건이 추가되면 스택 오버플로우가 발생하지 않습니다.

2.2 원인: 과도한 재귀 깊이 (Excessive Recursion Depth)

재귀 호출이 종료 조건을 가지고 있더라도, 재귀 호출의 깊이가 너무 깊으면 스택 메모리가 여전히 초과될 수 있습니다. 이는 특히 재귀 깊이가 매우 큰 경우에 발생합니다.
Java 예제:
public class ExcessiveRecursionExample { public static void deepRecursion(int n) { if (n == 0) { return; } deepRecursion(n - 1); // 재귀 호출 } public static void main(String[] args) { deepRecursion(100000); // 큰 깊이의 재귀 호출 -> StackOverflowError 발생 가능 } }

해결 방법: 반복문으로 리팩토링 (Refactoring to Iteration)

재귀 호출 대신 반복문을 사용하여 문제를 해결할 수 있습니다. 반복문은 재귀 호출처럼 호출 스택을 사용하지 않으므로, 스택 오버플로우의 위험이 없습니다.
Java 예제:
public class IterativeSolution { public static void iterativeMethod(int n) { while (n > 0) { n--; } } public static void main(String[] args) { iterativeMethod(100000); // StackOverflowError 발생하지 않음 } }
이 코드에서는 반복문을 사용하여 재귀 호출의 위험을 피하고, 더 큰 입력 값도 처리할 수 있습니다.
 

2.3 꼬리 재귀 최적화 (Tail Recursion Optimization)

  • Tail Recursion은 재귀 호출이 함수의 마지막 동작으로 이루어지는 경우입니다. 일부 컴파일러나 런타임에서는 이러한 꼬리 재귀를 최적화하여 스택 프레임을 재사용함으로써 스택 오버플로우를 방지할 수 있습니다.
  • Java에서는 기본적으로 꼬리 재귀 최적화(Tail Call Optimization, TCO)가 지원되지 않지만, 이 패턴을 활용할 수 있는 경우, 알고리즘을 다른 언어로 구현하거나, 꼬리 재귀 최적화가 가능한 런타임 환경에서 실행할 수도 있습니다.
꼬리 재귀 예제:
public class TailRecursionExample { public static int tailRecursiveMethod(int n, int result) { if (n == 0) { return result; // 종료 조건 } return tailRecursiveMethod(n - 1, result * n); // 마지막 호출로 꼬리 재귀 } public static void main(String[] args) { System.out.println(tailRecursiveMethod(5, 1)); // 결과: 120 } }
위의 예제에서는 tailRecursiveMethod가 꼬리 재귀를 사용하고 있으나, Java에서 자동으로 최적화되지는 않습니다. 하지만 일부 다른 언어에서는 이러한 패턴을 최적화하여 스택 오버플로우를 방지할 수 있습니다.

2.4 스택 크기 조정

  • JVM(Java Virtual Machine) 스택 크기 조정: Java에서는 JVM의 스택 크기를 조정하여 스택 오버플로우를 방지할 수 있습니다. 그러나 이것은 임시적인 해결책에 불과하며, 근본적인 문제를 해결하지는 않습니다.
JVM 스택 크기 조정 예제:
  • 커맨드 라인에서 Xss 옵션을 사용하여 스택 크기를 늘릴 수 있습니다.
java -Xss2m YourClassName
  • 이 옵션은 기본 스택 크기를 초과하는 큰 재귀나 깊은 함수 호출을 처리할 때 유용할 수 있습니다. 하지만 스택 크기를 무작정 늘리는 것은 메모리 낭비로 이어질 수 있으며, 근본적인 문제를 해결하는 방법은 아닙니다.

2.5 재귀 대신 데이터 구조 사용

  • 재귀 호출을 데이터 구조(예: 스택 자료구조)를 사용하여 구현할 수 있습니다. 이렇게 하면 스택 오버플로우를 피하면서 재귀 알고리즘의 논리를 유지할 수 있습니다.
Java에서 스택을 사용한 예제:
import java.util.Stack; public class StackBasedSolution { public static void iterativeFactorial(int n) { Stack<Integer> stack = new Stack<>(); int result = 1; while (n > 0) { stack.push(n--); } while (!stack.isEmpty()) { result *= stack.pop(); } System.out.println(result); // 결과 출력 } public static void main(String[] args) { iterativeFactorial(5); // 결과: 120 } }
이 방법은 호출 스택이 아닌 별도의 스택 객체를 사용하여 스택 오버플로우 문제를 해결합니다.
 
Share article

heo-gom