백엔드 개발자 블로그

스택 오버 플로우(SOF) 본문

Java

스택 오버 플로우(SOF)

backend-dev 2024. 4. 18. 22:16

Stack Overflow가 발생하는 상황을 살펴보고 해결방법을 알아봅시다.


Stack Overflow란 무엇인가?

스택 오버 플로우는 지정한 스택 메모리 사이즈보다 더 많은 스택 메모리를 사용하게 되어 에러가 발생하는 상황을 말합니다.

버퍼 오버 플로우와 차이

버퍼 오버 플로우는 데이터를 저장할 메모리 위치가 유효한지를 검사하지 않고 데이터를 저장하다보니 데이터가 저장될 수 있는 가용 메모리 공간을 벗어 나는 경우를 말합니다.

그렇다면 스택 메모리에는 어떤 데이터들이 저장될까?

스택 메모리는 보통 원시 타입이나 힙 메모리에 저장되는 데이터의 메모리 주소를 저장합니다. 메소드 영역 내에서 생성된 스택 메모리 데이터는 메소드 종료시 메모리가 해제됩니다. 스택 영역은 LIFO의 구조를 갖고 변수에 새로운 데이터가 할당되면 이전 데이터는 지워진다. 쓰레드 생성시 고유의 스택 메모리가 생성됩니다. 그러므로 쓰레드 간의 스택 메모리 데이터를 공유할 수는 없고 공유하기 위해서는 static 영역이나 heap 영역을 사용해야 합니다.


Stack Overflow가 발생하는 상황들

1. 재귀함수

스택오버플로우의 대표적인 사례로 재귀함수를 예로 들수 있습니다. 

아래의 코드는 전달 받은 임의 수 범위까지 1부터 곱해 나가는 재귀함수입니다.

public long calculateFactorial(long number) {
    return number == 1 ? 1 : number * calculateFactorial(number - 1);
}

문제 발생

재귀 함수를 사용하면 호출한 함수가 종료되지 않은 채 새로운 함수를 호출하므로 스택에 메모리가 계속적으로 저장되게 되므로 스택 메모리에 더 이상 가용 메모리가 없을 경우에 스택 오버 플로우가 발생하게 됩니다.

해결안

함수 호출 시 이전의 호출한 스택 메모리는 종료하면 됩니다. 다음 함수 호출시 현재 함수의 연산된 결과를 전달하면 됩니다. 그러면 함수 종료 시 이전 함수의 데이터와 연산할 필요가 없으니 스택 메모리의 이전 함수의 데이터를 따로 저장할 필요가 없어집니다.

public long calculateTailFactorial(long number, long sum) {
    return number == 1 ? sum : calculateTailFactorial(number - 1, number * sum);
}

이 처럼 현재 연산을 다음 함수에 결과값으로 도출하기위해 파라미터를 추가하였다. 그리고 수행을 해보면 정상적으로 결과값을 도출하는 것을 확인할 수 있습니다.

 

2. 상호 참조

상호 참조란 두 클래스간에 생성을 위임하면서 체이닝을 이루게 되면 발생하게 됩니다. 

아래코드를 봅시다.

public class ClassOne {
    private int oneValue;
    private ClassTwo clsTwoInstance = null;

    public ClassOne() {
        oneValue = 0;
        clsTwoInstance = new ClassTwo();
    }
}

public class ClassTwo {
    private int twoValue;
    private ClassOne clsOneInstance = null;

    public ClassTwo() {
        twoValue = 10;
        clsOneInstance = new ClassOne();
    }
}

ClassOne의 생성자에서 ClassTwo를 생성하고 ClassTwo 생성자에서 ClassOne을 생성하게 되어있습니다. 이런 관계에서 ClassOne을 생성하게 되면 ClassTwo를 생성하게 되고 ClassTwo는 ClassOne을 생성하게 됩니다. 

문제 발생

이와 같이 StackOverflow가 발생하는 것을 볼 수 있습니다.

해결안

상호간의 생성 관계를 만들지 않으면 됩니다. 또는 클래스내에서 인스턴스를 직접 생성하기보다는 주입을 통해서 인스턴스를 생성하면 됩니다.

public void getClassTwo(ClassTwo classTwo){
    this.clsTwoInstance = classTwo;
}

 

3. 본인 참조

본인 참조는 상호 참조와 원인은 비슷합니다. 단순히 본인 클래스 내에서 본인을 생성하면서 무한으로 생성되는 이슈입니다. 아래 코드를 살펴봅시다.

public class AccountHolder {

    private String firstName;
    private String lastName;

    AccountHolder jointAccountHolder = new AccountHolder();
}

이렇게 클래스 내에서 본인 클래스를 다시 생성하게 되면 무한으로 생성하는 로직이 수행되면서 스택 오버 플로우가 발생하게 됩니다.

문제 발생

해결안

클래스내에서 본인 클래스를 직접 생성하지 않으면 됩니다. 본인 참조 또한 주입을 받으면 문제를 해결할 수 있습니다.


참고