한 줄 정의
데이터를 한 방향으로 흘려보내며 단계별 필터가 차례로 가공하는 단방향 파이프-필터 모놀리스 아키텍처입니다.
쉽게 말하면
유닉스 셸의 cat file.txt | grep ERROR | sort | uniq -c 를 그대로 아키텍처로 옮긴 모습입니다.
각 명령이 하나의 필터 이고, | 가 파이프 입니다. 데이터는 항상 한 방향으로만 흐르고, 각 필터는 자기 일만 하면 다음 단계가 알아서 처리합니다.
함수형 프로그래밍의 map, filter, reduce 체인이나 MapReduce 프로그래밍 모델 역시 같은 사고방식입니다.
왜 이렇게 설계했는가?
- 해결하는 문제: 데이터를 단계별로 변환·검증·저장해야 하는 워크플로우에서, 각 처리 단계를 독립적이고 재사용 가능한 단위로 분리하고 싶을 때를 위한 구조입니다.
- 이게 없다면: 하나의 거대한 함수 안에서 변환 로직, 검증 로직, 저장 로직이 뒤엉켜 한 군데 바꾸면 다른 곳이 깨지는 전형적인 모놀리스가 됩니다.
토폴로지
파이프라인은 파이프(pipe) 와 필터(filter) 두 종류의 요소로만 이뤄집니다.
flowchart LR F1[필터] -->|파이프| F2[필터] F2 -->|파이프| F3[필터] F3 -->|파이프| F4[필터]
파이프는 한 필터의 출력을 다음 필터의 입력으로 단방향(point-to-point) 연결합니다. 필터는 시스템이 수행해야 할 비즈니스 기능 한 조각을 담습니다.
핵심 내용
파이프
파이프는 필터 사이의 통신 경로 입니다. 대개 단방향(unidirectional) 으로 동작하며, 한 필터에서 입력을 받아 다른 필터로 출력하는 점대점(point-to-point) 연결입니다.
파이프를 통해 전달되는 페이로드(payload) 데이터의 형식은 정해져 있지 않습니다. 하지만 고성능을 보장하기 위해 소량의 데이터를 주고받도록 파이프라인을 설계하는 것이 일반적입니다.
배포 방식에 따른 파이프 구현
필터를 개별 서비스로 분산 배포한 경우, 파이프는 REST나 메시징, 스트리밍 등 원격 통신 프로토콜을 사용하여 단방향 원격 호출을 수행합니다.
배포 토폴로지가 모놀리스이든 분산이든, 파이프는 동기와 비동기 두 방식 모두 로 운용할 수 있습니다. 모놀리스 배포에서는 스레드나 내장 메시징이 파이프와 필터의 비동기 통신에 쓰입니다.
필터
필터는 자기 완결적(self-contained) 이고 다른 필터로부터 독립적 이어야 합니다. 한 필터는 보통 단 하나의 작업만 수행합니다. 여러 작업을 조합한 복합 필터(composite filter)는 응집도와 재사용성을 떨어뜨리므로 피해야 합니다.
필터는 역할에 따라 네 가지로 나뉩니다.
생산자(Producer)
파이프라인의 시작점입니다. 들어오는 입력은 없고 출력만 있습니다. 보통 외부 시스템·메시지 큐·API에서 데이터를 가져오는 어댑터 역할을 합니다.
변환기(Transformer)
입력을 받아 일부 또는 전체를 변환한 뒤 다음 단계로 내보냅니다. 함수형 프로그래밍의 map 연산에 해당합니다.
테스터(Tester)
입력을 검사하여 조건을 만족하는 것만 통과시키고, 그렇지 않은 것은 버리거나 다른 경로로 보냅니다. 함수형 프로그래밍의 reduce (혹은 filter) 연산에 해당합니다.
소비자(Consumer)
파이프라인의 종료점입니다. 출력 파이프 없이 결과를 데이터베이스에 저장하거나 화면에 표시하는 등 부수 효과(side effect)를 일으키는 역할을 합니다.
데이터 토폴로지
대부분의 파이프라인 아키텍처는 모놀리스 형태로 배포되므로, 데이터 토폴로지라고 하면 단일 모놀리스 데이터베이스를 떠올리기 쉽습니다. 하지만 꼭 그래야 하는 것은 아닙니다. 이 아키텍처 스타일에서 데이터 토폴로지는 단일 데이터베이스부터 필터당 하나의 데이터베이스에 이르기까지 다양합니다.
아래 예시는 프로덕션 환경에서 실행되는 지속적 적합성 함수(continuous fitness function) 를 위한 파이프라인 아키텍처입니다. 아키텍처를 테스트하는 이 함수는 반응성이나 확장성 같은 운영 특성을 분석합니다.
flowchart LR RDB[(원시 데이터<br/>DB)] --> CR[Capture Raw Data<br/>생산자] CR --> TS[Time Series Selector<br/>변환기] SDB[(선택 규칙<br/>DB)] --> TS TS --> TA[Trend Analyzer<br/>변환기] TA --> ADB[(분석 DB)] TA --> GT[Graphing Tool<br/>소비자] GT --> R[보고서]
Capture Raw Data 필터가 별도의 데이터베이스에서 원시 데이터를 가져오고, Time Series Selector 필터는 또 다른 데이터베이스에서 구성 정보(예: 분석 대상 기간)를 읽어옵니다. Trend Analyzer 필터는 데이터를 분석하여 그 결과를 별도의 분석 DB에 저장하고, 마지막으로 Graphing Tool 필터가 그래픽 보고서를 생성합니다.
이처럼 파이프라인 아키텍처의 데이터베이스는 하나일 수도 있고 여러 개일 수도 있습니다.
클라우드 환경 고려 사항
서버리스 환경에서 파이프라인은 매우 자연스럽게 구현됩니다. 각 필터를 AWS Lambda 함수로 만들고 Step Functions 로 오케스트레이션하는 방식입니다.
{
"Comment": "헌법상 후보들을 추적하고 분석한다.",
"StartAt": "Capture Raw Data",
"States": {
"Capture Raw Data": {
"Type": "Task",
"Resource": "arn:aws:lambda:region:account_id:function:raw_data_capture",
"Next": "Time Series Selector"
},
"Time Series Selector": {
"Type": "Task",
"Resource": "arn:aws:lambda:region:account_id:function:time_series_selector",
"Next": "Trend Analyzer"
},
"Trend Analyzer": {
"Type": "Task",
"Resource": "arn:aws:lambda:region:account_id:function:trend_analyzer",
"Next": "Graphing Tool"
},
"Graphing Tool": {
"Type": "Task",
"Resource": "arn:aws:lambda:region:account_id:function:function_graphing_tool",
"End": true
}
}
}이 시점부터는 엄밀히 말해 모놀리스가 아니라 분산 시스템에 가깝지만, 토폴로지의 개념과 사고방식은 그대로 유지됩니다.
일반적인 위험
필터에 너무 많은 책임 부여
파이프라인 아키텍처의 주요 목표는 시스템의 기능성을 단일 목적 필터들로 분리하는 것입니다. 각 필터는 데이터에 대해 한 가지 구체적인 작업만 수행하고, 이후 처리를 위해 데이터를 다른 필터로 넘겨줍니다.
가장 흔한 위험은 하나의 필터에 너무 많은 책임을 부여하는 것입니다. 각 필터 컴포넌트의 목적을 명확히 함으로써 이 위험을 완화할 수 있습니다.
필터 간 양방향 통신 도입
파이프는 단방향으로만 사용해야 합니다. 이는 필터들의 협업을 피하고 관심사를 명확히 분리하기 위한 것입니다.
만약 양방향 통신을 피할 수 없다면 아키텍처를 재검토해야 합니다. 양방향 통신이 꼭 필요하다는 것은 파이프라인 아키텍처가 적합한 스타일이 아니거나, 필터가 너무 복잡하여 기능성이 올바르게 구분되지 않았음을 보여주는 징조입니다.
오류 조건(error condition) 처리
오류 조건 의 처리와 관련해서도 상당한 위험이 있습니다. 파이프라인 도중에(즉, 처리가 시작되고 아직 완료되기 전에) 오류가 발생했을 때 파이프라인을 적절히 종료하고 복구하는 방법을 판단하기가 어려울 때가 많습니다.
따라서 파이프라인 내에서 발생할 수 있는 모든 치명적인 오류 조건을 미리 파악한 후에 아키텍처를 정의하는 것이 중요합니다.
필터 간 계약 관리
각 파이프에는 다음 필터로 전송할 데이터의 내용과 형식을 규정하는 계약(contract) 이 있습니다.
필터 간의 계약을 변경할 때는, 계약이 적용되는 필터들이 오작동하는 일이 없도록 엄격한 거버넌스와 테스트가 수반되어야 합니다.
거버넌스
필터의 종류(생산자/변환기/테스터/소비자)를 코드 수준에서 강제하면 아키텍처 의도를 보존할 수 있습니다. 자바라면 어노테이션과 컴파일 타임/런타임 체크가 효과적입니다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FilterType {
public FilterTypes value();
}public enum FilterTypes {
PRODUCER,
TESTER,
TRANSFORMER,
CONSUMER
}이렇게 하면 모든 필터 클래스가 자신의 종류를 명시하도록 강제할 수 있습니다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FilterType {
public FilterTypes value();
}
@FilterEntrypoint
@FilterType(FilterTypes.TRANSFORMER)
public class TrendAnalysisFilter { }[FilterEntrypoint]
[FilterType(FilterTypes.TRANSFORMER)]
class TrendAnalysisFilter { }이런 식으로 어노테이션을 사용해 각 필터의 역할을 명시적으로 선언하면, 정적 분석 도구가 잘못된 조합(예: 소비자가 출력을 가지는 경우)을 컴파일 타임이나 빌드 단계에서 잡아낼 수 있습니다.
다만 이 방식은 입력/출력 파이프의 유무 같은 구조적 제약 만 강제할 수 있습니다. 변환기로 선언된 필터 내부에서 테스터 로직을 수행하는 것까지는 정적 분석으로 잡아내기 어렵습니다.
이 거버넌스의 본질적인 가치는 필터의 역할을 명시적으로 선언하게 만들어, 개발자가 설계 의도를 인식하고 잘못된 로직을 넣기 전에 한 번 더 생각하게 하는 데 있습니다.
팀 토폴로지 고려 사항
| 팀 유형 | 적합도 | 이유 |
|---|---|---|
| 스트림 정렬 팀 (기능 전담 팀) | 높음 | 파이프라인 아키텍처는 일반적으로 작고 자기완결적이며, 시스템 전체를 통과하는 단일한 여정 또는 흐름을 나타냅니다. 스트림 정렬 팀은 이 흐름을 처음부터 끝까지 소유합니다 |
| 활성화 팀 (역량 코칭 팀) | 높음 | 모듈성이 높고 기술적 관심사에 따라 필터가 분리되어 있어, 나머지 흐름에 영향을 주지 않고 새 필터를 추가할 수 있습니다. 예를 들어 지속적 적합성 함수 예시에서 시계열 선택기 필터 뒤에 새로운 변환 필터를 추가해서 다른 추세 분석을 수행할 수 있습니다 |
| 난해한 하위시스템 팀 (전문 도메인 팀) | 적합 | 각 필터가 매우 구체적인 작업을 수행하므로 팀원들이 각자 독립적으로(그리고 다른 필터와도 독립적으로) 복잡한 필터 처리를 작업할 수 있습니다. 데이터가 한 방향으로만 전달되므로 자신의 특정 필터 처리에 국한된 복잡성에만 집중할 수 있습니다 |
| 플랫폼 팀 (공통 기반 팀) | 적합 | 높은 수준의 모듈성 덕분에 공통 도구, 서비스, API 및 작업을 유용하게 사용할 수 있습니다 |
장단점
| 아키텍처 특성 | 평가 |
|---|---|
| 전반적인 비용 | 매우 낮음 |
| 분할 방식 | 기술적 |
| 퀀텀 개수 | 1 |
| 단순성 | ★★★★★ |
| 모듈성 | ★★☆☆☆ |
| 유지보수성 | ★★★☆☆ |
| 테스트성 | ★★★☆☆ |
| 배포성 | ★★☆☆☆ |
| 진화성 | ★★★☆☆ |
| 반응성 | ★★★☆☆ |
| 확장성 | ★☆☆☆☆ |
| 탄력성 | ★☆☆☆☆ |
| 내결함성 | ★☆☆☆☆ |
애플리케이션 로직이 여러 필터 유형으로 분리되었다는 점에서 파이프라인 아키텍처 스타일은 기술적 분할 방식의 아키텍처입니다. 또한 일반적으로 모놀리스 배포 단위로 구현되므로 아키텍처 퀀텀 수는 항상 1 입니다.
단순성·비용·모듈성
파이프라인 아키텍처 스타일의 주요 강점은 전반적인 비용, 단순성, 그리고 모듈성입니다. 본질적으로 모놀리스 아키텍처라서 분산 아키텍처 스타일과 관련된 복잡성이 없습니다. 분산 스타일보다 단순하고 이해하기 쉬우며, 구축 및 유지보수 비용이 상대적으로 낮습니다. 아키텍처의 모듈성은 다양한 필터 유형과 변환기 간 관심사 분리를 통해 달성됩니다. 어떤 필터든 다른 필터에 영향을 주지 않고 수정하거나 교체할 수 있습니다.
배포성·테스트성 ★★☆☆☆ / ★★★☆☆
배포성과 테스트성은 평균 수준인데, 이는 필터 유형에 의한 높은 모듈성 덕분입니다. 하지만 일반적으로 파이프라인 아키텍처는 여전히 모놀리스임을 유념해야 합니다. 따라서 의례(ceremony), 위험, 낮은 배포 빈도, 그리고 테스트 커버리지 등에서 단점이 존재합니다.
내결함성 ★☆☆☆☆
파이프라인 아키텍처는 일반적으로 모놀리스 시스템으로 배포되므로 내결함성을 지원하지 않습니다. 파이프라인 아키텍처의 작은 한 부분에서 메모리 부족 오류가 발생하면 애플리케이션 전체가 죽습니다. 더 나아가, 대부분의 모놀리스형 애플리케이션과 마찬가지로 평균 복구 시간(MTTR) 이 길기 때문에 전반적인 가용성이 떨어집니다. 시동(startup) 시간은 대개 분 단위입니다.
탄력성·확장성 ★☆☆☆☆
탄력성과 확장성은 매우 낮게 평가됩니다. 이는 주로 모놀리스 배포 방식 때문입니다. 이 아키텍처 스타일을 비동기 통신을 사용하는 분산 아키텍처로 구현하면 그런 특성들을 크게 향상시킬 수 있습니다. 대신 전반적인 비용과 단순성이 타격을 입는다는 트레이드오프가 존재합니다.
분산 아키텍처로의 전환
낮은 점수를 받은 운영 특성들은 대부분 이 아키텍처를 비동기 통신을 사용하는 분산 아키텍처로 구현해서 개선할 수 있습니다. 이 경우 각각의 필터가 개별 배포 단위가 되고, 파이프는 원격 호출이 됩니다. 대신 단순성이나 비용과 같은 다른 특성들이 나빠지는데, 이는 소프트웨어 아키텍처의 고전적인 트레이드오프에 해당합니다.
적합한 상황
사용하면 좋은 경우
- 처리 단계들이 명확히 분리되고 고정된 순서가 있으며, 결정론적 작업 흐름(항상 단방향)인 모든 시스템 에 적합합니다
- 단순하기 때문에 시간과 예산이 엄격하게 제한된 상황 에도 매우 잘 맞습니다
사용하지 말아야 할 경우
- 파이프가 기본적으로 단방향이므로 필터 간 양방향 통신이 필요한 시나리오 에는 적합하지 않습니다
- 테스터 필터들을 잔뜩 추가하면 비결정론적 작업흐름에도 사용할 수 있지만 권장하지 않습니다. 그런 접근법을 사용하면 비교적 단순한 이 아키텍처 스타일이 지나치게 복잡해져서 유지보수성, 테스트성, 배포성, 신뢰성에 부정적인 영향을 미칠 수 있습니다. 비결정론적 작업흐름이 필요한 상황이라면 이벤트 주도 아키텍처가 훨씬 더 적합합니다
- 높은 확장성, 탄력성, 내결함성 을 요구하는 시스템에는 모놀리스이므로 적합하지 않습니다. 다만 분산 아키텍처 접근법을 사용하면 그런 우려 사항을 완화하는 데 도움이 될 것입니다
예시: 서비스 원격 측정 파이프라인
서비스의 원격 측정(telemetry) 정보가 스트리밍을 통해 아파치 카프카로 전송되는 시스템입니다. 카프카로 스트리밍되는 다양한 종류의 데이터를 파이프라인 아키텍처 스타일을 사용해서 처리합니다.
flowchart LR K[Kafka] --> SC[Service Info Capture<br/>생산자] SC --> D[Duration<br/>테스터] D -->|관련 있음| DC[Duration Calculator<br/>변환기] D -->|관련 없음| U[Uptime<br/>테스터] U -->|관련 있음| UC[Uptime Calculator<br/>변환기] U -->|관련 없음| E[끝] DC --> DBO[Database Output<br/>소비자] UC --> DBO DBO --> M[(MongoDB)]
필터들의 관심사 분리에 주목해야 합니다.
- Service Info Capture (생산자): 카프카 토픽에 연결해서 스트리밍 데이터를 수신하는 것에만 관심을 가집니다
- Duration (테스터): 데이터를 검증하고 다음 파이프로 라우팅할지 여부를 결정하는 데에만 관심을 가집니다. 주어진 데이터가 서비스 요청 기간과 관련이 있으면 변환기 필터인 Duration Calculator 로 전달하고, 그렇지 않으면 테스터 필터인 Uptime 으로 전달합니다
- Uptime (테스터): 데이터가 가동 시간 지표와 관련이 있는지 판정합니다. 관련이 있다면 Uptime Calculator 로 데이터를 보내고, 이 처리 흐름과는 무관한 데이터이므로 파이프라인을 끝냅니다
- Duration Calculator / Uptime Calculator (변환기): 주어진 데이터로 각각의 지표를 계산하고, 수정된 데이터를 소비자 필터인 Database Output 에 전달합니다
- Database Output (소비자): 이를 묶고 MongoDB 데이터베이스에 영구 저장합니다
이 예는 파이프라인 아키텍처의 확장 능력(extensibility) 이 얼마나 좋은지 보여줍니다. 예를 들어 데이터베이스 연결 대기 시간 같은 새로운 지표를 처리해야 한다면, Uptime 필터 뒤에 연결 대기 시간을 위한 테스터 필터와 변환기 필터를 추가하면 됩니다.
내 생각
- 백엔드 실무에서 가장 흔하게 마주치는 파이프라인은 로그/이벤트 가공 파이프라인 입니다. Filebeat → Logstash → Elasticsearch 같은 ELK 스택, 혹은 Kafka Streams의 토폴로지가 거의 그대로 이 모델입니다. “아키텍처 스타일”이라고 거창하게 부르지 않아도 이미 매일 쓰고 있습니다.
- 가장 큰 함정은 테스터 필터의 위치 선정 입니다. 무거운 변환을 한 뒤에 테스터로 걸러내면 비용이 두 배로 듭니다. 가능한 한 파이프라인 앞단에 가벼운 검증 필터를 두어 잘못된 입력을 빨리 떨어뜨리는 게 정석입니다(SQL 옵티마이저의 술어 푸시다운과 같은 원리).
- 서버리스(Lambda + Step Functions, GCP Workflows)와의 궁합이 정말 좋습니다. 다만 단계마다 cold start와 페이로드 크기 제한이 있어, 큰 데이터는 S3/오브젝트 스토리지에 쓰고 메타데이터만 파이프로 흘려보내는 패턴을 거의 강제로 쓰게 됩니다. 이걸 모르고 시작하면 운영 단계에서 비용이 폭발합니다.
더 알아볼 것
- Apache Camel의 라우트 정의 문법과 EIP(Enterprise Integration Patterns) 매핑
- AWS Step Functions vs. GCP Workflows vs. Argo Workflows 비교
- Kafka Streams 토폴로지 DSL이 파이프-필터를 어떻게 표현하는지
- 어노테이션 기반 아키텍처 거버넌스를 ArchUnit 같은 도구로 강제하는 방법
관련 개념
출처
- 마크 리처즈, 닐 포드, 『소프트웨어 아키텍처 The Basics』, 12장 파이프라인 아키텍처 스타일