Akka Stream의 Working Graph는 데이터 처리 플로우를 레고 블럭처럼 모듈화하고 구성할 수 있는 강력한 도구입니다. 이를 활용하면 복잡한 비동기 데이터 흐름을 간결하고 직관적으로 구성할 수 있습니다. 이에 대한 장단점과 Akka Stream이 제공하는 주요 장치를 설명드리겠습니다.
장점
모듈성 및 재사용성
데이터를 처리하는 각 단계를 작은 블럭(Graph Stage)으로 나누어 조립할 수 있어 코드 재사용성이 높아집니다.
기존의 블럭을 다른 Graph에 손쉽게 삽입하거나 대체할 수 있습니다.
비동기 및 병렬 처리
Akka Stream은 비동기적으로 데이터를 처리하므로 높은 처리량과 낮은 대기시간(Latency)을 보장합니다.
각 단계(Stage)가 독립적으로 병렬 실행될 수 있어 대규모 데이터 처리에 적합합니다.
명확한 데이터 흐름
데이터 플로우가 Graph 형태로 명시되기 때문에 복잡한 처리도 시각적으로 이해하기 쉽습니다.
Source
,Flow
,Sink
를 조합하여 데이터의 시작부터 끝까지의 흐름을 설계할 수 있습니다.
백프레셔 지원
다운스트림이 처리 속도를 따라가지 못할 경우, Akka Stream은 자동으로 백프레셔(Backpressure)를 적용하여 데이터 과부하를 방지합니다.
확장성
Akka의 멀티노드 환경에서 클러스터링 기능을 활용해 스트림을 분산 처리하거나 동적으로 확장할 수 있습니다.
단점
초기 학습 곡선
Graph DSL이나 복잡한 흐름을 설계할 때 처음에는 구조를 이해하고 설계하는 데 시간이 필요합니다.
특히 기존의 Imperative 방식에 익숙한 개발자들에게는 새로운 접근 방식으로 느껴질 수 있습니다.
디버깅 어려움
스트림이 비동기적으로 실행되므로 실행 중에 발생하는 에러를 디버깅하는 데 어려움이 있을 수 있습니다.
데이터 흐름이 복잡할수록 오류의 원인을 추적하기 힘들어질 수 있습니다.
추가 리소스 소비
각 Stage는 별도의 비동기 Task로 실행되므로, 작은 단위로 쪼갤수록 리소스 사용량이 증가할 수 있습니다.
Akka Stream이 제공하는 주요 장치
Graph DSL
복잡한 데이터 흐름을 표현하기 위해 제공되는 도메인 특화 언어(DSL).
Source
,Sink
,Flow
등을 유기적으로 연결해 Directed Graph 형태로 구성.
Source / Sink / Flow
Source: 데이터 스트림의 시작점. (e.g., 파일, 네트워크, 데이터베이스)
Flow: 데이터를 변환하거나 필터링하는 중간 단계.
Sink: 데이터 스트림의 종착점. (e.g., 저장, 출력)
FlexiFlow (Custom Graph Stage)
기본 제공되는 연산자 외에도 사용자가 직접 커스텀 Stage를 구현할 수 있습니다.
Materializer
정의한 Graph를 실행 가능한 스트림으로 변환합니다.
실행 시 필요한 리소스나 스레드를 할당합니다.
Backpressure
다운스트림의 처리 속도를 기준으로 업스트림의 데이터를 조절하여 스트림이 과부하되지 않도록 보장.
Predefined Stages
Akka Stream은 자주 사용되는 연산자를 기본적으로 제공합니다. (e.g.,
map
,filter
,groupBy
,merge
,broadcast
)
Supervision Strategy
스트림 실행 중 발생한 에러를 다루기 위해
Resume
,Restart
,Stop
등의 정책을 제공합니다.
Akka Stream의 Working Graph는 설계 단계에서 약간의 추가 학습이 필요하지만, 복잡한 스트림 처리 시 강력한 모듈성과 유연성을 제공합니다. 특히 백프레셔와 병렬 처리 지원은 대규모 실시간 데이터 처리 시스템에서 큰 장점으로 작용합니다. 😊
Akka Stream의 Working Graph를 이용하는 과정을 살펴보겠습니다.
Graph 설계
데이터 흐름을 Graph를 통해 화이트보드에 먼저그리고 설계된 코드를 일치시키는것이 AkkaStream Graph의 장점입니다.
다음과 같이 생상자의 속도와 안정적인 소비를 위해 다음과 같이 Stream을 Graph를 설계했다고 가정해봅시다.
- Source는 데이터가 처음 입력받은 입력지점이며 Stream으로 다음 단계로 흘려보낼수 있습니다.
- Via를 통해 다음단계가 수행됩니다. 입력된 값에서 +1을 수행합니다.
- 소비자가 생산자의 생산 속도를 못따라 잡는경우 overflow가 발생할수 있습니다. 중간에 안정적 처리를 위한 buffer전략을 채택할수 있습니다. ( 여기서는 오래된 데이터 드랍)
- 필요하면 throttler를 통해 추가적인 tps제약을 할수 있습니다. 소비의 속도에 따른 생산속도를 자동 조절하는 backpresure를 선택할수도 고정 tps를 이용할수 있습니다.
- Sink를 통해 최종 출력으로 연결됩니다. console log를 수행할수도 있고, kafka에 전달될수도 있으며 소비를 처리하는 실행기를 분리해 소비액터로 연결될수도 있습니다.
위와같은 데이트 스트림을 화이트보드로 먼저하고 코드로 일치하는 샘플입니다.
코드구현
private val materializer = Materializer.createMaterializer(context.system) private fun onProcessNumber(command: ProcessNumber): Behavior<GraphCommand> { context.log.info("Received ProcessNumber command with number: ${command.number} - ${context.self.path().name()}") Source.single(command.number) .via(operation) .buffer(1000, OverflowStrategy.dropHead()) .throttle(10, Duration.ofSeconds(1)) .runWith(Sink.foreach { result -> command.replyTo.tell(ProcessedNumber(result)) }, materializer) return this }
- 초기 셋업과정은 복잡할수 있지만~ 복잡한 성능고려 처리를 직관적이고 심플하게 이용할수 있습니다.
- materializer : stream이 수행되는 실행공간으로 액터의 컨텍스트로부터 획득가능하며 , 액터모델없이 외부에서도 사용가능합니다.
TestCode
@Test fun testProcessNumberAdd() { val probe: TestProbe<GraphCommand> = testKit.createTestProbe() val graphActor: ActorRef<GraphCommand> = testKit.spawn(GraphActor.create()) graphActor.tell(ProcessNumber(5, probe.ref)) val response = probe.receiveMessage() as ProcessedNumber assertEquals(6, response.result) }
- Akka TestKit은 흐름을 블락하지 않고 테스트를 하는 테스트기를 기본제공합니다.
- 지정된 Buffer를 넘치게한후 발생하는 시나리오작성도 가능하며 로컬에서 더 안전한 장치가 되기 위해 개선해나갈수 있습니다.
기본 액터모델에 Stream기를 탑재한 전체 샘플코드로 , 액터모델에 강력한 스트림기능을 탑재할수 있습니다.
스트리밍 프로그래밍의 기본철학은 사용자로부터 발생하는 데이터를 모아두었다가 한꺼번에 처리하는 배치방식이 아닌 유체의 흐름처럼 다루어야한다로
실제 존재하는 유체장치(물또는 수도의 흐름을 안정적으로 공급)로 부터 얻은 아이디어가가 장치로 적용되어있습니다.
전체 샘플코드 :
- src : https://github.com/psmon/kopring-reactive-labs/tree/main/KotlinBootReactiveLabs/src/main/kotlin/org/example/kotlinbootreactivelabs/actor/stream
- test : https://github.com/psmon/kopring-reactive-labs/tree/main/KotlinBootReactiveLabs/src/test/kotlin/org/example/kotlinbootreactivelabs/actor/stream