48. dynamic 과 DLR: 런타임 바인딩과 콜사이트 캐시
난이도 상내 리뷰 · C#
해설 · C#
해설 — dynamic 과 DLR: 런타임 바인딩과 콜사이트 캐시
난이도: 상
요약
dynamic 표현식은 컴파일 시점에 바인딩되지 않고, 런타임에 실제 값의 런타임 타입을 기준으로 멤버 조회·오버로드 해석이 수행된다. 이 과정은 DLR 의 CallSite<T> 와 바인더(binder)가 담당하며, 결과는 콜사이트에 캐시된다. 예측 출력: Dog / Ov(Animal) / Ov(Dog) / woof / RuntimeBinderException.
핵심 개념
- 정적 바인딩 vs 동적 바인딩: 일반 호출은 컴파일러가 인자의 정적 타입으로 오버로드를 확정한다.
dynamic인자가 끼면 그 호출의 바인딩이 런타임으로 연기되어, 실행 시점의 런타임 타입으로 다시 오버로드 해석한다. - 가상 디스패치는 별개:
Kind()같은 virtual 호출은 dynamic 과 무관하게 항상 런타임 타입의 오버라이드를 부른다. - DLR 콜사이트: 각 dynamic 연산 지점마다 컴파일러가
CallSite<Func<...>>를 생성한다. 첫 호출 시 C# 런타임 바인더가 해당 런타임 타입에 맞는 바인딩을 표현식 트리로 만들어 대리자로 컴파일하고, 콜사이트에 캐시한다.
단계별 내부 동작
- L1
a.Kind()→a의 정적 타입은Animal이지만Kind는 virtual → 런타임 타입Dog의 오버라이드 → "Dog". - L2
ViaStatic(new Dog())→ 파라미터a의 정적 타입이Animal. 내부Ov(a)는 정적 타입 Animal 로 오버로드 해석 →Ov(Animal)→ "Ov(Animal)". (인자의 런타임 타입이 Dog 여도 정적 바인딩은 Animal 을 본다.) - L3
ViaDynamic(new Dog())→ 파라미터d가dynamic. 내부Ov(d)는 런타임 바인딩: 실행 시점d의 런타임 타입이Dog이므로 더 특수한Ov(Dog)선택 → "Ov(Dog)". - L4
x.Bark()→x가 dynamic 이라도 런타임에Dog로 조회되어Bark존재 → "woof". (컴파일러는Bark존재를 검증하지 않는다.) - L5
x.Meow()→ 런타임 타입Dog에Meow없음 → 바인더가 멤버를 찾지 못해RuntimeBinderException을 던진다(컴파일 오류가 아니라 런타임 예외) → catch 되어 "RuntimeBinderException".
콜사이트 캐시 (polymorphic inline cache)
각 콜사이트는 다단계 캐시(L0 대리자 → L1 규칙 집합 → L2 전역)를 갖는다. 처음 어떤 런타임 타입을 만나면 바인더가 "이 타입이면 이렇게 호출하라"는 규칙(테스트 + 대상)을 만들어 캐시한다. 같은 런타임 타입으로 재호출되면 캐시된 규칙의 타입 테스트만 통과시켜 재바인딩 없이 빠르게 실행한다. 여러 타입이 오가면 규칙이 여러 개 쌓여(다형성) 순차 테스트한다. 그래서 첫 호출은 비싸고 이후는 저렴하다.
흔한 오해·함정
dynamic==object아니다:object는 정적 타입으로 멤버가 거의 없다.dynamic은 컴파일 검증을 끄고 런타임 바인딩을 켠다.- 컴파일 안전성 상실:
x.Meow()처럼 오타/부재 멤버가 컴파일에 잡히지 않고 런타임에 터진다. - 성능 비용: 첫 콜사이트 바인딩은 표현식 트리 컴파일을 수반해 비싸다. 핫패스에서 dynamic 남용은 지연·할당을 유발.
- 오버로드 급변: dynamic 인자 하나가 호출 전체의 오버로드 해석을 런타임으로 옮긴다 — 정적 코드와 다른 오버로드가 선택될 수 있다(L2 vs L3).
- 확장 메서드 안 됨: dynamic 대상에는 확장 메서드가 바인딩되지 않는다(정적 using 정보가 런타임에 없음).
면접 포인트
- 정적 바인딩과 dynamic 런타임 바인딩이 같은 인자에 다른 오버로드를 고르는 이유.
- DLR
CallSite/바인더/표현식 트리/콜사이트 캐시(다형 인라인 캐시)의 역할. - dynamic 의 비용·안전성 상실과, 게임 서버 핫패스에서 피해야 하는 이유.