364 lines
11 KiB
Markdown
364 lines
11 KiB
Markdown
# 실무 DI 패턴: kernbench 구현으로 배우는 Dependency Injection
|
|
|
|
---
|
|
|
|
## 슬라이드 1 — 오늘 이야기할 것
|
|
|
|
**질문:** 코드를 어떻게 설계해야 테스트하기 쉽고, 갈아끼우기 쉬울까?
|
|
|
|
**답:** Dependency Injection (DI)
|
|
|
|
오늘은 이론이 아니라 **실제로 돌아가는 시뮬레이터 코드**를 보면서 배웁니다.
|
|
|
|
```
|
|
kernbench
|
|
└── AI 가속기 하드웨어를 Python으로 시뮬레이션하는 프레임워크
|
|
- 수십 개의 하드웨어 컴포넌트 (NOC, HBM, PE, CPU...)
|
|
- 각 컴포넌트는 런타임에 교체 가능
|
|
- 테스트에서 Mock 컴포넌트로 즉시 대체 가능
|
|
```
|
|
|
|
---
|
|
|
|
## 슬라이드 2 — DI가 없으면 어떤 일이 생기나
|
|
|
|
```python
|
|
# ❌ DI 없는 코드
|
|
class IoCpuComponent:
|
|
def run(self, env, nbytes):
|
|
router = PathRouter() # 직접 생성 — 교체 불가
|
|
hbm = HbmCtrlComponent() # 직접 생성 — 교체 불가
|
|
yield env.timeout(10.0)
|
|
```
|
|
|
|
**문제:**
|
|
- 테스트할 때 실제 `PathRouter`와 `HbmCtrl`이 항상 따라온다
|
|
- 컴포넌트를 Mock으로 바꾸려면 **소스 코드를 수정**해야 한다
|
|
- 다른 topology(다른 라우팅 전략)를 쓰고 싶으면 **또 수정**
|
|
|
|
> 클래스가 자기 의존성을 스스로 만들면, 그 클래스는 의존성과 결합된다
|
|
|
|
---
|
|
|
|
## 슬라이드 3 — DI의 핵심 원칙
|
|
|
|
**의존성은 밖에서 만들어서 안으로 넣어준다**
|
|
|
|
```
|
|
┌────────────────────────────┐
|
|
│ 조립자 (Assembler) │ ← 누가 무엇을 쓸지 결정
|
|
│ GraphEngine.__init__ │
|
|
└────────────┬───────────────┘
|
|
│ ctx 주입
|
|
▼
|
|
┌────────────────────────────┐
|
|
│ 컴포넌트 (Component) │ ← 어떻게 동작하는지만 알면 됨
|
|
│ IoCpuComponent │
|
|
│ self.ctx.router.find_path(...) ← 그냥 사용
|
|
└────────────────────────────┘
|
|
```
|
|
|
|
**세 가지 역할 분리:**
|
|
1. **Interface** — 무엇을 할 수 있는가 (`ComponentBase`)
|
|
2. **Implementation** — 어떻게 하는가 (`IoCpuComponent`, `HbmCtrlComponent`, ...)
|
|
3. **Assembler** — 무엇을 연결할 것인가 (`GraphEngine`)
|
|
|
|
---
|
|
|
|
## 슬라이드 4 — 패턴 1: Constructor Injection
|
|
|
|
> 생성자로 의존성을 받는다
|
|
|
|
```python
|
|
# kernbench/components/base.py
|
|
|
|
class ComponentBase(ABC):
|
|
def __init__(self, node: Node, ctx: ComponentContext | None = None):
|
|
self.node = node
|
|
self.ctx = ctx # 외부에서 주입받은 의존성
|
|
self.in_ports: dict[str, simpy.Store] = {}
|
|
self.out_ports: dict[str, simpy.Store] = {}
|
|
```
|
|
|
|
```python
|
|
# 사용 측 — ctx를 직접 만들지 않는다
|
|
class IoCpuComponent(ComponentBase):
|
|
def _dispatch(self, env, txn):
|
|
path = self.ctx.router.find_node_path(...) # ctx는 이미 들어와 있음
|
|
yield self.out_ports[next_hop].put(...)
|
|
```
|
|
|
|
**언제 쓰나:**
|
|
- 컴포넌트가 살아있는 동안 의존성이 바뀌지 않을 때
|
|
- 의존성 없이는 컴포넌트가 동작하지 않을 때 (필수 의존성)
|
|
|
|
---
|
|
|
|
## 슬라이드 5 — Context Object 패턴
|
|
|
|
> 의존성이 많아지면 묶어서 하나로
|
|
|
|
```python
|
|
# kernbench/components/context.py
|
|
|
|
@dataclass
|
|
class ComponentContext:
|
|
router: PathRouter # 라우팅 정책
|
|
resolver: AddressResolver # 주소 해석
|
|
positions: dict[str, ...] # 물리적 위치 정보
|
|
ns_per_mm: float # 전파 지연 상수
|
|
edge_map: dict[...] # 엣지 정보
|
|
spec: dict # 토폴로지 스펙
|
|
```
|
|
|
|
**왜 Context로 묶나?**
|
|
- 생성자 인자가 6개면 → 컴포넌트 추가할 때마다 시그니처 변경
|
|
- Context 하나면 → 새 필드 추가해도 기존 컴포넌트 무영향
|
|
- 컴포넌트는 **필요한 것만 꺼내 쓴다**
|
|
|
|
```python
|
|
class TwoDMeshNocComponent(ComponentBase):
|
|
def _route(self, env, txn):
|
|
src_pos = self.ctx.positions.get(prev_hop) # 위치만 사용
|
|
ns_per_mm = self.ctx.ns_per_mm # 상수만 사용
|
|
# router, resolver 등은 건드리지 않음
|
|
```
|
|
|
|
---
|
|
|
|
## 슬라이드 6 — 패턴 2: Registry + Factory
|
|
|
|
> 문자열 키 → 클래스 매핑으로 런타임 교체
|
|
|
|
```python
|
|
# kernbench/components/base.py
|
|
|
|
class ComponentRegistry:
|
|
_registry: dict[str, type[ComponentBase]] = {}
|
|
|
|
@classmethod
|
|
def register(cls, impl: str, component_cls: type[ComponentBase]):
|
|
cls._registry[impl] = component_cls
|
|
|
|
@classmethod
|
|
def create(cls, node, overrides=None, ctx=None) -> ComponentBase:
|
|
if overrides and node.impl in overrides:
|
|
return overrides[node.impl](node, ctx) # 1순위: 호출자 override
|
|
if node.impl in cls._registry:
|
|
return cls._registry[node.impl](node, ctx) # 2순위: 등록된 구현
|
|
return DefaultComponent(node, ctx) # 3순위: 기본값 fallback
|
|
```
|
|
|
|
**Resolution 우선순위:**
|
|
```
|
|
overrides[impl] ← 테스트/실험용 주입
|
|
↓ (없으면)
|
|
_registry[impl] ← 프로덕션 구현
|
|
↓ (없으면)
|
|
DefaultComponent ← 안전한 fallback
|
|
```
|
|
|
|
---
|
|
|
|
## 슬라이드 7 — Registry 등록 방식
|
|
|
|
```python
|
|
# kernbench/components/impls/__init__.py
|
|
|
|
from kernbench.components.base import ComponentRegistry
|
|
from kernbench.components.impls.noc import TwoDMeshNocComponent
|
|
from kernbench.components.impls.io_cpu import IoCpuComponent
|
|
# ...
|
|
|
|
ComponentRegistry.register("noc_2d_mesh_v1", TwoDMeshNocComponent)
|
|
ComponentRegistry.register("io_cpu_v1", IoCpuComponent)
|
|
ComponentRegistry.register("hbm_ctrl_v1", HbmCtrlComponent)
|
|
# ...
|
|
```
|
|
|
|
**topology.yaml (설정 파일)**
|
|
```yaml
|
|
nodes:
|
|
- id: sip0.cube0.noc
|
|
impl: noc_2d_mesh_v1 # ← 이 문자열이 Registry 키
|
|
```
|
|
|
|
**흐름:**
|
|
```
|
|
YAML → impl 문자열 → Registry.create() → 실제 컴포넌트 인스턴스
|
|
```
|
|
|
|
impl 문자열만 바꾸면 동작이 바뀐다. 코드 수정 없음.
|
|
|
|
---
|
|
|
|
## 슬라이드 8 — 패턴 3: Override Injection (테스트용)
|
|
|
|
> 호출자가 특정 impl만 갈아끼운다
|
|
|
|
```python
|
|
# tests/test_component_registry.py
|
|
|
|
class SpyXbar(ComponentBase):
|
|
calls = 0
|
|
|
|
def run(self, env, nbytes):
|
|
SpyXbar.calls += 1
|
|
yield env.timeout(0)
|
|
|
|
|
|
# 테스트에서 xbar_v1만 SpyXbar로 교체
|
|
engine = GraphEngine(
|
|
graph,
|
|
component_overrides={"xbar_v1": SpyXbar} # ← 이것만 추가
|
|
)
|
|
|
|
result = engine.run(msg)
|
|
assert SpyXbar.calls > 0 # Xbar가 실제로 호출됐는지 검증
|
|
```
|
|
|
|
**핵심:** 테스트 코드가 프로덕션 코드를 **수정하지 않는다**
|
|
|
|
---
|
|
|
|
## 슬라이드 9 — 조립자: GraphEngine
|
|
|
|
> 컴포넌트를 생성하고 연결하는 유일한 곳
|
|
|
|
```python
|
|
# kernbench/sim_engine/engine.py
|
|
|
|
class GraphEngine:
|
|
def __init__(self, graph, component_overrides=None):
|
|
|
|
# 1. 공유 의존성 생성
|
|
ctx = ComponentContext(
|
|
router=PathRouter(graph),
|
|
resolver=AddressResolver(graph),
|
|
positions={nid: n.pos_mm for nid, n in graph.nodes.items()},
|
|
ns_per_mm=...,
|
|
)
|
|
|
|
# 2. 컴포넌트 생성 (DI: ctx 주입)
|
|
self._components = {
|
|
node_id: ComponentRegistry.create(node, overrides, ctx)
|
|
for node_id, node in graph.nodes.items()
|
|
}
|
|
|
|
# 3. 포트 연결 (배선)
|
|
for e in graph.edges:
|
|
store = simpy.Store(self._env)
|
|
self._components[e.src].out_ports[e.dst] = store
|
|
self._components[e.dst].in_ports[e.src] = store
|
|
```
|
|
|
|
**생성 → 주입 → 연결** — 이 세 단계가 한 곳에서만 일어난다
|
|
|
|
---
|
|
|
|
## 슬라이드 10 — 전체 구조 한눈에 보기
|
|
|
|
```
|
|
topology.yaml
|
|
│ impl: "noc_2d_mesh_v1"
|
|
▼
|
|
GraphEngine.__init__() ← 조립자
|
|
│
|
|
├── ComponentContext 생성 ← 공유 의존성 묶음
|
|
│ ├── PathRouter
|
|
│ ├── AddressResolver
|
|
│ └── positions, ns_per_mm, ...
|
|
│
|
|
├── ComponentRegistry.create(node, overrides, ctx)
|
|
│ ├── overrides["noc_2d_mesh_v1"]? → SpyNoc (테스트)
|
|
│ ├── registry["noc_2d_mesh_v1"]? → TwoDMeshNocComponent (프로덕션)
|
|
│ └── fallback → DefaultComponent
|
|
│
|
|
└── 포트 배선: out_ports / in_ports 연결
|
|
|
|
Component (TwoDMeshNocComponent)
|
|
└── self.ctx.positions, self.ctx.ns_per_mm 사용
|
|
(라우터, 리졸버는 건드리지 않음 — 필요한 것만)
|
|
```
|
|
|
|
---
|
|
|
|
## 슬라이드 11 — 무엇을 얻었나
|
|
|
|
| 상황 | DI 없이 | DI 있이 |
|
|
|------|---------|---------|
|
|
| NOC 알고리즘 교체 | 소스 코드 수정 | YAML에서 impl 문자열 변경 |
|
|
| Xbar 동작 검증 | 실제 HW 전부 구동 | `overrides={"xbar_v1": SpyXbar}` |
|
|
| 새 컴포넌트 추가 | 기존 코드 수정 | `register("new_v1", NewComp)` |
|
|
| 컨텍스트 필드 추가 | 모든 생성자 수정 | `ComponentContext`에 필드 추가 |
|
|
| 테스트 격리 | 불가능 | 필요한 것만 override |
|
|
|
|
---
|
|
|
|
## 슬라이드 12 — 실무 적용 체크리스트
|
|
|
|
**설계할 때 물어볼 것:**
|
|
|
|
1. **이 클래스가 직접 `new`(생성)하는 것은 무엇인가?**
|
|
→ 생성하는 것 = 교체할 수 없는 것. 생성자로 받을 수 없는지 검토.
|
|
|
|
2. **의존성이 3개 이상이면?**
|
|
→ Context Object로 묶어라.
|
|
|
|
3. **테스트에서 이 클래스를 단독으로 실행할 수 있는가?**
|
|
→ 없다면 DI가 필요하다는 신호.
|
|
|
|
4. **설정(YAML/config)으로 동작을 바꾸고 싶은가?**
|
|
→ Registry + 문자열 키 패턴.
|
|
|
|
5. **누가 조립하는가?**
|
|
→ 조립자는 하나여야 한다. 컴포넌트 안에 조립 로직이 있으면 안 된다.
|
|
|
|
---
|
|
|
|
## 슬라이드 13 — 안티패턴: 이것은 하지 말자
|
|
|
|
```python
|
|
# ❌ 서비스 로케이터 (컴포넌트 안에서 registry 호출)
|
|
class BadComponent(ComponentBase):
|
|
def run(self, env, nbytes):
|
|
router = ComponentRegistry.get("router") # 컴포넌트가 직접 찾는다
|
|
...
|
|
|
|
# ❌ 전역 싱글톤 직접 참조
|
|
class BadComponent(ComponentBase):
|
|
def run(self, env, nbytes):
|
|
router = GlobalRouter.instance() # 교체 불가
|
|
...
|
|
|
|
# ❌ 생성자 안에서 의존성 생성
|
|
class BadComponent(ComponentBase):
|
|
def __init__(self, node):
|
|
self.router = PathRouter(node.graph) # 테스트에서 격리 불가
|
|
```
|
|
|
|
**공통 문제:** 컴포넌트가 자기 의존성을 스스로 해결한다 → 결합도 증가
|
|
|
|
---
|
|
|
|
## 슬라이드 14 — 요약
|
|
|
|
> **DI = 의존성의 생성과 사용을 분리하는 것**
|
|
|
|
```
|
|
생성 → Registry / Assembler (GraphEngine)
|
|
사용 → Component (IoCpuComponent, TwoDMeshNocComponent, ...)
|
|
```
|
|
|
|
**kernbench에서 배운 패턴 3가지:**
|
|
|
|
1. **Constructor Injection** — 필수 의존성은 생성자로
|
|
2. **Context Object** — 의존성 묶음을 하나의 dataclass로
|
|
3. **Registry + Override** — 문자열 키로 구현체 선택, 테스트에서 교체
|
|
|
|
**결과:** 141개 테스트, YAML 한 줄로 컴포넌트 교체, 프로덕션 코드 수정 없이 Mock 주입
|
|
|
|
---
|
|
|
|
*참고 코드: kernbench/src/kernbench/components/*
|