Error #1023: Stack overflow occurred
이거 혹시 당해 보신 적 없으신가요? 아래 코드를 실행하시면 바로 나옵니다.
function stackover():void{
stackover();
}
stackover();
stack overflow란 스택이 흘러 넘쳤다 라는 뜻이라기 보단 스택이 꽉 찼다 란 의미로 해석할 수 있습니다(저게 양키들에겐 확 와닿는 표현일지도 모르겠지만 콩글리쉬 입장에선 stack memory is full ! 머 이렇게 나왔으면 훨씬 좋지 않았을까 싶습니다 ^^)
어째서 stack이 꽉 찰까요? stack은 언제 사용되나요? 애시당초 stack이란 뭘까요?
stack이란?
stack의 원 뜻은 일정한 장소에 뭔가를 계속 쌓아둔다는 것인데 pile은 무더기로 쌓은 것에 비해 stack은 차곡차곡 쌓인걸 말합니다. 우린 어디에 차곡차곡 물건을 쌓아두면 보통 가장 마지막에 쌓은 것부터 꺼내게 됩니다. 바닥에 있는걸 꺼내려면 너무 힘들기 때문이죠. 이걸 반대로 얘기하면 다음과 같은 두 가지 용도를 도출할 수 있습니다.
- 마지막에 쌓은 거부터 사용하거나
- 애시당초 사용되는 순서가 중요하지 않다.
라는 거죠. 이 경우 stack은 매우 좋은 대안입니다. 쌓는 쪽은 오는 대로 받아서 차곡차곡 쌓기만 하면 되고 쓰는 쪽은 쌓여있는 것의 젤 위에 있는 걸 꺼내 쓰기만 하면 되기 때문에 저장하고 빼서 쓰는 방법 중엔 가장 고속입니다.
프로그램이 stack을 사용하는 경우
위에서 설명한 맥락 그대로 고속으로 처리되길 바랄 때 씁니다. 하지만 쌓고 난 뒤 사용되는 순서가 나중에 쌓인 것 부터 실행되길 원할 때도 사용합니다. 프로그램에서 이러한 순서로 실행되는 대표적인 예가 함수의 호출입니다. 아래 코드를 보죠.
function testA( test:int ):int{
return testB( test + 1000 );
}
function testB( test:int ):int{
return testC( test + 100 );
}
function testC( test:int ):int{
test += 10;
return test;
}
trace( testA(0) ); //1110
위에서 실행한 testA의 경우 다음과 같은 순서로 실행됩니다.
- 먼저 마지막 코드에서 호출한 testA( 0 ) 가 stack 쌓입니다.
- 따라서 이 때 가상의 stack배열을 표현하면 stack[0] = {function:testA, param:0} 요정도가 됩니다.
- 일단 testA내부에 들어오면 인자로 전달한 0의 복사본(새값)이 지역변수(인자)인 test에 잡히고 여기에 1000을 더한 값을 testB에게 전달합니다.
- 따라서 testA는 완료되지 않고 stack에는 testB가 쌓입니다.
- stack[1] = {function:testB, param:1000 }
- testB의 내부에서도 testC를 호출하기 때문에 마찬가지로 testB는 완료되지 않고 testC가 완료되기를 대기하는 형태가 됩니다.
- stack[2] = {function:testC, param: 1100 }
- testC에서는 다른 함수로의 분기가 없기 때문에 순차적으로 로직이 진행되어 인자로 넘어온 1100에 10을 더한 1110을 반환하게 됩니다.
- 일단 최종단점이 testC에서 반환이 시작되면 stack을 거슬러 stack[1]로 올라갑니다. testB는 1110을 반환하게 되고
- 최종적으로 시작점이었던 testA가(stack[0]) 1110을 trace에 반환하게 됩니다.
이러한 함수의 중첩된 호출은 stack메모리를 사용하게 됩니다. 중복을 제거하고 함수나 클래스를 잘게 나누면 보통 아래와 같은 코드가 흔해집니다.
function testA( test:int ):int{
some1();
return testB( test + 1000 ) + calc1( test );
}
function testB( test:int ):int{
test = calc2( test );
some2();
return testC( test + 100 ) + calc3( test );
}
function testC( test:int ):int{
test += 10 + calc4( test );
some3();
return test;
}
testA(0);
위의 코드에서 등장하는 함수는 some시리즈 3개, test시리즈 3개, calc시리즈 4개이므로 stack메모리 블럭은 10개나 필요하게 됩니다.
stack이 꽉 차는 한계선
스펙 문서에 명확히 몇 k의 메모리를 먹게 되면 다운되는지 안 나와있습니다. 따라서 정답은 좀 심하면 다운 당한다 라고 생각 하시는 게 정신건강에 이롭습니다.
stack이 꽉 차는 조건은 크게 두 가지 입니다.
- 함수가 너무 크고 인자의 크기가 너무 크다.
- 함수의 중첩된 호출이 너무 많다.
두 번째 경우는 위의 샘플에서 충분히 보여드렸습니다. 하지만 첫 번째 경우도 제법 당합니다. 인자로 문자열을 보내는데 1메가짜리 문자열 두 개를 보내려 하면 그 함수는 별로 중첩이 없어도 금새 stackMemory를 고갈시킵니다.
함수를 완료 짓기 전에 기억하고 있어야 할 데이터 양이 많기 때문입니다. 함수의 잦은 호출은 stack의 카운터를 증가시키지만 인자나 함수 내부에서 선언한 지역변수의 크기는 개별 stack요소의 크기를 결정합니다.
따라서 복잡한 지역변수와 대규모 데이터를 사용하는 함수가 다중호출 구조를 갖고 있으면 함수 중첩을 몇 개 안 해도 금새 다운 되곤 합니다.
복잡한 stack의 추적
여태 다룬 케이스는 일명 일자 로직으로 stack이 일렬로 쌓였다가 해지되는 경우입니다. 다음과 같은 경우는 시점마다 stack의 사이즈가 다릅니다.
function testA():void{
subA();
testB();
}
function testB():void{
}
function subA():void{
subB();
}
function subB():void{
subC();
}
function subC():void{
}
testA();
위의 코드를 차분히 보면
- testA를 실행시키면 subA가 실행되고
- subA는 subB, subC를 차례로 호출합니다.
- 따라서 subC까지 진행되었을 때 총 stack의 양은 [testA, subA, subB, subC] 의 4개가 됩니다.
- 하지만 subC는 반환되기 시작하여 쭉 subA까지 반환이 되면 stack에는 다시 [testA]만 남게 되어 1개가 됩니다.
- 이후 testB가 실행되는 시점에선 [testA, testB] 가 남게 되어 2개가 됩니다.
- 마지막으로 testB가 반환되고 testA가 반환하기 직전에는 [testA]만 남게 되어 1개가 되겠죠.
- testA가 반환되는 시점으로 stack은 완전히 비워집니다.
즉 stack은 실행 중에 사용량이 수시로 변합니다. 따라서 실제로 stack overflow를 당했을 때의 환경은 매우 복잡한 양상을 띄게 됩니다. 당하기 전이나 후에 브레이크를 걸어보면 stack에 별거 없을 수도 있다는 거죠. 따라서 함수 내부에서도 stack관리를 따로 신경 써 줘야 합니다.
결론
stack overflow는 오직 런타임에만 발생하며 문법적으로 의미적으로 버그가 아닌데 발생합니다.
따라서 다음과 같은 결과를 만들어 냅니다.
- 발생해봐야 깨달음이 옵니다. 보통 설계 시엔 중복제거나 역할분담모델만 고려하기 때문에 실제 실행환경에서 머신이 무한한 stack memory를 갖고 있다고 가정하고 만드는 셈입니다. 경험 많은 개발자가 참여하지 않으면 설계 상의 물리적인 stack한계를 고려했는가 판단하기 쉽지 않습니다.
- 위의 이유로 인해 런타임에러가 특정 상황에서 발생하기도 하고 비특정 상황에서 발생하기도 하기 때문에 일단 디버깅의 첫단계인 버그 재현 자체가 매우 어렵습니다. 발생된 런타임 에러를 재현할 수 없으니 고치기는 더욱 더 힘듭니다.
- 설령 그 런타임버그를 재현하는데 성공했다고 해도 단순히 재귀호출을 잘못해서 걸린 거면 몰라도 많은 경우에 완전한 구조변경을 요구 합니다.
- 대 부분 이 에러가 보고 되는 시점이 거의 완성 단계이므로 설계변경을 요구하는 상태가 되면 엄청난 재난 상황이 됩니다.
어쩌라는 거냐구요?
함수를 작게 작게 쪼개는 건 좋습니다. 하지만 함수 내부에서 다른 함수를 호출할 때 얼마나 중첩되었는지 신경을 써야합니다.
함수 내부에서 다른 함수를 호출한다는 것은 암묵적으로 함수 사이에 의존성이 있다는 의미입니다.
의존성에 대해서 말씀 드리자면 가장 베스트는 의존성이 없는 거고 세컨 베스트는 의존성이 최소화인 것입니다. 이 원칙을 함수 세계에서도 고수하시면 무모한 다중 함수 호출을 요구하는 알고리즘이 적어질 수 있습니다.
하지만 어쩌라는 거냐구요?
그거야 당연히 애시당초 왜 stack overflow를 당했다고 생각하시나요. 그건 무모하게 욕심을 낸 만능 함수를 호출하려고 했기 때문입니다. 과도하게 호스트코드를 단순화하기 위해 거대한 프레임웍을 짜서 프레임웍의 도입점 함수를 만능으로 만들었으니 그 만능함수가 감당해야 할 내용이 너무 많아 무지막지한 stack을 사용한 것이죠.
욕심을 좀 버리시고 호스트코드에도 나눠주시거나 만능 도입점 대신 분산된 프레임웍의 기능별 도입점 함수를 나눠서 만들면 많이 해결됩니다.
관련된 글:
좋은글 감사합니다~
사실 기초중에 기초인데 그간 포스팅을 왜 이것부터 안했나 싶다는.
TAF만들면서 오랜만에 당해서 썼다는.
아..! 오늘 하루를 벌써 알차게 보낸것 같아요 ㅎㅎ
막 개념이 생겼을테니 좀 잊고 있다가 나중에 당하면 다시 와서 읽어봐 ^^
설계의 중요성을 다시 한번 느끼게 되네요.
잘 보고 갑니다^^
사실 무식한 재귀호출이나 프레임웍을 짜거나, 파서를 하드하게 짜는 상황이 아니면 별로 안당할지도 모르겠습니다 ^^
마지막에 언급하신 기능별 도입점 함수는 아래와 같은 경우인가요?
public function method(data:*):*{
if(data is MyData) {
return methodWithMyData(data);
} else if(data is Vector.) {
return methodWithMyDataVector(data);
} else {
throw new Error(“잘못된 인자”);
}
}
private function methodWithMyData(data:MyData)
private function methodWithMyDataVector(data:Vector.)
아뇨 그정도는 스택카운트를 계산해보면 전혀 개선되지 않습니다.
애시당초 이게 스택오버플로를 당했으면 methodWithMyData 내부의 문제입니다.
따라서 저 이름을 보다 구체적으로 예를 들어 initializeWithData 라고 하죠.
그럼 스택오버플로를 막으려면 아래와 같이 분산해야 효과가 있습니다.
initializeWithData1( data );
initializeWithData2( data );
initializeWithData3( data );
그렇군요. 도입점이라는 말이 잘 이해가 되지 않았었습니다.
말그대로 모든 경우에 커버하는 함수보다는 저렇게 기능별함수를 잘게 쪼개는 편이 스텍카운트로 부터 오는 런타임 오류를 줄여줄 수 있다는 말이군요.
넵. 이게 참 프레임웍 짤 때는 유혹이 심하죠 ^^
인자로 보내는 문자열의 크기가 스택의 크기에 영향을 주나요? 참조를 넘기기 때문에 문자열의 크기와는 관계가 없다고 생각하는데요. 제가 잘못알고 있었는지 궁금합니다.
넵 참조로 넘기시면 참조만큼만 영향을 주기 때문에 괜찮습니다. 하지만 문자열은 참조가 아닙니다.
String이란 객체는 말이 객체고 모든 프리머티브형은 반드시 객체를 복사하여 값으로 사용되도록 합니다.
a = new String( ‘aaaa’ );
b = a;
이렇게 쓴다고 b가 a의 참조를 잡냐? 전혀 아니죠. b는 즉시 a의 값을 복사한 새 객체로 환원됩니다.
문자열의 길이가 관계없다고 생각하시는 지식은 자바를 비롯하여 다른 언어에서 String이 객체로 작동하는 것을 생각하신게 아닌가 싶습니다.
몰랐던 것을 알게 되었네요^^ 설명 감사합니다.
인자로 보내는 것 자체가 문제가 되는것이 아니라 받아서 처리하는과정에서 문제가 생길 수는 있겠죠.
기본 데이터 형은 각각 의 한계값을 가지고 있는 경우가 있습니다.
숫자형데이터 타입에서 한계 범위를 벗어난다거나 비트맵 데이터에서 4800픽셀(?) 이상의 값을 처리하려고 한다던가 한번에 로드될 수있는 외부 데이터 양(64kbyte?) 넘었다던가 하면 역시 오버플로우 에 걸리게 되는 것이죠. 플래시 플레이어가 가진 데이터 처리 한계값을 숙지하는것도 중요하다고 생각됩니다.
위에 예시된값은 옛날 값이라 지금은 얼마나 더 free 해졌는지는 잘 모르갰네요.
아, 그 부분은 stack over가 아니라 variable overflow ^^
제가 느끼는 바로는 플래시는 점점 더 네이티브 llvm의 인터페이스를 많이 노출하는 방향으로 가고 있습니다. 따라서 데이터형도 점점 엄격해져가고 있습니다.
세분화된 형을 제공하는 대신 허용범위는 더욱 엄격해지겠죠.
음.. stack 사이즈는 컴파일러 옵션 중 default-script-limits의 첫번째 값(max-recursion-depth)으로 결정될겁니다. 기본 값은 1000.
그리고 variable overflow보단 buffer overflow가 더 친숙한 ㅎㅎ
오 역시 mxmlc의 달인!
버퍼 오버죠 ^^ 이해를 도울라고 ㅋㅋ
그나저나 살아계셨군요.
요즘엔 통 활동이 없으셔서 어디 가신줄 알았음.
아.. 어디 오긴 왔지요. ㅎㅎ
얼마 전에 이사를 했는데 환경이 영 안 좋아서, 게다가 마침 Flash Builder 기간도 expired 되었고 해서, 잠시 휴식기를 가지고 있어요.. ㅎㅎ
(덕분에 한동안 vi로 작업했던..-_-;)
mxml에도 해당 값 설정하는 프로퍼티가 있었던 걸로 기억하는데.. Builder가 없으니 LiveDocs 열어보기가 영 ㅎㅎ
음.. 그러고보니 궁금한 것이.. 인자의 크기가 영향을 미칠까요?
일반적으로는 레퍼런스 형태로 스택에 저장하던 것 같았는데..
작년에도 메모리 구조를 다시 한 번 훑었는데, 또 기억이 안나네요 ㅎㅎ
인자 수 제한의 경우에는 또 플레이어마다 각각 지정된 값이 있긴 했는데..
으음.. 점점 더 멀어져가는 Flex..;;
영향을 마구 미칩니다 ^^
간단하게 시각적으로 확인하시려면 빌더에서 에러를 발생시키면 디버그 창에 스택이 쌓여있는게 보입니다.
쌓여있는 스택을 클릭해서 눌러보시면 함수와 값이 모두 저장되어있는걸 확인하실 수 있습니다.
특히 값의 경우 각 스택별로 같은 변수에 대해서도 전부 다른 값을 저장하고 있는 것도 보실 수 있습니다 ^^
그리고 사실 위에 분이 말씀하셔서 그렇지 스택오버플과 버퍼오버플은 완전히 다른 주제잖아요 ^^
버퍼오버플은 나중에 또 다룰 기회가 있겠죠.
시간이 된다면 프로젝트를 같이 한번 진행해보고 싶군요.
얻는게 더 많을 듯 한 느낌… 때문일까요? ^^
저도요! 근데 호주에 가신다면서요. 부럽부럽
여행을 좋아하지만 요번에는 출장을 핑계로 ㅋㅋ
시간이 되면 항상 새로운 것을 볼 수 있는 기회를 갖으려고 노력하죠.
아이와 같은 사상으로 돌아가게 해주거든요. 순수이성 ㅎ
출장 다녀오면 7월 말이니깐,
8월 초에 도선형과도 아시는거 같은데 같이 한번 뵈요. ^^~
옙.