‘Dynamic Pipeline’ 패턴: 실시간 처리를 위한 가변 메서드 체이닝

발행: (2026년 1월 14일 오후 07:09 GMT+9)
9 min read
원문: Dev.to

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와 동적인 실시간 조정이 필요한 제 특정 사용 사례에 이 방법이 큰 도움이 되었다고 생각합니다.

이 접근법에 대한 확립된 명칭이 있거나, 가변 파이프라인을 유지하면서 발생할 수 있는 잠재적 함정이 있다면 댓글로 의견을 공유해 주시면 감사하겠습니다. 읽어 주셔서 감사합니다.

Back to Blog

관련 글

더 보기 »

Go에서 우아한 도메인 주도 설계 객체

❓ Go에서 도메인 객체를 어떻게 정의하시나요? Go는 전형적인 객체‑지향 언어가 아닙니다. Domain‑Driven Design(DDD) 같은 개념을 구현하려고 할 때, 예를 들어 En…

Slices: 마이크로서비스에 적합한 크기

Slices: 마이크로서비스에 적합한 크기 Granularity Trap 마이크로서비스를 도입하는 모든 팀은 결국 같은 벽에 부딪힌다: 서비스는 얼마나 크게 만들어야 할까?