17장 - 특이한 제어 구조
Edited by / (frog-slayer)
17.1. Multiple Returns from a Routine
return
과 exit
문은 원할 때 프로그램이 루틴으로부터 빠져나올 수 있도록 하는 제어 구조들로, 호출된 루틴에서 호출한 루틴으로 제어를 돌려준다. 아래는 return
문의 사용 가이드라인.
- 가독성을 향상시키기 위한
return
사용- 일부 루틴에서는 원하는 결과를 얻는 즉시 호출한 루틴으로 반환하는 것이 좋다.
- 에러를 감지한 경우, 별도의 정리 작업이 필요하지 않은데도
return
을 사용하지 않으면 불필요하게 코드량이 증가할 수 있다.
// C++ Example of a Good Multiple Return from a Routine
Comparison Compare( int value1, int value2 ) {
if ( value1 < value2 ) return Comparison_LessThan;
else if ( value1 > value2 ) return Comparison_GreaterThan;
return Comparison_Equal;
}
- 가드 절(Guard Clauses)를 통해 복잡한 에러 처리를 단순화
- 많은 에러 조건을 확인해야하는 경우, 깊은 중첩이 발생할 수 있다.
- 이 경우, 가드 절(early return, exit)을 활용하면 코드를 더 깔끔하게 만들 수 있다.
'Visual Basic 나쁜 예 (깊은 중첩으로 인한 가독성 저하)
If file.validName() Then
If file.Open() Then
If encryptionKey.valid() Then
If file.Decrypt( encryptionKey ) Then
' lots of code
...
End If
End If
End If
End If
'Visual Basic 좋은 예 (가드절을 통한 가독성 개선)
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
' lots of code
...
- 각 루틴에서
return
문 사용은 최소화- 루틴의 여러 곳에서
return
을 사용하면 코드 흐름을 예측하기 어려울 수 있다. - 가독성이 향상되는 경우에만 적절히 사용할 것
- 루틴의 여러 곳에서
17.2. Recursion
재귀(recursion)는 보통
- 문제의 일부를 쉽게 해결할 수 있고
- 큰 하나의 문제를 더 작은 하위 문제들로 나눌 수 있는 경우
신중하게 사용하면 문제를 푸는 데 큰 도움을 준다.
단, 재귀를 사용하는 경우
- 간단하고 우아하게 해결할 수 있는 경우는 적고
- 그것보다는 간단하고 우아하지만 이해하기 어려운 경우가 많고
- 대부분은 심각하게 복잡한 경우가 많다.
따라서 재귀는 항상 신중하게, 선택적으로 사용해야 한다.
Tips for Using Recursion
- 재귀 종료를 보장할 것
- 루틴에 비-재귀적 경로가 포함되어 있는지를 확인할 것. 즉 재귀의 종료 조건을 두고, 더 이상 재귀가 필요하지 않은 경우 재귀가 꼭 멈출 수 있도록 할 것.
- 안전 카운터(safety counter) 사용
- 어떤 경우, 종료 조건만으로는 재귀 호출을 확실히 제한하기 어려울 수 있다.
- 이런 경우, 안전 카운터를 사용해 일정 횟수 이상의 재귀 호출이 발생하지 않도록 제한할 수 있다.
- 재귀는 한 개의 함수로 제한
- A -> B -> C -> A와 같이 여러 함수가 서로 호출하는 순환 재귀(cyclic recursion)는 디버깅하기 어렵고, 종료 조건을 파악하기도 힘들다.
- 가능하면 한 함수 내로 재귀 호출을 제한하는 것이 바람직하다.
- 스택 사용량을 염두에 두기
- 재귀를 사용하는 경우, 프로그램이 얼마나 많은 스택 공간을 사용하게 될지는 프로그램을 직접 돌리기 전에 미리 알기 어렵다.
- 안전 카운터를 사용하는 경우, 스택 오버플로를 방지할 수 있도록 제한을 정하기
- 재귀 함수 내에서 지역 변수를 할당하는 경우, 특히 메모리를 많이 쓰는 객체를 할당하는 경우에는 스택보다는 힙을 사용할 수 있도록 하기
- 팩토리얼이나 피보나치 수 계산을 위해 재귀 사용하지 않기
- 자주 보이는 재귀 예제들이지만, 재귀로 풀기에는 너무 비효율적이다.
- 반복문으로 푸는 게 낫다.
17.3. goto
The Argument Against goto
s
데이크스트라(Dijkstra)의 주장에 따르면 코드의 품질은 goto
문의 수에 반비례하며, goto
를 사용하지 않는 코드에 대한 정당성 증명이 훨씬 더 쉽다.
- 포매팅 문제
goto
문이 포함된 코드는 포매팅하기 어렵다.- 보통은 들여쓰기를 통해 논리적 구조를 나타내지만, 들여쓰기를 통해
goto
의 논리적 구조를 나타내는 일은 어렵다.
- 컴파일러 최적화 방해
- 어떤 최적화의 경우, 프로그램 내의 몇몇 구문들에 있는 흐름 제어들에 의존한다.
- 무분별한
goto
의 사용은 흐름을 분석하기 어렵게 만들어 코드 최적화를 방해한다. - 소스 코드 수준에서는
goto
가 효율적으로 보일 수 있지만, 컴파일러 최적화를 방해하기에 전체적인 실행 성능은 저하될 수 있다.
goto
가 포함됐다고 해서 코드를 작고 빠르게 만드는 경우는 적다.- 코드는 위레서 아래로 흘러야 한다는 원칙을 위반
- 코드 흐름이 난잡해지고, 유지 보수가 어려워진다.
- 잘 쓰이는 경우라 해도, 한번 도입되면 코드 전반에 퍼져나가게 되므로, 아예 쓰지 않는 게 좋다.
The Argument for goto
s
무차별적으로 사용해도 좋다는 게 아니라 특정한 경우, 신중하게 사용한다면 좋을 때도 있다는 입장이다.
- 잘 사용된
goto
는 중복 코드 문제를 해결하는 데 유용할 수 있다- 중복 코드는 소스 및 실행 파일의 크기를 줄인다.
- 적절히
goto
를 사용하면 중복 코드를 피할 수 있다.
goto
는 자원을 할당하고, 해당 자원들에 대한 연산을 수행하고, 그 자원들을 할당 해제하는 루틴에서 유용하게 쓰일 수 있다.- 에러가 발생했을 때 해당 자원에 대한 할당 해제를 잊어버릴 가능성을 줄이는 데 도움을 준다.
- 어떤 경우에는
goto
를 사용해 빠르고 작은 코드를 만들 수도 있다.
goto
가 문제가 되는 것은, 이를 무분별하게 사용하기 때문이다. 좋은 프로그래밍은 꼭 goto
를 삭제하는 것이 아니라, 문제에 맞는 적절한 제어 구조를 선택하는 것이다.
goto
사용의 유해성에 대한 실험적 증거는 부족하고, VB, C++, Ada를 포함한 현대적 프로그래밍 언어들에서도 여전히 goto
문은 지원되고 있다.
The Phony goto
Debate
사실 goto
문에 대한 논증은 얕은 방식으로 다뤄지는 경우가 많다. goto
를 제거한 코드가 얼마나 쉽게 작성될 수 있는지, 혹은 goto
를 사용하지 않으면 코드에 내용을 추가해야 한다는 식의 논쟁은 별로 쓸모가 없다.
단순한 코드 예시로 goto
가 좋다, 나쁘다는 주장을 할 게 아니라, 보다 복잡한 실제 사례에서, goto
를 신중히 사용할 경우의 장단점을 논의하는 데 집중해야 한다.
Error Processing and goto
s
아래 예는 파일을 삭제하는 루틴으로, 각 파일을 처리할 때마다 에러가 발생할 수 있다. 이렇게 에러가 발생하는 경우에는 해당 자원을 정리해야 한다. goto
를 이용해 에러가 발생할 때마다 레이블로 이동해 자원 정리 코드로 빠르게 접근할 수 있게 한다.
Sub PurgeFiles( ByRef errorState As Error_Code )
Dim fileIndex As Integer
Dim fileToPurge As Data_File
Dim fileList As File_List
Dim numFilesToPurge As Integer
MakePurgeFileList( fileList, numFilesToPurge )
errorState = FileStatus_Success
fileIndex = 0
While ( fileIndex < numFilesToPurge )
fileIndex = fileIndex + 1
If Not ( FindFile( fileList( fileIndex ), fileToPurge ) ) Then
errorState = FileStatus_FileFindError
GoTo END_PROC
End If
If Not OpenFile( fileToPurge ) Then
errorState = FileStatus_FileOpenError
GoTo END_PROC
End If
If Not OverwriteFile( fileToPurge ) Then
errorState = FileStatus_FileOverwriteError
GoTo END_PROC
End If
If Not Erase( fileToPurge ) Then
errorState = FileStatus_FileEraseError
GoTo END_PROC
End If
End While
END_PROC:
DeletePurgeFileList( fileList, numFilesToPurge )
End Sub
아래의 코드들은 goto
를 사용하지 않는 방식으로 위 루틴을 재작성한 것들이다.
- 중첩
if
문Sub PurgeFiles( ByRef errorState As Error_Code ) Dim fileIndex As Integer Dim fileToPurge As Data_File Dim fileList As File_List Dim numFilesToPurge As Integer MakePurgeFileList( fileList, numFilesToPurge ) errorState = FileStatus_Success fileIndex = 0 While ( fileIndex < numFilesToPurge And errorState = FileStatus_Success ) fileIndex = fileIndex + 1 If FindFile( fileList( fileIndex ), fileToPurge ) Then If OpenFile( fileToPurge ) Then If OverwriteFile( fileToPurge ) Then If Not Erase( fileToPurge ) Then errorState = FileStatus_FileEraseError End If Else errorState = FileStatus_FileOverwriteError End If Else errorState = FileStatus_FileOpenError End If Else errorState = FileStatus_FileFindError End If End While DeletePurgeFileList( fileList, numFilesToPurge ) End Sub
goto
를 사용하지 않고 프로그래밍해온 사람들에게는 goto
버전보다 더 읽기 쉬울 것이다. 문제는 중첩이 너무 깊어지면 코드 가독성이 떨어지게 된다는 점이다.
goto
를 사용하면 각 조건을 파악하기 위해 크게 머리를 쓸 필요가 없어진다.
- 상태 변수 상태 변수를 사용해 에러 상태를 추적하고, 에러 발생 시 상태를 설정에 나중에 처리하는 방식이다. 중첩을 피할 수 있고 코드도 읽기 더 쉽다.
Sub PurgeFiles( ByRef errorState As Error_Code )
Dim fileIndex As Integer
Dim fileToPurge As Data_File
Dim fileList As File_List
Dim numFilesToPurge As Integer
MakePurgeFileList( fileList, numFilesToPurge )
errorState = FileStatus_Success
fileIndex = 0
While ( fileIndex < numFilesToPurge ) And ( errorState = FileStatus_Success )
fileIndex = fileIndex + 1
If Not FindFile( fileList( fileIndex ), fileToPurge ) Then
errorState = FileStatus_FileFindError
End If
If ( errorState = FileStatus_Success ) Then
If Not OpenFile( fileToPurge ) Then
errorState = FileStatus_FileOpenError
End If
End If
If ( errorState = FileStatus_Success ) Then
If Not OverwriteFile( fileToPurge ) Then
errorState = FileStatus_FileOverwriteError
End If
End If
If ( errorState = FileStatus_Success ) Then
If Not Erase( fileToPurge ) Then
errorState = FileStatus_FileEraseError
End If
End If
End While
DeletePurgeFileList( fileList, numFilesToPurge )
End Sub
중첩된 if
보다 훨씬 더 이해하기 쉽기는 하지만, 상태 변수에 대한 문서화가 필요하다. 각 상태와 사용처를 잘 정리해, 프로그래머들이 혼동을 일으키지 않을 수 있게 하자.
try-finally
VB나 자바 등에서는 에러 발생가 발생했을 때, 사용 중에 있던 자원을 정리할 수 있도록 try-finally
문을 지원한다.
Sub PurgeFiles()
Dim fileIndex As Integer
Dim fileToPurge As Data_File
Dim fileList As File_List
Dim numFilesToPurge As Integer
MakePurgeFileList( fileList, numFilesToPurge )
Try
fileIndex = 0
While ( fileIndex < numFilesToPurge )
fileIndex = fileIndex + 1
FindFile( fileList( fileIndex ), fileToPurge )
OpenFile( fileToPurge )
OverwriteFile( fileToPurge )
Erase( fileToPurge )
End While
Finally
DeletePurgeFileList( fileList, numFilesToPurge )
End Try
End Sub
goto
를 사용하지 않고도 goto
를 사용할 때보다 훨씬 더 깔끔한 코드가 됐다. 단, 에러를 제대로 감지하려면 try
문 내의 루틴들이 에러 코드가 아니라 예외를 던져야만 한다. 예외가 아닌 다른 방법을 통해 에러를 처리하는 방법들과는 같이 쓰기 어려울 수도 있다.
Comparison of the Approaces
방법 | 장점 | 단점 |
---|---|---|
goto | 간결하고 중복을 피할 수 있음. 불필요한 테스트 없음 | goto 임 |
중첩 if | goto 를 사용하지 않음. | 구조 파악이 어려움 |
상태 변수 | goto , 깊은 중첩이 없음. | 추가적 테스트 도입 |
try-finally | goto , 깊은 중첩이 없음. | 모든 언어에서 사용할 수 있는 건 아님 |
언어에서 try-finally
를 지원하면, 가장 직관적이기 때문에 이 방법을 쓰는 게 좋다. 만약 지원하지 않으면 상태 변수 방법이 (보통은) 더 좋지만, 항상 그런 것만은 아니다.
goto
s and Sharing Code in an else
Clause
아래와 같이 2중 조건문을 만족하는 경우 else
문의 MID_LOOP
코드로 이동하는 경우를 보자. 가독성이 심각하게 떨어진다.
//C++ Example of Sharing Code in an else Clause with a goto
if ( statusOk ) {
if ( dataAvailable ) {
importantVariable = x;
goto MID_LOOP;
}
}
else {
importantVariable = GetValue();
MID_LOOP:
// lots of code
...
}
위 코드는 여러 방식으로 재작성할 수 있다. 그냥 //lots of code
에 있는 코드를 복사-붙여넣기 해도 되겠지만, 더 나은 방법은 //lots of code
부분의 코드를 독립적인 루틴으로 만드는 것이다.
//C++ Example of Sharing Code in an else Clause by Putting Common Code into a Routine
if ( statusOk ) {
if ( dataAvailable ) {
importantVariable = x;
DoLotsOfCode( importantVariable );
}
}
else {
importantVariable = GetValue();
DoLotsOfCode( importantVariable );
}
보통은 위와 같이 루틴을 새로 만드는 방법이 제일 좋지만, 루틴 내에서 중복 코드가 발생하고 있다. 아래와 같이 조건을 재구성하면 새 루틴을 만들지 않으면서 중복 코드까지도 줄일 수 있다.
//C++ Example of Sharing Code in an else Clause Without a goto
if ( ( statusOk && dataAvailable ) || !statusOk ) {
if ( statusOk && dataAvailable ) {
importantVariable = x;
}
else {
importantVariable = GetValue();
}
// lots of code
...
}
Summary of Guidelines for Using goto
s
- 사용하는 언어에서 원하는 제어 구조를 제공하지 않는 경우, 이를 모사하기 위해 사용
- 언어에서 해당 제어 구조를 제공하는 경우에는
goto
를 쓰지 말 것.
- 언어에서 해당 제어 구조를 제공하는 경우에는
- 성능 향상을 위한
goto
의 경우, 실제로 성능 향상에 도움을 줄지를 재볼 것.- 대부분의 경우에는
goto
를 쓰지 않고도, 성능 저하 없이 리팩터링이 가능하다. - 성능 향상이 일어나는 경우 문서화를 잘 해둬, 다른 프로그래머들이 해당
goto
문을 지우지 않도록 하자.
- 대부분의 경우에는
- 제어 구조를 흉내내는 경우가 아니라면
goto
는 루틴 당 한 번만 사용할 것- 뒤가 아니라 앞으로 가는
goto
사용은 자제할 것
- 사용하지 않는 레이블은 삭제할 것
goto
를 사용했을 때, 도달할 수 없는 코드가 발생하지 않도록 할 것goto
라고 절대 쓰면 안되는 건 아니라는 점을 명심하기