17장: 특이한 제어 구조
17장: 특이한 제어 구조
모든 언어가 제공하지는 않지만, 조심해서 쓰면 유용한 제어 구조들
17.1 루틴에서 여러 번 반환하기
return/exit는 루틴을 끝내고 호출자에게 제어를 돌려주는 정상적인 출구- 저자의 기본 지침은 루틴 안의 반환 수를 최소화하라는 것
- 아래처럼 답이 이미 확정되고 이후 정리 작업도 없어서, 즉시 반환하는 편이 더 읽기 쉬운 제한적 경우에만 여러 반환을 허용
Comparison Compare(int value1, int value2) {
if (value1 < value2) return Comparison_LessThan;
if (value1 > value2) return Comparison_GreaterThan;
return Comparison_Equal;
}
- 오류 조건이 많아 정상 경로가 깊이 중첩되면 guard clause(early return)로 오류를 먼저 걸러냄
- 정상 경로가 들여쓰기 없이 드러나고, 오류 검사와 오류 처리가 가까이 놓임
- 실제 코드에서는 오류 상태 설정·자원 정리가 추가되므로 무조건 더 간결한 것은 아님
' 오류를 먼저 처리해 정상 경로의 중첩을 없앰
If Not file.validName() Then Exit Sub
If Not file.Open() Then Exit Sub
If Not encryptionKey.valid() Then Exit Sub
If Not file.Decrypt(encryptionKey) Then Exit Sub
' 정상 처리
...
- 반환이 많으면 루틴의 끝을 읽을 때 위쪽에서 이미 끝났을 가능성을 계속 추적해야 함
- 따라서 여러 반환은 관례가 아니라 가독성을 분명히 높이는 경우에만 사용
여러 반환을 판단하는 기준
- 반환 직전에 잠금 해제, 트랜잭션 종료, 오류 상태 기록처럼 반드시 해야 하는 정리 작업이 있다면 early return이 그 작업을 건너뛰지 않게 설계해야 함
- RAII/
defer/finally처럼 정리를 구조적으로 보장하는 장치를 우선 활용 - 그렇지 못하면 반환마다 필요한 상태 기록과 정리가 가까이 있는지 확인
- RAII/
- 목표는 기계적으로 출구를 하나로 만들거나 여러 개로 만드는 것이 아니라, 반환 위치·결과·정리 규칙이 한눈에 보이게 하는 것이다. 다만 저자는 그 판단이 애매하면 반환 수를 줄이는 쪽을 권한다.
String readFirstLine(Path path) throws IOException {
// try-with-resources가 모든 return 경로에서 파일을 닫는다.
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line = reader.readLine();
if (line == null) return "";
return line.trim();
}
}
17.2 재귀
- 문제의 작은 부분은 직접 풀 수 있고, 큰 부분은 더 작은 같은 문제로 분해할 수 있을 때 적합
- 퀵정렬처럼 부분 배열을 나눠 같은 정렬을 호출하는 경우
- 맞는 소수의 문제에서는 단순하고 우아하지만, 대부분은 반복문이 더 이해하기 쉬움
- 미로 탐색처럼 가능한 길을 차례로 탐색하고 이미 방문한 위치를 기록하는 문제에는 자연스러움
저자의 기준: 재귀를 “가능하면 피하라”는 뜻은 아니다. 트리·그래프 탐색이나 분할 정복처럼 문제 구조가 자기 유사하면 재귀가 가장 단순할 수 있다. 다만 팩토리얼처럼 반복문으로 더 명확히 풀리는 문제에는 쓰지 말고, 재귀를 선택하기 전에 반복문과 명시적 스택 대안을 비교한다.
void QuickSort(int first, int last, String[] names) {
if (last > first) {
int midpoint = Partition(first, last, names);
QuickSort(first, midpoint - 1, names);
QuickSort(midpoint + 1, last, names);
}
}
재귀 사용 지침
- 재귀를 멈추는 비재귀 경로를 반드시 둘 것 — 종료 조건이 없으면 무한 재귀
- 단순한 종료 검사가 어렵다면, 호출마다 다시 만들어지지 않는 안전 카운터를 매개변수나 멤버로 유지
- 순환 재귀(A → B → C → A)는 파악하기 어려움 — 가능하면 한 루틴 안의 재귀로 제한
- 스택 사용량을 확인할 것
- 안전 한계는 스택 오버플로를 막을 정도로 설정
- 재귀 루틴의 큰 지역 객체는 특히 주의
- 팩토리얼·피보나치처럼 단순 반복으로 명확히 풀리는 문제에는 재귀를 쓰지 말 것
int Factorial(int number) {
int result = 1;
for (int factor = 2; factor <= number; factor++) {
result *= factor;
}
return result;
}
- 재귀와 스택+반복은 서로 대체 가능함 — 둘 다 검토한 뒤 더 이해하기 쉬운 쪽 선택
재귀가 특히 잘 맞는 문제의 형태
- 트리·그래프 탐색, 분할 정복, 중첩된 자료 구조처럼 문제의 모양 자체가 자기 유사할 때 효과적
- 그래프처럼 순환할 수 있는 구조에서는 종료 조건만으로 부족할 수 있음. 미로 탐색의
visited처럼 이미 처리한 상태를 기록해 같은 상태로 다시 들어가지 않게 해야 함 - 안전 카운터는 알고리즘의 종료 조건을 대신하는 장치가 아니라, 예측하지 못한 입력·버그로 인한 스택 오버플로를 막는 최후의 안전망
- 반복으로 바꿀 때는 호출 스택이 하던 일을 명시적 스택에 저장해야 한다. 코드가 더 복잡해진다면 재귀가 복잡도를 실제로 줄이는 경우일 수 있음
boolean hasPath(Node node, Set<Node> visited) {
if (!visited.add(node)) return false; // 이미 방문한 정점: 순환 차단
if (node.isExit()) return true; // 재귀하지 않는 종료 경로
for (Node next : node.neighbors()) {
if (hasPath(next, visited)) return true;
}
return false; // 모든 선택지가 실패
}
17.3 goto
goto를 반대하는 이유
- 들여쓰기로 논리 구조를 표현하기 어렵고, 위에서 아래로 읽히는 흐름을 깨뜨림
- 제어 흐름 분석을 어렵게 해 컴파일러 최적화도 방해할 수 있음
- 허용하면 신중한 사용보다 나쁜 사용이 퍼지기 쉬움
제한적으로 옹호되는 경우
- 현대적인 구조가 없는 언어에서 구조화된 제어를 정확히 흉내 낼 때
- 오류 발생 뒤 공통 자원 정리 코드로 이동해야 하며, 중복 코드나 깊은 중첩보다 명확할 때
- 하지만 대부분은 작은 루틴 분리,
try-finally, 상태 변수, 중첩if, 조건 재구성으로 대체 가능
오류 처리의 대안 비교
- 중첩
if:goto는 없지만 중첩이 깊고, 오류 처리와 검사 위치가 멀어짐 - 상태 변수: 깊은 중첩을 피하고 문제의 진행 순서를 잘 표현하지만 추가 검사가 생김
try-finally: 예외 기반 코드라면 정리 작업을 가장 명확하게 분리하지만, 코드베이스 전체의 일관성이 필요- 프로젝트 전체에서 한 방식을 일관되게 정하고, 개별 상황의 트레이드오프를 고려
goto 사용 지침
- 내장 구조로 대체 가능하면 사용하지 않음
- 효율 때문에 쓴다면 실제 이득을 측정하고 문서화
- 구조를 흉내 내는 경우가 아니면 루틴당 레이블 하나, 전방 이동만 허용
- 모든 레이블이 사용되는지, 도달 불가능한 코드가 생기지 않는지 확인
- 정말 드문 예외라면 의도를 명확히 문서화하고 사용 가능
goto의 평가는 금지 여부만으로 끝나지 않는다. 오류 처리처럼 공통 정리 지점이 필요한 코드는 goto, 중첩 if, 상태 변수, try-finally가 각각 다른 비용을 만든다. 팀의 언어·예외 처리 관례 안에서 가장 읽기 쉽고, 자원 정리를 빠뜨리지 않는 방식을 선택한다.
17.4 특이한 제어 구조에 대한 관점
- 동적
goto, 루틴 중간 진입, 자기 수정 코드처럼 유연성을 우선한 구조는 결국 위험한 것으로 여겨짐 - 소프트웨어 개발은 프로그래머가 할 수 있는 일을 제한해 복잡성을 관리하면서 발전해 왔음
- 따라서 비정상적인 제어 구조는 강한 회의와 대안 검토를 전제로 사용
참고 자료
- 반환: Martin Fowler, Refactoring — guard clause로 중첩 조건문을 치환하는 방법
goto: Dijkstra, Wulf, Knuth, Rubin 등의goto논쟁과 구조적 프로그래밍 자료
체크리스트: 특이한 제어 구조
return
- 반환은 꼭 필요할 때만 쓰는가?
- 반환이 가독성을 높이는가?
재귀
- 재귀를 멈추는 코드가 있는가?
- 안전 카운터로 종료를 보장하는가?
- 재귀를 한 루틴으로 제한했는가?
- 재귀 깊이가 스택 한계 안에 있는가?
- 단순 반복보다 재귀가 더 나은가?
goto
- 최후의 수단으로, 가독성과 유지보수성을 높일 때만 쓰는가?
- 효율 목적이라면 이득을 측정하고 문서화했는가?
- 루틴당 레이블 하나와 전방 이동만 사용하는가?
- 모든 레이블이 실제로 사용되는가?
요점 정리
- 여러 반환은 예외적으로 깊은 중첩을 막고 가독성을 높일 수 있지만, 기본적으로 반환 수를 최소화한다.
- 재귀는 소수의 문제에 우아한 해법을 제공하므로 반복문 대안과 비교해 선택한다.
goto가 읽기 쉽고 유지보수하기 좋은 코드가 되는 경우는 드물며, 최후의 수단으로만 사용한다.