C#] Pipes and Filter Architectural Pattern
순차적이고 독립적인 processing data 또는 tasks를 위한 modular 하고 유연한 system을 설계하기 위한 목적의 design pattern이다. 여러 단계를 거치며 각 단계별로 특정한 data 변환 또는 operation이 필요한 data streams을 다루는데 매우 유용하다. data processing pipelinex, text processing와 관련된 application에서 일반적으로 사용되는 패턴이다.
어떻게 이 디자인 패턴이 동작하고 기본적인 개념에 대해 살펴보자.
Pipes and Filter Architectural Pattern의 이해
Pipes and Filters pattern은 component-based architectural design pattern이다. Filters라 불리는 여러 components로 구성된다. 각 Filter는 특정 데이터 operation을 실행한다. filters는 개별로 동작하며, pipeline's 순서에서 각 차례에 대한 직접적 지식이 없다. pipes 전체에서 filters들은 분리되어, 하나의 filter에서 다른 filter로 data를 전달하는 배관역할을 한다.
이러한 데이터 처리 방법론은 순차적 order를 따른다. 즉, 단계별 filters를 통해서 data가 처리된다.
이러한 순차적 접근은 modular design을 단순하게 하고, 쉽게 확장할 수 있는 구조이다. 왜냐하면, system 전체에 방해없이 filters를 추가하거나 삭제하는 것이 가능하기 떄문이다.
이제 pattern을 구현해보자.
Example 구현
"Sentiment Analyzer Bot"을 만들어보자. 이 bot의 목적은 입력 text에 대한 sentiment 값("Positive", "Negative", "Neutral")을 제공한다. 이 시나리오는 text-processing pipeline에 적합하다. pipe를 통해 연결될 3 filters를 만들어보자.
Filters 생성
IFilter interface는 sentiment analysis filters의 순차적 개발을 위한 기초이며, processing pipeline으로 통합한다.
public interface IFilter
{
public string Process(string input);
}
input string을 받아 output string을 반환한다. interface를 만들었으니 구현을 해보자.
첫번째 filter는 lowercase이다.
public class LowerCaseFilter : IFilter
{
public string Process(string input)
{
return input.ToLower();
}
}
두번째 filter는 조사, 접속사를 제거한다.
public class RemoveStopWordsFilter : IFilter
{
private static readonly HashSet<string> StopWords = new()
{
"a", "an", "and", "are", "as", "at", "be", "but", "by", "my", "not", "of", "on", "or", "the", "to"
};
public string Process(string input)
{
var words = input.Split(new[] { " ", "\t", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
var filterWords = words.Where(x => !StopWords.Contains(x));
return string.Join(" ", filterWords);
}
}
앞의 두 필터를 거친 다음, 마지막 필터에서 실제 sentiment를 분석한다.
public class SentimentFilter : IFilter
{
public string Process(string input)
{
var positiveWords = new HashSet<string>
{
"good", "great", "awesome", "fantastic", "happy", "love", "like"
};
var negativeWords = new HashSet<string>
{
"bad", "terrible", "awful", "hate", "dislike", "sad"
};
var words = input.Split(new[] { " ", "\t", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
var positiveCount = words.Count(x => positiveWords.Contains(x));
var negativeCount = words.Count(x => negativeWords.Contains(x));
if (positiveCount > negativeCount) return "Positive";
return negativeCount > positiveCount ? "Negative" : "Neutral";
}
}
이제 filter는 모두 만들었고, pipe로 연결해보자.
Pipe 생성
SentimentAnalyzerPipe class에서 filter를 연결하자.
public static class SentimentAnalyzerPipe
{
public static string Analyze(string text)
{
IFilter[] sentimentPipeLine =
{
new LowerCaseFilter(),
new RemoveStopWordsFilter(),
new SentimentFilter()
};
return sentimentPipeLine.Aggregate(text, (current, filter) => filter.Process(current));
}
}
IFilter type의 배열을 초기화해서 sentimentPipeLine을 생성했다. pipe의 각 filter를 순차적으로 실행한다.
초기값 text를 받아서 LowerCaseFilter를 실행하고 그 결과값으로 RemoveStopWordsFilter를 실행하고 그 결과값으로 SentimentFilter를 실행한다.
Aggregate 함수에 대해 아래 링크에서 자세히 설명한다.
C#] System.Linq.Enumerable.Aggregate 함수 (tistory.com)
이제 pipeline을 실행해보자.
Pipe 실행
생성된 pipe를 가지고, 몇몇 text를 분석해보자.
var positiveSentiment = SentimentAnalyzerPipe.Analyze("I am happy");
var negativeSentiment = SentimentAnalyzerPipe.Analyze("I am sad");
var neutralSentiment = SentimentAnalyzerPipe.Analyze("I am ok");
결과는 각각 Positive, Negative, Neutral이다.
적절히 사용하도록, 이 패턴의 장단점을 살펴보자.
장점
1. modularity : 개발, 테스트, 각 component 운영이 쉬워진다.
2. flexibility : filter 추가와 삭제, 교체가 용이하다.
3. reusability : filter는 다른 pipeline에서도 사용이 가능하다.
단점
1. filters간의 데이터 흐름관리와 에러처리가 복잡할 수 있다.
2. data가 여러 filters를 거쳐야하고 추가 processing은 지연속도를 발생시켜, 실시간 처리에 영향을 줄 수 있다.
3. filters의 isolation 때문에, filters 간의 interaction 이나 dependency를 구현하는게 어렵다.
결론
Pipes and Filters pattern은 복잡한 업무를 Filters라 부르는 작은 독립 components로 변경할 수 있다. 그 다음, 이러한 filters를 연결하도록 Pipe를 사용한다.