1. 꼬리 재귀 (Tail Recursion)란?

**꼬리 재귀(Tail Recursion)**는 함수 내에서 재귀 호출이 가장 마지막에 실행되는 작업인 경우를 말합니다. 즉, 재귀 함수를 호출한 후에 그 반환 값을 가지고 추가적인 연산(예: 곱셈, 덧셈, 다른 함수 호출 등)을 수행하지 않고, 재귀 호출의 결과가 바로 현재 함수의 최종 결과가 되어야 합니다.

이러한 구조는 컴파일러가 꼬리 호출 최적화(TCO)를 적용하여 재귀 호출을 반복문(loop)처럼 변환할 수 있게 만들어, 스택 프레임이 계속 쌓이는 것을 방지합니다.

1.1. 꼬리 재귀인 경우의 함수 예시

다음은 팩토리얼을 계산하는 함수를 꼬리 재귀 형태로 작성한 예입니다. tailrec 변경자를 사용하여 코틀린 컴파일러에게 이 함수가 꼬리 재귀임을 알립니다.

예시: 팩토리얼 (꼬리 재귀)

// 꼬리 재귀 형태의 팩토리얼 함수
tailrec fun factorialTailRecursive(n: Int, accumulator: Long = 1L): Long {
    // 기본 사례: n이 1 이하이면, 누적된 결과(accumulator)를 반환
    if (n <= 1) {
        return accumulator
    } else {
        // 재귀 호출이 함수의 가장 마지막 작업입니다.
        // 다음 호출로 (n-1)과 (현재 n * 이전 accumulator) 값을 전달합니다.
        return factorialTailRecursive(n - 1, n * accumulator)
    }
}

fun main() {
    println("5! (꼬리 재귀): " + factorialTailRecursive(5)) // 출력: 120

    // 매우 큰 값도 스택 오버플로 없이 계산 가능합니다.
    // 예를 들어 factorialTailRecursive(20000) 호출도 잘 동작합니다.
    // (결과값이 매우 커서 Long 타입 범위를 초과할 수 있으므로 주의)
    println("20! (꼬리 재귀): " + factorialTailRecursive(20))
}

꼬리 재귀인 이유:factorialTailRecursive 함수에서 else 블록 안의 return factorialTailRecursive(n - 1, n * accumulator) 부분이 핵심입니다.

  1. n - 1n * accumulator라는 다음 호출을 위한 인자들을 계산합니다.
  2. 그리고 factorialTailRecursive 함수를 호출하여 그 결과를 즉시 반환합니다.
  3. 재귀 호출 이후에 어떤 추가적인 연산도 수행되지 않습니다. 이것이 꼬리 재귀의 조건입니다.

1.2. 꼬리 재귀가 아닌 경우의 함수 예시

다음은 동일한 팩토리얼 계산을 하지만, 꼬리 재귀가 아닌 일반적인 재귀 형태로 작성한 예입니다.

예시: 팩토리얼 (꼬리 재귀 아님)

// 일반 재귀 (꼬리 재귀 아님)
fun factorialStandard(n: Int): Long {
    // 기본 사례: n이 1 이하이면 1을 반환
    if (n <= 1) {
        return 1L
    } else {
        // 재귀 호출 factorialStandard(n - 1)의 결과를 받은 후,
        // 그 결과에 현재 n을 곱하는 추가 연산이 있습니다.
        return n * factorialStandard(n - 1)
    }
}

fun main() {
    println("5! (일반 재귀): " + factorialStandard(5)) // 출력: 120

    // 매우 큰 값(예: 20000)에 대해 factorialStandard를 호출하면
    // 스택 오버플로 오류(StackOverflowError)가 발생할 가능성이 높습니다.
    try {
        println("계산 시도: factorialStandard(20000)")
        factorialStandard(20000) // StackOverflowError 발생 가능
    } catch (e: StackOverflowError) {
        println("20000! (일반 재귀): 스택 오버플로 발생!")
    }
}

꼬리 재귀가 아닌 이유: else 블록의 return n * factorialStandard(n - 1) 부분을 보면, factorialStandard(n - 1) 재귀 호출이 먼저 실행되고, 그 함수의 반환 값이 돌아온 후에 현재 n 값과 곱셈 연산이 수행됩니다. 이렇게 재귀 호출 이후에 추가적인 작업이 남아있으면 꼬리 재귀가 아닙니다. 따라서 컴파일러는 이를 단순 반복문으로 최적화할 수 없습니다.

2. 꼬리 호출 최적화 (Tail Call Optimization - TCO)

**꼬리 호출 최적화(TCO)**는 특정 조건을 만족하는 꼬리 재귀 함수에 대해 컴파일러가 수행하는 최적화 기법입니다. 코틀린에서는 함수에 tailrec 변경자를 명시적으로 붙여주면 컴파일러가 이 함수를 TCO 대상으로 간주합니다.

TCO의 핵심은 재귀 호출 시 새로운 스택 프레임을 생성하는 대신, 현재 스택 프레임을 재사용하도록 코드를 변환하는 것입니다. 이는 실질적으로 재귀 함수를 **반복문(loop)**과 유사한 형태로 바꾸는 것과 같습니다. 이 덕분에 아무리 많은 재귀 호출이 일어나도 스택 메모리가 계속 증가하지 않아 스택 오버플로 오류를 방지할 수 있습니다.

2.1. TCO 예시: sumEvenNumbersDownToZero