7.3.2 함수의 중첩

스위프트에서는 함수를 중첩하여 사용할 수 있습니다. 이렇게 작성된 함수를 중첩 함수라고 합니다. 함수 내에 작성된 함수는 내부 함수, 내부 함수를 포함하는 바깥쪽 함수는 외부 함수입니다.

함수 내에 작성할 수 있는 내부 함수의 수에는 제한이 없습니다. 외부 함수 내에 여러 개의 내부 함수를 정의할 수도 있고, 외부 함수 내에 작성된 내부 함수에 또 다른 내부 함수를 작성할 수도 있습니다.

외부 함수가 종료되면 내부 함수도 종료되게 되는데 이를 내부 함수의 생명 주기(Life Cycle)라고 합니다. 내부 함수는 일반적으로 외부 함수를 거치지 않으면 접근할 수 없습니다. 이 때문에 내부 함수는 외부의 코드로부터 차단되는 결과를 가져옵니다. 이를 함수의 은닉성이라고 합니다. 중첩된 함수를 구현하면 함수의 은닉성을 높일 수 있습니다. 예제를 보겠습니다.

// 외부 함수
func outer(base: Int) -> String {
    // 내부 함수
    func inner(inc: Int) -> String {
        return "\(inc)를 반환합니다"
    }
    let result = inner(inc: base + 1)
    return result
}

outer(base: 3)
// "4를 반환합니다"

outer는 Int 타입의 값을 인자로 받아 문자열을 반환하는 함수입니다. 이 함수의 내부에는 inner라는 이름의 함수가 작성되어 있는데, 이 함수는 외부에서 참조할 수 없으며 오로지 outer 함수 내부에서만 참조할 수 있습니다. 직접 인자값을 전달할 수도 없습니다. 말하자면 inner 함수는 외부로부터 은닉되어 있습니다.

함수의 생명 주기는 참조 카운트와 관련되어 있습니다. 함수는 참조 카운트가 0에서 1이 되는 순간 생성되어 1 이상인 동안 유지되다가, 0이 되면 소멸하는 과정을 반복합니다. 내부 함수의 경우는 어떨까요?

내부 함수를 참조할 수 있는 곳은 그 함수를 선언해준 외부 함수 이외에는 없습니다. 나머지 외부 범위로부터 내부 함수는 은닉되기 때문입니다. 외부 함수가 실행되면서 내부 함수에 대한 참조가 발생하면 생성되고, 외부 함수가 종료되면서 내부 함수에 대한 참조도 종료되면 내부 함수는 소멸합니다.

// 외부 함수
func outer(base: Int) -> (Int) -> String {
    // 내부 함수
    func inner(inc: Int) -> String {
        return "\(inc)를 반환합니다"
    }
    
    return inner
}

let fn1 = outer(param: 3) // outer()가 실행되고, 그 결과로 inner가 대입됩니다
let fn2 = fn1(30) // inner(inc: 30)과 동일합니다

여기서 주의 깊게 봐야할 점은 은닉성이 있는 내부 함수 inner를 외부 함수의 실행 결과로 반환함으러써 내부 함수를 외부에서도 접근할 수 있는 길이 열렸다는 점입니다. 이제까지 내부에서 정의된 함수 inner는 오로지 외부 함수인 outer를 통해서만 접근할 수 있었습니다. 이로 인해 완벽한 은닉성이 제공되었습니다. 하지만 내부 함수를 이렇게 반환하면 outer 함수의 실행 결과는 내부 함수 inner 그 자체가 됩니다. 이를 할당받은 상수 fn1에는 내부 함수가 대입되므로 fn1을 사용하여 얼마든지 inner를 호출할 수 있습니다.

inner 함수의 생명 주기에도 주의할 필요가 있습니다. 본래 inner는 외부 함수인 outer가 실행 종료되면 소멸하도록 설계되어 있습니다. 즉 원래대로는 다음 구문이 실행되면 inner는 소멸되어야 합니다

let fn1 = outer(param: 3) // outer()가 실행되고, 그 결과로 inner가 대입됩니다

그런데 inner함수가 소멸하지 않고 fn1에 할당된 채로 생명을 유지하다가 (30)이라는 함수 호출 연산자 구문을 만나 실행되는 것을 확인할 수 있습니다. 즉 외부 함수에서 내부 함수를 반환하게 되면 외부 함수가 종료되더라도 내부 함수의 생명이 유지됩니다

그러면 만약 내부 함수에 외부 함수의 지역 상수, 또는 지역 변수가 참조되면 어떻게 될까요? 예제를 살펴봅시다.

func basic(param: Int) -> (Int) -> Int {
    let value = param + 20
    
    func append(add: Int) -> Int {
        return value + add
    }
    
    return append
}

1. let result = basic(param: 10) 
2. result(10) // 40

1번에서 basic 함수가 실행되고 내부 함수인 append가 반환됩니다. basic 함수는 실행이 끝나 종료되지만 반환된 내부 함수 append는 상수 result가 참조하고 있는 까닭에 소멸하지 않고 계속 남아있다가 2번에서 실행됩니다.

궁금한 점은 2번 코드가 실행될 때 value 상수는 존재하지 않아 오류가 발생할 것 같지만, 그렇지 않습니다. 이 현상의 원인은 클로저(Closure) 때문입니다. 정확히는 append 함수가 클로저를 갖기 때문입니다. 클로저를 설명하자면 다음과 같습니다.

  1. 클로저는 두 가지로 이루어진 객체입니다. 하나는 내부 함수이며, 또 다른 하나는 내부 함수가 만들어진 주변 환경입니다.

  2. 클로저는 외부 함수 내에서 내부 함수를 반환하고, 내부 함수가 외부 함수의 지역 변수나 상수를 참조할 때 만들어집니다.

이를 요약하면 "클로저란 내부 함수와 내부 함수에 영향을 미치는 주변 환경(Context)을 모두 포함한 객체이다"입니다. 클로저에 관해서는 다음 챕터에서 자세히 배워보겠습니다.

Last updated