goto 문
C++에는 이들 외에 프로그램의 실행 흐름을 임의의 위치로 직접 점프시키는 특별한 제어문인 goto
문이 있습니다.
goto
문은 C 언어에서부터 존재했던 기능으로, 초기 프로그래밍 언어에서 자주 사용되었으나 현대 프로그래밍에서는 그 사용이 강력히 권장되지 않습니다.
이 장에서는 goto
문의 작동 방식과 왜 현대 C++ 프로그래밍에서 사용을 지양해야 하는지에 대한 주의사항을 함께 다루겠습니다.
goto
문이란 무엇인가?
goto
문은 프로그램의 실행 흐름을 코드 내의 레이블(label) 이 지정된 특정 위치로 직접 이동시키는 비구조적인 점프 문입니다.
goto 레이블_이름;
// ... (어떤 코드) ...
레이블_이름: // 콜론(:)으로 끝나는 식별자
// 레이블_이름으로 점프했을 때 실행될 코드
레이블_이름
: 유효한 식별자여야 하며, 같은 함수 내에서 유일해야 합니다. 레이블 뒤에는 반드시 콜론(:
)이 붙습니다.goto 레이블_이름;
: 이 문장이 실행되면 프로그램의 실행 흐름은레이블_이름:
이 있는 위치로 즉시 이동합니다.
#include <iostream>
int main() {
int count = 1;
START_LOOP: // "START_LOOP"라는 레이블 정의
std::cout << "현재 카운트: " << count << std::endl;
count++;
if (count <= 3) {
goto START_LOOP; // START_LOOP 레이블로 점프
}
std::cout << "루프 종료." << std::endl; // 루프가 끝난 후 실행될 코드
return 0;
}
위 예시는 for
나 while
문으로 쉽게 구현할 수 있는 기본적인 반복을 goto
문으로 구현한 것입니다.
count
가 3 이하인 동안 START_LOOP
레이블로 계속 점프하여 코드를 반복합니다.
goto
문의 잠재적 위험
goto
문은 프로그램의 흐름을 자유롭게 옮길 수 있는 유연성을 제공하지만 이 유연성이 오히려 큰 단점으로 작용하여 심각한 문제를 야기할 수 있습니다.
그래서 현대 C++ 프로그래밍에서는 goto
문 사용을 거의 금기시하며, 대부분의 경우 더 구조적인 대안을 사용하도록 권장합니다.
1. 스파게티 코드 (Spaghetti Code) 유발:
goto
문을 남용하면 프로그램의 실행 흐름이 예측 불가능하고 복잡하게 얽히게 되어 마치 스파게티처럼 꼬인 코드가 됩니다. 이는 코드를 읽고 이해하기 어렵게 만들며, 논리적 오류를 찾아내고 수정하는 것을 매우 어렵게 만듭니다.
2. 코드의 가독성 저하:
코드의 논리적인 흐름을 추적하기가 매우 어려워집니다. for
, while
, if-else
와 같은 구조적인 제어문은 코드 블록의 시작과 끝이 명확하여 흐름을 쉽게 파악할 수 있지만, goto
는 이러한 구조를 무너뜨립니다.
3. 디버깅의 어려움: 예상치 못한 점프는 프로그램의 상태를 추적하기 어렵게 만듭니다. 특정 버그가 발생했을 때 어떤 경로를 통해 해당 코드에 도달했는지 파악하기가 힘들어집니다.
4. 자원 누수 (Resource Leak) 위험:
goto
문은 스코프(Scope)를 건너뛸 수 있습니다. 예를 들어, 특정 블록에서 자원(예: 메모리, 파일 핸들, 네트워크 소켓)을 할당했는데, goto
를 통해 이 블록을 빠져나가면서 자원 해제 코드가 실행되지 않으면 자원 누수가 발생할 수 있습니다. 이는 프로그램의 안정성과 성능에 치명적입니다.
// (예시: 자원 누수 위험)
void processData() {
int* ptr = new int[100]; // 메모리 할당
if (someErrorCondition) {
goto ERROR_HANDLER; // 에러 발생 시 점프
}
// ... 정상 처리 코드 ...
ERROR_HANDLER:
// 문제가 발생하여 여기까지 점프했지만, delete[] ptr;이 없다면 메모리 누수!
// delete[] ptr; // 이 줄이 없으면 문제가 됨
std::cout << "오류 처리 완료." << std::endl;
} // 함수 종료 시 ptr이 가리키던 메모리는 해제되지 않음 (힙 메모리)
5. 객체 생명주기 문제:
C++에서 객체는 스코프를 벗어날 때 소멸자(Destructor)가 호출되어 정리 작업을 수행합니다. goto
문으로 스코프를 건너뛰면 소멸자가 호출되지 않아 예상치 못한 동작이나 자원 누수가 발생할 수 있습니다.
goto
문의 제한적인 사용 사례
그럼에도 불구하고 goto
문이 아주 드물게 사용되는 몇 가지 상황이 있습니다.
주로 다중 중첩 루프를 한 번에 빠져나가야 할 때입니다.
#include <iostream>
int main() {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
if (i == 1 && j == 1 && k == 1) {
std::cout << "특정 조건 (1,1,1) 만족, 모든 루프 탈출!" << std::endl;
goto END_ALL_LOOPS; // 모든 중첩 루프를 한 번에 벗어남
}
std::cout << "i: " << i << ", j: " << j << ", k: " << k << std::endl;
}
}
}
END_ALL_LOOPS:
std::cout << "모든 루프를 벗어났습니다." << std::endl;
return 0;
}
이러한 상황은 break
문이 가장 안쪽 루프만 종료시키기 때문에 플래그 변수를 사용하거나 함수에서 return
하는 방식으로 해결할 수도 있습니다.
#include <iostream>
int main() {
bool found = false; // 플래그 변수
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
if (i == 1 && j == 1 && k == 1) {
std::cout << "특정 조건 (1,1,1) 만족!" << std::endl;
found = true; // 플래그 설정
break; // 가장 안쪽 루프만 종료
}
std::cout << "i: " << i << ", j: " << j << ", k: " << k << std::endl;
}
if (found) break; // 플래그가 true면 중간 루프 종료
}
if (found) break; // 플래그가 true면 바깥 루프 종료
}
std::cout << "모든 루프를 벗어났습니다." << std::endl;
return 0;
}
이 방법은 goto
보다 코드가 다소 길어질 수 있지만 훨씬 구조적이고 이해하기 쉽습니다.
다중 루프를 포함하는 코드를 별도의 함수로 분리하고, 특정 조건이 만족되면 함수를 return
하여 모든 루프를 한 번에 종료할 수 있습니다.
#include <iostream>
void findAndExit() {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 3; ++k) {
if (i == 1 && j == 1 && k == 1) {
std::cout << "특정 조건 (1,1,1) 만족, 함수 종료!" << std::endl;
return; // 함수를 즉시 종료하여 모든 루프를 벗어남
}
std::cout << "i: " << i << ", j: " << j << ", k: " << k << std::endl;
}
}
}
}
int main() {
findAndExit();
std::cout << "모든 루프를 벗어났습니다." << std::endl;
return 0;
}
이 방법은 코드의 모듈화에도 도움이 되므로, goto
의 강력한 대안으로 사용될 수 있습니다.
결론: goto
문 사용에 대한 권고
goto
문은 C++ 언어에 존재하지만, 특별하고 극히 드문 경우를 제외하고는 거의 사용하지 않는 것이 좋습니다.
코드를 비구조적으로 만들고, 가독성을 해치며, 잠재적인 버그와 자원 누수의 위험을 높이기 때문입니다.
현대 C++ 프로그래밍에서는 if
, else if
, switch
, for
, while
, do-while
, break
, continue
와 같은 구조적인 제어문과 함수 분할(return
)을 통해 goto
없이도 거의 모든 종류의 흐름 제어를 깔끔하고 안전하게 구현할 수 있습니다.
따라서, 초보 프로그래머는 goto
문을 "알아두되, 사용하지 않는" 연산자로 이해하고 넘어가는 것이 바람직합니다.
레거시 코드에서 goto
문을 만나더라도 그 의도를 파악할 수 있도록 개념만 이해하는 정도로 충분합니다.