무엇인가
처리 단계(필터)를 줄로 이어 놓고 데이터를 한 방향으로 흘려보낸다. 각 필터는 앞이 누구고 뒤가 누구인지 모른 채 입력을 받아 출력을 낼 뿐이다.
마스터-슬레이브가 “같은 일을 여럿이 나눠서”라면, 이쪽은 “다른 일을 차례로” 다.
구조
graph LR S[원본] --> F1[필터: 소문자화] --> F2[필터: 구두점 제거] --> F3[필터: 공백 정리] --> D[결과]
C++로 보기
필터를 같은 시그니처의 함수로 통일해 두면 줄로 잇는 일이 그냥 합성이 된다.
#include <algorithm>
#include <cctype>
#include <functional>
#include <iostream>
#include <string>
#include <vector>
using Filter = std::function<std::string(std::string)>;
// 각 필터는 앞뒤를 모른다. 입력 하나 받아 출력 하나 낸다.
std::string toLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
std::string stripPunct(std::string s) {
s.erase(std::remove_if(s.begin(), s.end(),
[](unsigned char c) { return std::ispunct(c) != 0; }),
s.end());
return s;
}
std::string squeezeSpaces(std::string s) {
auto isDoubleSpace = [](char a, char b) { return a == ' ' && b == ' '; };
s.erase(std::unique(s.begin(), s.end(), isDoubleSpace), s.end());
return s;
}
// 파이프: 필터들을 순서대로 통과시킨다.
class Pipeline {
std::vector<Filter> filters_;
public:
Pipeline& add(Filter f) {
filters_.push_back(std::move(f));
return *this;
}
std::string run(std::string input) const {
for (const auto& f : filters_) input = f(std::move(input));
return input;
}
};
int main() {
Pipeline p;
p.add(toLower).add(stripPunct).add(squeezeSpaces);
std::cout << '[' << p.run("Hello,, WORLD!! Nice To Meet You.") << "]\n";
// 순서를 바꾸거나 단계를 빼는 것이 한 줄이다
Pipeline q;
q.add(stripPunct).add(squeezeSpaces);
std::cout << '[' << q.run("Hello,, WORLD!! Nice To Meet You.") << "]\n";
}필터가 서로를 모르기 때문에 순서를 바꾸거나 중간에 하나를 끼워 넣는 일이 싸다. 다만 그 대가로 필터 사이에 넘길 수 있는 것이 하나의 타입으로 고정된다. 중간 단계에서 부가 정보를 실어 보내고 싶어지는 순간 이 패턴이 삐걱대기 시작한다.
언제 쓰나
- 입력을 여러 단계로 변환하고, 그 단계 구성이 바뀔 수 있을 때.
- 각 단계가 독립적이라 따로 테스트하고 싶을 때.
- 스트림처럼 데이터가 계속 들어올 때(단계별로 동시에 돌릴 수 있다).
대가
- 중간에 오류가 나면 어디서 났는지 흐릿하다. 단계마다 맥락을 실어 나르지 않기 때문이다.
- 단계마다 복사가 생기기 쉽다. 위 예제도
std::string을 값으로 주고받는다 (이동 덕에 최악은 아니지만, 큰 데이터라면 뷰나 참조 설계를 따로 해야 한다). - 단계 사이에 상태를 공유해야 하는 요구가 생기면 구조가 무너진다.
실제로 만나는 곳
유닉스 셸 파이프(cat x | grep y | sort), 컴파일러의 전처리 → 파싱 → 최적화 → 코드생성,
이미지·오디오 처리 체인, 그리고 C++20의 ranges 어댑터 체인도 같은 발상이다.