본 발표는 Ruby의 `callcc` (call with current continuation) 연산자를 중심으로, 이 기능이 제공하는 강력한 가능성과 더불어 내재된 문제점들을 심층적으로 다룹니다. 비록 `callcc`가 Ruby 2.2.2부터 비권장(deprecated)되었고 파이버(Fiber)가 대안으로 제시되었지만, 파이버가 코루틴(coroutine) 기능에 특화되어 `callcc`의 모든 기능을 대체하지 못합니다. 따라서 본 발표에서는 `callcc`가 여전히 유효한 활용 사례가 있음을 DSL(Domain Specific Language) 구현을 통해 보여주고, `callcc`의 한계를 극복할 수 있는 `shift/reset`이라는 제한된 연속(Limited Continuation) 연산자를 소개하며, 이들의 구현 방식과 성능 개선 가능성을 탐구합니다.
연속(Continuation)이란 무엇인가?
연속은 현재 평가 중인 식의 값을 받은 후 수행될 계산에 해당합니다. 즉, 프로그램의 현재 실행 지점 이후에 남은 모든 계산을 의미합니다. Ruby에는 callcc
라는 메서드가 구현되어 있어 현재 시점의 연속을 제1급 값(first-class value)으로 다룰 수 있게 합니다. callcc
를 호출하면 현재의 연속이 블록 인수로 전달되며, 이는 setjmp
와 longjmp
처럼 동작하여 프로그램의 실행 흐름을 자유롭게 제어할 수 있게 합니다.
callcc
의 활용: DSL 구현
callcc
는 백트래킹(backtracking)을 수행하는 DSL 구현에 강력한 도구가 될 수 있습니다. 예를 들어, Haskell의 리스트 모나드(List Monad)처럼 비결정 계산(non-deterministic computation)을 플랫하게 기술하는 DSL을 Ruby에서 구현할 때 callcc
가 유용합니다. 일반적인 Ruby 코드로는 콜백 지옥(callback hell)에 빠지거나 복잡한 메타 프로그래밍(meta-programming)을 통해 구문을 배열로 변환한 후 flat_map
을 적용하는 방식이 필요합니다. 그러나 callcc
를 사용하면 변수를 Ruby의 것을 그대로 활용하면서도 백트래킹을 자연스럽게 표현할 수 있어, 개발자가 DSL을 훨씬 직관적으로 작성할 수 있게 됩니다. 이는 callcc
가 특정 코드 블록을 여러 번 재실행하여 다양한 경우의 수를 탐색하는 데 적합하기 때문입니다.
callcc
의 문제점
그럼에도 불구하고 callcc
에는 몇 가지 중요한 문제가 있습니다. 첫째, callcc
는 현재의 연속을 모두 캡처합니다. 이는 DSL 구현 시 필요한 특정 코드 범위뿐만 아니라 그 이후의 불필요한 코드까지 모두 포함하게 되어 오히려 불편함을 초래합니다. 또한, callcc
로 잘라낸 연속은 한 번 호출되면 돌아오지 않으므로, 원래의 실행 흐름으로 돌아오기 위해 명시적으로 callcc
연속을 다시 호출해야 하는 등 구현이 복잡해집니다. 둘째, callcc
는 성능이 매우 비효율적입니다. 메타 프로그래밍으로 순식간에 끝나는 SEND+MORE=MONEY 퍼즐 같은 문제도 callcc
를 사용하면 몇 분이 지나도 끝나지 않을 수 있습니다. 이는 callcc
가 VM(Virtual Machine)의 전체 상태(스택과 현재 실행 위치)를 스냅샷으로 기록하고 복원하는 과정에서 스택 메모리 복사(memcpy)가 발생하기 때문입니다. Ruby의 callcc
는 부분 백업(partial backup)과 같은 최적화를 시도하지만, 여전히 비용이 많이 드는 작업입니다.
shift/reset
의 등장과 해결책
callcc
의 문제를 해결하기 위해 shift/reset
이라는 제한된 연속 연산자가 제안되었습니다. shift/reset
은 shift
부터 reset
까지의 한정된 범위의 연속만을 잘라냅니다. 이는 callcc
가 전체 연속을 캡처하는 것과 달리, 프로그래머가 원하는 정확한 범위의 연속만을 얻을 수 있게 하여 DSL 구현을 훨씬 단순화합니다. shift/reset
으로 잘라낸 연속은 함수처럼 사용할 수 있으며, reset
지점에 도달하면 원래의 실행 흐름으로 자동으로 돌아옵니다. 이는 flat_map
과 같은 고차 함수와 결합하여 더욱 간결하고 강력한 DSL을 구현할 수 있게 합니다.
놀랍게도 callcc
를 사용하여 shift/reset
을 구현하더라도 성능이 개선됩니다. 이는 shift/reset
의 구조가 Ruby의 callcc
내부 최적화(스택의 필요한 부분만 잘라내는)가 더 잘 적용될 수 있는 형태로 프로그램을 정리하기 때문으로 추정됩니다. 실제로 SEND+MORE=MONEY 퍼즐 해결 시간이 메타 프로그래밍 방식보다는 느리지만(0.14초
vs 0.5초
), callcc
단독 사용 시 몇 분 이상 걸리던 것에 비하면 훨씬 현실적인 수준으로 단축됩니다.
향후 과제
shift/reset
의 잠재력을 최대한 발휘하기 위해서는 C 확장 라이브러리 형태로 VM의 내부 구현에 직접 접근하여 구현하는 것이 필요합니다. shift/reset
은 shift
호출 시 reset
이후에 필요한 스택을 저장할 필요가 없으므로, 이러한 정보를 VM에 전달함으로써 스택 저장 범위를 더욱 제한하고 최적화를 극대화할 수 있습니다. 현재는 C 확장 라이브러리 구현이 진행 중이며, 이를 통해 Ruby에서 연속 연산자의 실용성과 효율성을 더욱 높일 수 있을 것으로 기대됩니다.
결론적으로, Ruby의 `callcc`는 비록 비권장되었지만 DSL 구현과 같은 특정 영역에서 강력한 추상화를 제공하는 잠재력을 지니고 있습니다. 그러나 그 전역적인 특성과 성능 문제는 광범위한 활용을 제약해왔습니다. `shift/reset`은 이러한 `callcc`의 문제점을 해결하는 대안으로, 필요한 범위의 연속만을 다루어 DSL 구현을 단순화하고 성능을 개선하는 데 크게 기여합니다. `shift/reset`의 C 확장 라이브러리 구현은 Ruby에서 연속 연산자의 효율성을 극대화하고, 더욱 강력하고 유연한 DSL 및 프로그래밍 패러다임의 등장을 가능하게 할 것입니다. 이는 Ruby가 단순한 스크립트 언어를 넘어 더욱 풍부한 표현력을 가진 언어로 발전하는 데 중요한 발걸음이 될 것입니다.