17장 - 특이한 제어 구조

Edited by / (frog-slayer)

17.1. Multiple Returns from a Routine

returnexit문은 원할 때 프로그램이 루틴으로부터 빠져나올 수 있도록 하는 제어 구조들로, 호출된 루틴에서 호출한 루틴으로 제어를 돌려준다. 아래는 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)는 보통

  1. 문제의 일부를 쉽게 해결할 수 있고
  2. 큰 하나의 문제를 더 작은 하위 문제들로 나눌 수 있는 경우

신중하게 사용하면 문제를 푸는 데 큰 도움을 준다.

단, 재귀를 사용하는 경우

  1. 간단하고 우아하게 해결할 수 있는 경우는 적고
  2. 그것보다는 간단하고 우아하지만 이해하기 어려운 경우가 많고
  3. 대부분은 심각하게 복잡한 경우가 많다.

따라서 재귀는 항상 신중하게, 선택적으로 사용해야 한다.

Tips for Using Recursion

  • 재귀 종료를 보장할 것
    • 루틴에 비-재귀적 경로가 포함되어 있는지를 확인할 것. 즉 재귀의 종료 조건을 두고, 더 이상 재귀가 필요하지 않은 경우 재귀가 꼭 멈출 수 있도록 할 것.
  • 안전 카운터(safety counter) 사용
    • 어떤 경우, 종료 조건만으로는 재귀 호출을 확실히 제한하기 어려울 수 있다.
    • 이런 경우, 안전 카운터를 사용해 일정 횟수 이상의 재귀 호출이 발생하지 않도록 제한할 수 있다.
  • 재귀는 한 개의 함수로 제한
    • A -> B -> C -> A와 같이 여러 함수가 서로 호출하는 순환 재귀(cyclic recursion)는 디버깅하기 어렵고, 종료 조건을 파악하기도 힘들다.
    • 가능하면 한 함수 내로 재귀 호출을 제한하는 것이 바람직하다.
  • 스택 사용량을 염두에 두기
    • 재귀를 사용하는 경우, 프로그램이 얼마나 많은 스택 공간을 사용하게 될지는 프로그램을 직접 돌리기 전에 미리 알기 어렵다.
    • 안전 카운터를 사용하는 경우, 스택 오버플로를 방지할 수 있도록 제한을 정하기
    • 재귀 함수 내에서 지역 변수를 할당하는 경우, 특히 메모리를 많이 쓰는 객체를 할당하는 경우에는 스택보다는 힙을 사용할 수 있도록 하기
  • 팩토리얼이나 피보나치 수 계산을 위해 재귀 사용하지 않기
    • 자주 보이는 재귀 예제들이지만, 재귀로 풀기에는 너무 비효율적이다.
    • 반복문으로 푸는 게 낫다.

17.3. goto

The Argument Against gotos

데이크스트라(Dijkstra)의 주장에 따르면 코드의 품질은 goto문의 수에 반비례하며, goto를 사용하지 않는 코드에 대한 정당성 증명이 훨씬 더 쉽다.

  • 포매팅 문제
    • goto문이 포함된 코드는 포매팅하기 어렵다.
    • 보통은 들여쓰기를 통해 논리적 구조를 나타내지만, 들여쓰기를 통해 goto의 논리적 구조를 나타내는 일은 어렵다.
  • 컴파일러 최적화 방해
    • 어떤 최적화의 경우, 프로그램 내의 몇몇 구문들에 있는 흐름 제어들에 의존한다.
    • 무분별한 goto의 사용은 흐름을 분석하기 어렵게 만들어 코드 최적화를 방해한다.
    • 소스 코드 수준에서는 goto가 효율적으로 보일 수 있지만, 컴파일러 최적화를 방해하기에 전체적인 실행 성능은 저하될 수 있다.
  • goto가 포함됐다고 해서 코드를 작고 빠르게 만드는 경우는 적다.
  • 코드는 위레서 아래로 흘러야 한다는 원칙을 위반
    • 코드 흐름이 난잡해지고, 유지 보수가 어려워진다.
    • 잘 쓰이는 경우라 해도, 한번 도입되면 코드 전반에 퍼져나가게 되므로, 아예 쓰지 않는 게 좋다.

The Argument for gotos

무차별적으로 사용해도 좋다는 게 아니라 특정한 경우, 신중하게 사용한다면 좋을 때도 있다는 입장이다.

  • 잘 사용된 goto는 중복 코드 문제를 해결하는 데 유용할 수 있다
    • 중복 코드는 소스 및 실행 파일의 크기를 줄인다.
    • 적절히 goto를 사용하면 중복 코드를 피할 수 있다.
  • goto는 자원을 할당하고, 해당 자원들에 대한 연산을 수행하고, 그 자원들을 할당 해제하는 루틴에서 유용하게 쓰일 수 있다.
    • 에러가 발생했을 때 해당 자원에 대한 할당 해제를 잊어버릴 가능성을 줄이는 데 도움을 준다.
  • 어떤 경우에는 goto를 사용해 빠르고 작은 코드를 만들 수도 있다.

goto가 문제가 되는 것은, 이를 무분별하게 사용하기 때문이다. 좋은 프로그래밍은 꼭 goto를 삭제하는 것이 아니라, 문제에 맞는 적절한 제어 구조를 선택하는 것이다.

goto 사용의 유해성에 대한 실험적 증거는 부족하고, VB, C++, Ada를 포함한 현대적 프로그래밍 언어들에서도 여전히 goto 문은 지원되고 있다.

The Phony goto Debate

사실 goto 문에 대한 논증은 얕은 방식으로 다뤄지는 경우가 많다. goto를 제거한 코드가 얼마나 쉽게 작성될 수 있는지, 혹은 goto를 사용하지 않으면 코드에 내용을 추가해야 한다는 식의 논쟁은 별로 쓸모가 없다.

단순한 코드 예시로 goto가 좋다, 나쁘다는 주장을 할 게 아니라, 보다 복잡한 실제 사례에서, goto를 신중히 사용할 경우의 장단점을 논의하는 데 집중해야 한다.

Error Processing and gotos

아래 예는 파일을 삭제하는 루틴으로, 각 파일을 처리할 때마다 에러가 발생할 수 있다. 이렇게 에러가 발생하는 경우에는 해당 자원을 정리해야 한다. 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를 사용하지 않는 방식으로 위 루틴을 재작성한 것들이다.

  1. 중첩 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를 사용하면 각 조건을 파악하기 위해 크게 머리를 쓸 필요가 없어진다.

  1. 상태 변수 상태 변수를 사용해 에러 상태를 추적하고, 에러 발생 시 상태를 설정에 나중에 처리하는 방식이다. 중첩을 피할 수 있고 코드도 읽기 더 쉽다.
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보다 훨씬 더 이해하기 쉽기는 하지만, 상태 변수에 대한 문서화가 필요하다. 각 상태와 사용처를 잘 정리해, 프로그래머들이 혼동을 일으키지 않을 수 있게 하자.

  1. 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
중첩 ifgoto를 사용하지 않음.구조 파악이 어려움
상태 변수goto, 깊은 중첩이 없음.추가적 테스트 도입
try-finallygoto, 깊은 중첩이 없음.모든 언어에서 사용할 수 있는 건 아님

언어에서 try-finally를 지원하면, 가장 직관적이기 때문에 이 방법을 쓰는 게 좋다. 만약 지원하지 않으면 상태 변수 방법이 (보통은) 더 좋지만, 항상 그런 것만은 아니다.

gotos 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 gotos

  • 사용하는 언어에서 원하는 제어 구조를 제공하지 않는 경우, 이를 모사하기 위해 사용
    • 언어에서 해당 제어 구조를 제공하는 경우에는 goto를 쓰지 말 것.
  • 성능 향상을 위한 goto의 경우, 실제로 성능 향상에 도움을 줄지를 재볼 것.
    • 대부분의 경우에는 goto를 쓰지 않고도, 성능 저하 없이 리팩터링이 가능하다.
    • 성능 향상이 일어나는 경우 문서화를 잘 해둬, 다른 프로그래머들이 해당 goto문을 지우지 않도록 하자.
  • 제어 구조를 흉내내는 경우가 아니라면
    • goto는 루틴 당 한 번만 사용할 것
    • 뒤가 아니라 앞으로 가는 goto 사용은 자제할 것
  • 사용하지 않는 레이블은 삭제할 것
  • goto를 사용했을 때, 도달할 수 없는 코드가 발생하지 않도록 할 것
  • goto라고 절대 쓰면 안되는 건 아니라는 점을 명심하기