---
title: 제네릭 프로시저
tags: [concept, SDF]
sources: [SDF-ch3-generic-procedures, SDF-ch5-evaluation, SDF-ch6-layering]
updated: 2026-04-30
---

# 제네릭 프로시저

## 한 줄 정의

인수의 타입(혹은 임의의 술어 조합)에 따라 적절한 핸들러를 동적으로 선택·호출하며, 기존 구조를 수정하지 않고 새 타입 핸들러를 추가함으로써 가산적으로 확장 가능한 프로시저.

## 핵심 내용

### 컴비네이터에서 제네릭 프로시저로의 이행

Ch2의 컴비네이터는 아름다운 폐쇄 구조를 만들지만, 완성된 "다이아몬드"에 새 면을 추가하기는 어렵다:

> "Systems built by combinators result in beautiful diamond-like systems... but it is very hard to add to a diamond. If a system is built as a ball of mud, it is easy to add more mud."

제네릭 프로시저는 이 균형점을 다르게 설정한다. 구조 자체는 개방적(open)이고, 핸들러는 언제든지 추가 가능하다. "진흙 덩어리"가 아니라 핵심 디스패치 인프라는 단단하되 확장 지점은 열려있는 구조다.

### 술어-디스패치 메커니즘

SDF의 제네릭 프로시저는 **술어-디스패치(predicate dispatch)** 방식을 사용한다. 각 핸들러는 *적용 가능성 명세(applicability specification)*, 즉 인수 각각에 대한 술어 리스트와 함께 등록된다:

```scheme
;; 기본 수치 연산
(define-generic-procedure-handler plus
  (all-args 2 number?)
  (lambda (a b) (+ a b)))

;; 기호 산술 확장
(define-generic-procedure-handler plus
  (any-arg 2 symbolic? number?)
  (lambda (a b) (list '+ a b)))
```

단일 디스패치(OOP의 메서드)가 수신자 하나의 타입만 보는 것과 달리, 이 방식은 **모든 인수의 조합**에 따라 핸들러를 결정한다. 어드벤처 게임 예시(Ch3.5)에서 4인수 `generic-move!`가 이를 잘 보여준다.

### 구현의 세 축

1. **generic-procedure-constructor**: 디스패치 전략을 인자로 받아 제네릭 프로시저 생성기를 반환. 디스패치 전략 자체가 교체 가능한 컴포넌트다.

2. **dispatch-store**: 핸들러 저장·검색 전략을 캡슐화. 단순 선형 스캔, 트라이(trie) 인덱스, 캐시 래핑 등 다양한 구현을 교체 가능.

3. **메타데이터 테이블**: 제네릭 프로시저에 규칙 목록을 "스티키 노트"처럼 첨부. 런타임 확장의 기반.

### 폐쇄성(Closure): 제네릭 산술의 자기 참조

제네릭 프로시저의 폐쇄성 속성을 제대로 활용하면, 확장들이 자기 자신을 기반으로 구축될 수 있다:

```scheme
(let ((g (make-generic-arithmetic make-simple-dispatch-store)))
  (add-to-generic-arithmetic! g numeric-arithmetic)
  (extend-generic-arithmetic! g symbolic-extender)
  (extend-generic-arithmetic! g function-extender)
  (install-arithmetic! g))
```

이 구조에서 `function-extender`가 추가하는 함수 산술은 기호 산술을 포함한 전체 제네릭 산술을 재귀적으로 사용한다. 컴비네이터 방식에서는 이런 자기 참조 구조를 만들기 어렵다.

### 자동 미분: 제네릭 프로시저의 킬러 애플리케이션

Ch3.3의 자동 미분은 제네릭 프로시저의 힘을 가장 인상적으로 보여준다. 미분 객체 `[x, δx]`를 새 타입으로 도입하고, 기존 산술 연산들에 이 타입을 처리하는 핸들러만 추가하면 연쇄 법칙이 자동으로 적용된다. 기존 수치·기호 산술 코드는 한 줄도 수정하지 않는다.

```scheme
;; sqrt 연산에 미분 핸들러 추가
(define diff:sqrt
  (diff:unary-proc sqrt (lambda (x) (/ 1 (* 2 (sqrt x))))))
```

이것이 가산적 프로그래밍의 이상적인 형태다.

### 효율적 디스패치

핸들러가 많아지면 단순 선형 스캔은 느려진다. 두 가지 최적화:

- **트라이(Trie) 인덱스**: 인수 시퀀스를 트라이로 인덱싱하여 첫 번째 인수 타입으로 후보 집합을 좁힘
- **캐시 래핑**: 인수 타입 태그 조합을 키로 이전 디스패치 결과를 캐시. `cache-wrapped-dispatch-store`로 임의의 디스패치 전략에 투명하게 추가 가능

## SDF에서의 등장

- [[SDF-ch3-generic-procedures]]: 핵심 챕터. 술어-디스패치 제네릭 프로시저 전 구현, 자동 미분, 트라이 디스패치, 추상 술어, 어드벤처 게임 예시
- [[SDF-ch5-evaluation]]: `g:eval`이 제네릭 프로시저로 구현됨. 새 표현식 타입을 핸들러 추가만으로 인터프리터에 도입
- [[SDF-ch6-layering]]: 레이어드 프로시저는 제네릭 프로시저를 기반으로 구현됨. "레이어드 데이텀의 decoration은 제네릭 연산을 지원하는 태깅의 일반화"

## 실천 시 주의점

**규칙 충돌과 순서 의존성**: 적용 가능성이 겹치는 두 핸들러가 있을 때, 어느 것이 선택되는지는 추가 순서에 의존할 수 있다. 이 암묵적 의존성은 시스템이 커질수록 관리하기 어려워진다. 의식적으로 술어들 사이의 포함 관계(`set-predicate<=!`)를 명시적으로 선언해 우선순위를 명확히 해야 한다.

**디스패치 비용**: 모든 호출에서 인수 타입을 동적으로 확인하는 비용이 발생한다. 캐싱으로 많이 줄일 수 있지만, 성능이 중요한 내부 루프에는 직접 호출을 고려해야 한다.

**디버깅 어려움**: 어떤 핸들러가 선택됐는지 추적하기 어렵다. 디스패치 로직을 투명하게 볼 수 있는 디버깅 도구 없이는 버그 원인 파악이 힘들다.

**"100 functions on 1 data structure"**: Alan Perlis의 격언처럼, 제네릭 프로시저는 많은 연산이 하나의 공통 인터페이스를 통해 다양한 타입에 적용되는 구조를 지향한다. 반대로 타입별로 별도의 함수 집합을 만드는 것은 제네릭 프로시저의 이점을 살리지 못한다.

## 관련 개념

- [[additive-programming]] — 핸들러 추가가 가산적 확장의 핵심 메커니즘
- [[combinators]] — 컴비네이터의 한계를 극복하는 상보적 접근
- [[layered-data]] — 제네릭 프로시저 위에 구축된 레이어링
- [[domain-specific-language]] — g:eval을 제네릭 프로시저로 만들어 DSL 확장성 확보
- [[partial-information]] — 추상 술어를 통한 타입 태깅이 부분 정보 처리와 연결
