Composable Systems를 이해하기 위한 Executable Chain 구축
I’m ready to translate the article for you, but I need the text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have the article text, I’ll translate it into Korean while preserving the original formatting, markdown, and code blocks.
영감
LangChain과 같은 프레임워크를 탐색하면서 반복되는 아이디어를 발견했습니다: 단일한 로직을 작성하는 대신, 실행이 체인으로 연결하고 재사용 및 재배열이 가능한 구성 가능한 단계들로 분할됩니다.
이 글은 LangChain 자체에 관한 것이 아니라, 기본 아이디어를 이해하기 위해 처음부터 최소한의 실행 가능한 체인 추상화를 구축하는 내용입니다.
왜 이렇게 할까?
실행 체인에 대한 정신 모델 없이 작업하면 다음과 같은 어려움에 직면하게 됩니다:
- 깊게 중첩된 조건문
- 하드코딩된 실행 순서
- 모든 것을 처리하는 거대한 메서드
그 결과, 전체 작업 체인은 깨지기 쉬우며 테스트가 어렵고 재배열도 힘들어집니다.
각 단계를 정확히 하나의 일을 수행하는 단위로 생각한다면, 체인의 기본 빌딩 블록에 도달하게 됩니다.
이러한 정신 모델을 갖춘 상태에서, 이를 지원하기 위한 가장 작은 추상화를 설계해 봅시다.
실행 가능한 체인(Executable Chain)이란?
이름에서 알 수 있듯이, 실행 가능한 체인은 작은 논리 단위들을 순서대로—또는 재배열해서—배치하여 더 큰 작업을 수행할 수 있게 만든 시퀀스입니다.
아래의 간단한 주문 처리(Order Processing) 체인을 살펴보세요.
각 단계는 하나의 명확한 프로세스를 담당하며, 체인은 순차적으로 실행됩니다.
Executable 은 다음과 같은 특성을 가집니다:
- 입력을 받는다
- 작업을 수행한다
- 출력을 반환한다
그리고 다음과 같은 속성을 갖는다:
- Composable (조합 가능)
- Testable (테스트 가능)
- Predictable (예측 가능)
Core 추상화 설계
Executable.java
@FunctionalInterface
public interface Executable {
ChainAction execute(ChainContext context) throws ChainException;
}
이 인터페이스는 체인의 기본 구성 요소입니다. 로직을 execute 메서드 안에 넣고 ChainContext를 사용해 현재 체인의 상태를 추적합니다. 메서드는 ChainAction을 반환하며, 이는 체인이 다음 단계로 진행할지 혹은 실행을 중단할지를 결정합니다.
ChainAction을 반환함으로써 흐름 제어가 명시적으로 되며, 개별 Executable은 더 큰 체인 구조에 대해 알 필요가 없습니다.
ChainAction.java
public enum ChainAction {
CONTINUE,
STOP
}
두 가지 가능한 동작을 선언한 자체 설명적인 열거형입니다.
- CONTINUE – 다음 단계로 이동
- STOP – 현재 체인을 그대로 중단
ChainException.java
public class ChainException extends RuntimeException {
// Other fields can be added for metadata
}
필요에 따라 추가 메타데이터를 담을 수 있는 사용자 정의 체크되지 않은 예외입니다.
ChainContext.java
public class ChainContext {
private final Map values;
public ChainContext() {
this.values = new HashMap<>();
}
public <T> void setValue(String key, T value) {
values.put(key, value);
}
public Object getValue(String key) {
return values.get(key);
}
@SuppressWarnings("unchecked")
public <T> T getValue(String key, Class<T> clazz) {
Objects.requireNonNull(clazz);
return (T) values.get(key);
}
}
이 클래스는 Executable에 들어오고 나가는 데이터를 일반화합니다.
입력값은 키‑값 쌍으로 설정하고, 출력값도 동일한 방식으로 처리합니다.
NOTE:
Map을 사용하면 타입 안전성을 포기하고 유연성을 얻습니다. 이는 추상화를 단순하고 범용적으로 유지하기 위한 의도적인 선택입니다.
Chain.java 로 전체 제어하기
이제 빌딩 블록을 정의했으니, 전체 실행을 조율하는 클래스를 만들 수 있습니다.
public class Chain {
private final String name;
private final List<Executable> executables;
private ChainContext context;
private Chain(String name) {
this.name = name;
this.executables = new ArrayList<>();
this.context = new ChainContext();
}
public static Chain of(String name) {
Objects.requireNonNull(name);
return new Chain(name);
}
public Chain next(Executable executable) {
executables.add(executable);
return this;
}
public Chain context(ChainContext context) {
this.context = context;
return this;
}
public void run() throws ChainException {
System.out.println("Executing Chain : " + name);
for (var e : executables) {
ChainAction state = e.execute(context);
if (state == ChainAction.STOP) break;
}
}
public void printChain() {
var s = executables.stream()
.map(e -> e.getClass().getSimpleName())
.collect(Collectors.joining(" --> "));
System.out.println(s);
}
}
구성 요소
name– 체인의 식별자.executables– 순서가 지정된 단계 목록.- Fluent API –
of(String),next(Executable),context(ChainContext)를 사용하면 가독성 있게 체인을 구축할 수 있습니다. run()– 각Executable이 반환하는ChainAction을 고려하면서 실행 목록을 순회합니다.printChain()– 체인을StepA --> StepB --> StepC형태로 시각화합니다.
각 Executable은 자신 앞이나 뒤에 무엇이 오는지 전혀 알지 못합니다. 이러한 느슨한 결합이 체인을 조합 가능하고, 테스트 용이하며, 쉽게 재배열할 수 있게 만드는 핵심 요소입니다.
Putting it Together
Defining a constants class
public class GenericConstants {
public static final String ORDER_ID = "orderId";
public static final String CURRENT_STATE = "currentState";
}
Defining the steps
public class ValidateOrder implements Executable {
@Override
public ChainAction execute(ChainContext context) throws ChainException {
System.out.println("Executing ValidateOrder");
context.setValue(GenericConstants.CURRENT_STATE, "ValidateOrder Done!");
return ChainAction.CONTINUE;
}
}
public class CheckInventory implements Executable {
@Override
public ChainAction execute(ChainContext context) throws ChainException {
System.out.println("Executing CheckInventory");
System.out.println(" Previous State : " + context.getValue(GenericConstants.CURRENT_STATE));
context.setValue(GenericConstants.CURRENT_STATE, "CheckInventory Done!");
return ChainAction.CONTINUE;
}
}
public class ReserveStock implements Executable {
@Override
public ChainAction execute(ChainContext context) throws ChainException {
System.out.println("Executing ReserveStock");
System.out.println(" Previous State : " + context.getValue(GenericConstants.CURRENT_STATE));
context.setValue(GenericConstants.CURRENT_STATE, "ReserveStock Done!");
return ChainAction.CONTINUE;
}
}
public class TakePayment implements Executable {
@Override
public ChainAction execute(ChainContext context) throws ChainException {
System.out.println("Executing TakePayment");
System.out.println(" Previous State : " + context.getValue(GenericConstants.CURRENT_STATE));
context.setValue(GenericConstants.CURRENT_STATE, "TakePayment Done!");
return ChainAction.CONTINUE;
}
}
public class SendNotification implements Executable {
@Override
public ChainAction execute(ChainContext context) throws ChainException {
System.out.println("Executing SendNotification");
System.out.println(" Previous State : " + context.getValue(GenericConstants.CURRENT_STATE));
System.out.println(" Order with Id: " + context.getValue(GenericConstants.ORDER_ID) + " processed!");
context.setValue(GenericConstants.CURRENT_STATE, "SendNotification Done!");
return ChainAction.CONTINUE;
}
}
Running the Chain
public class Main {
public static void main(String[] args) {
Chain chain = Chain.of("order-process-chain")
.next(new ValidateOrder())
.next(new CheckInventory())
.next(new ReserveStock())
.next(new TakePayment())
.next(new SendNotification());
ChainContext context = new ChainContext();
context.setValue(GenericConstants.ORDER_ID, "ORD-123");
chain.context(context);
chain.run();
System.out.println();
chain.printChain();
}
}
/* Output:
Executing Chain : order-process-chain
Executing ValidateOrder
Executing CheckInventory
Previous State : ValidateOrder Done!
Executing ReserveStock
Previous State : CheckInventory Done!
Executing TakePayment
Previous State : ReserveStock Done!
Executing SendNotification
Previous State : TakePayment Done!
Order with Id: ORD-123 processed!
ValidateOrder --> CheckInventory --> ReserveStock --> TakePayment --> SendNotification
*/
Note:
ChainContext는 체인 내에서 Executables 간에 데이터를 전달하는 데 사용됩니다.
What this implementation does NOT handle
SKIP,ROLLBACK,RETRY와 같은 다른 체인 동작(기능을 추가하게 됨).- 체인 및 실행 가능한 객체에 대한 라이프사이클 훅, 예:
onFailure(),before(),after(). - 단계의 병렬 실행.
- 이름을 기준으로 체인을 저장하는 Chain Registry.
마지막 말
단순한 추상화를 만드는 것은 재미있고 교육적입니다. 이는 다듬어진 프레임워크 뒤에 숨겨진 경우가 많은 트레이드‑오프와 마주하게 합니다.
이 실행 가능한 체인을 구축하면 워크플로 엔진부터 AI 파이프라인에 이르기까지 많은 현대 시스템이 동일한 기본 아이디어에 의존한다는 것이 명확해집니다. 추상화를 이해하면 프레임워크가 덜 마법처럼 느껴집니다.
