‘Dynamic Pipeline’ 패턴: 실시간 처리를 위한 가변 메서드 체이닝
Source: Dev.to
동적 파이프라인이란?
Dynamic Pipeline은 메서드 체이닝 패턴으로, 실행 중에 처리 단계들을 추가, 제거, 업데이트할 수 있으면서도 깔끔하고 유창한 API를 유지합니다.
Processor (Mutable)
+------------------------------------+
| |
Input ((Data)) ---------> | Filter A → Filter B → Filter C | ---------> Output ((Data))
| |
+------------------------------------+
^ ^ ^
| | |
User Settings / Events -------------+---------+---------+
(add / update / remove)
사용 예시
// Incrementally add processing steps
const processor = new Processor()
.addFilterA(param1) // + Process A
.addFilterB(param2) // + Process B
.addFilterC(param3); // + Process C
// Update parameters later
processor.updateFilterA(newParam);
// Subtract (remove) a process
processor.removeFilter('B');
주요 특성
| 특성 | 설명 |
|---|---|
Addition (add) | 새로운 처리 단계를 언제든지 추가할 수 있습니다. |
Subtraction (remove) | 기존 단계들을 동적으로 분리할 수 있습니다. |
Update (update) | 특정 단계의 매개변수를 전체 파이프라인을 재구성하지 않고 수정할 수 있습니다. |
| Order‑sensitive | 추가 순서가 실행 순서를 결정합니다. |
| Immediate use | .build() 호출이 필요 없으며, 프로세서는 항상 실행 가능한 상태입니다. |
은유: 인간 성장과 경험
const person = new Person()
.addEducation('University')
.addSkill('Programming')
.addExperience('Living abroad')
.addTrauma('Major failure');
인간이 진화하듯이, 여러분은 능력과 경험을 “추가”합니다. 삶은 그 뒤를 더 형성합니다:
// Forgetting a skill (remove)
person.removeSkill('Programming');
// Leveling up through practice (update)
person.updateSkill('Programming', { level: 'expert' });
// Losing something precious (remove)
person.removeExperience('Living abroad');
맥락에 따라 이러한 속성을 활용, 업데이트, 또는 놓아줄 수 있습니다.
Comparison with Existing Patterns
| 패턴 | 작업 | 순서 관련성 | 런타임 변이 |
|---|---|---|---|
| Builder | 구성 | 관련 없음 | .build() 후 불변 |
| Decorator | 래핑 | 관련 있음 | 적용 후 “풀기” 어려움 |
| Middleware | 등록 | 관련 있음 | 초기 등록 후 보통 정적 |
RxJS pipe | 변환 | 관련 있음 | 불변 (항상 새 인스턴스 반환) |
| Chain of Responsibility | 연결 | 관련 있음 | “하나가 처리하고 체인을 멈춤” |
| Dynamic Pipeline | 추가/제거/업데이트 | 관련 있음 | 완전 가변 |
Dynamic Pipeline의 장점은 구조화되고 순서가 지정된 파이프라인과 런타임 조정의 유연성을 균형 있게 결합한다.
사용 방법: 스트로크 안정화
그리기 애플리케이션에서 스트로크 안정화는 원시 포인터 입력에 컨볼루션 기반 필터(예: 가우시안 스무딩, 칼만 필터링)를 적용합니다. 이러한 필터는 사용자 상호작용에 반응해야 합니다.
const pointer = new StabilizedPointer()
.addNoiseFilter(1.5)
.addKalmanFilter(0.1)
.addGaussianFilter(5);
// Adjusting settings via the UI
settingsPanel.onChange(settings => {
pointer.updateNoiseFilter(settings.noiseThreshold);
if (!settings.useSmoothing) {
pointer.removeFilter('gaussian');
}
});
// Dynamic adjustments based on pen velocity
pointer.onVelocityChange(velocity => {
if (velocity > 500) {
// Prioritize performance by removing heavy filters during high‑speed motion
pointer.removeFilter('gaussian');
} else {
pointer.addGaussianFilter(5);
}
});
이 패턴은 해당 분야에만 국한되지 않으며, 가변적이고 순서가 중요한 처리 체인이 필요한 모든 상황에서 활용될 수 있습니다.
Source: …
가능한 구현 전략
1. 배열 기반 파이프라인 관리
파이프라인을 간단한 배열에 보관합니다. 이렇게 하면 추가된 순서가 자연스럽게 유지되고, 순회가 직관적입니다.
class Processor {
constructor() {
this.steps = []; // [{ id: 'A', fn: filterA }, …]
}
addFilterA(param) {
this.steps.push({ id: 'A', fn: data => filterA(data, param) });
return this;
}
// …다른 add/remove/update 메서드…
execute(input) {
return this.steps.reduce((data, step) => step.fn(data), input);
}
}
2. 타입 또는 고유 ID를 통한 식별
각 단계는 타입 문자열('validation', 'transform')이나 고유 식별자(UUID)로 식별할 수 있습니다. 이렇게 하면 맵과 결합했을 때 remove/update 연산이 O(1) 시간이 됩니다.
class Processor {
constructor() {
this.steps = []; // 순서가 보장된 리스트
this.index = new Map(); // id → steps 내 위치
}
addStep(id, fn) {
this.steps.push({ id, fn });
this.index.set(id, this.steps.length - 1);
return this;
}
removeStep(id) {
const pos = this.index.get(id);
if (pos !== undefined) {
this.steps.splice(pos, 1);
this.index.delete(id);
// 이후 항목들의 인덱스를 다시 매김…
}
return this;
}
// updateStep, execute 등
}
3. 내부 변형성을 가진 불변‑유사 API
유창하고 가변적인 파사드를 외부에 제공하면서, 내부 표현은 안전성을 위해 불변(예: 복사‑쓰기)으로 유지합니다. 이렇게 하면 가변성의 편리함을 유지하면서도 스레드 안전성을 포기하지 않을 수 있습니다.
class Processor {
constructor(steps = []) {
this._steps = steps; // 직접 변형되지 않음
}
addFilterA(param) {
const newStep = data => filterA(data, param);
return new Processor([...this._steps, { id: 'A', fn: newStep }]);
}
// remove / update는 새로운 Processor 인스턴스를 반환
}
이제 진정한 가변성(첫 번째와 두 번째 전략)이나 함수형 스타일(여전히 호출자에게는 가변하게 보이는)을 선택해 사용할 수 있습니다.
요약
Dynamic Pipeline 패턴은 다음을 제공합니다:
- Fluent, chainable API – 읽고 쓰기 쉽습니다.
- Runtime mutability – 실행 중에 단계 추가, 제거 또는 업데이트가 가능합니다.
- Order preservation – 호출 순서가 실행 순서를 정의합니다.
- Immediate usability – 별도의 “build” 단계가 필요하지 않습니다.
이 패턴은 Builder의 정적 특성과 Middleware/Decorator 스택의 유연성 사이에 위치하여, 가변적이고 순서가 보장된 처리 체인이 필요한 모든 도메인(그래픽, 데이터 검증, 이벤트 처리 등)에 유용한 도구가 됩니다.
고유 ID를 사용한 타입 관리
각 타입에 고유 식별자를 사용하면 시스템이 더 견고해집니다. 임시 문자열 비교에 의존하는 대신, 객체와 함께 식별자를 저장하고 필요할 때 조회할 수 있습니다.
파이프라인을 단일 함수로 캐시하기
고빈도 실행 시나리오에서는 구성 변경 시 전체 파이프라인을 하나의 함수로 캐시하는 것이 유리할 수 있습니다. 이렇게 하면 런타임 오버헤드를 줄일 수 있지만, 실제 적용 사례는 아직 충분히 검증되지 않았습니다.
최종 생각
이 접근법이 공식적인 “패턴”에 해당하는지는 확신할 수 없습니다—아키텍처 관용구에 가깝거나 많은 개발자가 직관적으로 이미 사용하고 있는 상식적인 기법일 수도 있습니다.
그럼에도 불구하고, 저는 깨끗하고 가독성 높은 API와 동적인 실시간 조정이 필요한 제 특정 사용 사례에 이 방법이 큰 도움이 되었다고 생각합니다.
이 접근법에 대한 확립된 명칭이 있거나, 가변 파이프라인을 유지하면서 발생할 수 있는 잠재적 함정이 있다면 댓글로 의견을 공유해 주시면 감사하겠습니다. 읽어 주셔서 감사합니다.