소개
디지털 전환의 시대에 프로세스 자동화는 기업 애플리케이션의 핵심 요소로 자리 잡고 있다. Turbo는 BPMN 2.0 표준을 지원하는 경량 프로세스 엔진으로, 워크플로우 관리, 로우코드 플랫폼 통합, 서비스 오케스트레이션 등 다양한 요구를 유연하게 처리할 수 있다. 본 문서는 실제 운영 사례와 내부 아키텍처 분석을 바탕으로, Turbo를 안정적으로 도입하고 성능을 극대화하는 방법을 제시한다.
아키텍처 구성 요소
Turbo는 모듈화된 설계를 통해 각 기능 영역을 명확히 분리하여 유지보수성과 확장성을 보장한다.
- 모델링 계층: BPMN XML 파싱 및 유효성 검사 수행
- 실행 계층: 인스턴스 생성, 태스크 진행, 상태 전이 관리
- 데이터 계층: 실행 중인 프로세스의 변수 및 컨텍스트 저장
- 확장 계층: 사용자 정의 노드, 스크립트 엔진, 이벤트 후크 제공
엔진의 주요 인터페이스는 다음과 같이 구성된다:
public interface WorkflowEngine {
FlowCreationOutcome defineProcess(ProcessDefinitionInput input);
DeploymentOutcome publishDefinition(DeploymentRequest request);
ProcessStartOutcome launchInstance(StartRequest startRequest);
TaskCompletionOutcome completeStep(TaskSubmission submission);
}
프로세스 정의 시 주의사항
모델 유효성 검사
배포 전 엔진은 자동으로 흐름 구조를 점검하며, 다음 조건을 충족하지 않으면 배포를 차단한다:
- 시작 이벤트는 정확히 하나만 존재해야 함
- 모든 게이트웨이와 태스크는 고유 식별자(key)를 가져야 함
- 어떤 노드도 시작점에서 도달 불가능한 상태일 수 없음
예제 코드:
public class BpmnValidator {
public void performValidation(BpmnModel model) {
assertSingleStartNode(model);
ensureAllNodesReachable(model);
enforceKeyUniqueness(model.getElements());
}
}
식별자 생성 전략
자동 키 생성을 위해 UUID 기반 생성기를 활용하는 것이 좋다:
IdProvider generator = new UuidBasedIdProvider();
String nodeId = generator.generate();
실행 시나리오 최적화
조건식 작성 가이드라인
Turbo는 기본적으로 Groovy 기반 평가기를 사용하므로, 아래와 같은 문법 규칙을 준수해야 한다.
| 사용 목적 |
권장 표현 |
비추천 표현 |
| 문자열 비교 |
state == 'APPROVED' |
state.equals('APPROVED') |
| 숫자 범위 |
value >= 50 && value <= 200 |
value between 50 and 200 |
| 널 체크 |
user != null && user.role == 'ADMIN' |
user?.role == 'ADMIN' |
| 컬렉션 포함 여부 |
roles.contains('EDITOR') |
roles.has('EDITOR') |
실행 데이터 관리
태스크 완료 시 변수를 전달하는 올바른 방식:
List<ExecutionVariable> context = Arrays.asList(
new ExecutionVariable("orderTotal", 1500),
new ExecutionVariable("currency", "KRW")
);
TaskSubmission payload = new TaskSubmission();
payload.setProcessId(instanceId);
payload.setNodeId(currentTaskId);
payload.setVariables(context);
engine.completeStep(payload);
병렬 처리 전략
병렬 게이트웨이 설정
분기 및 병합을 위한 게이트웨이는 다음과 같이 구성한다:
ForkJoinGate gateway = new ForkJoinGate();
gateway.setId("split_merge_point_1");
Map<String, String> correlation = new HashMap<>();
correlation.put("forkId", gateway.getId());
config.setProperty("GATEWAY_CORRELATION", correlation);
병렬 브랜치 상태 추적
모든 분기가 완료되었는지 확인하는 모니터링 로직:
public void checkBranchCompletion(String instanceId, String gateId) {
List<BranchInfo> branches = branchTracker.fetchStatus(instanceId, gateId);
long finished = branches.stream()
.filter(b -> b.isCompleted())
.count();
if (finished == branches.size()) {
resumeAfterMerge(instanceId, gateId);
}
}
결과 병합 전략
다양한 병합 방식을 상황에 맞게 선택할 수 있다:
- 전체 병합: 모든 분기의 데이터를 통합
- 조건 기반 병합: 특정 규칙에 따라 필드 선택
- 커스텀 병합: 비즈니스 로직에 특화된 알고리즘 적용
커스텀 병합 예시:
public class InvoiceDataMerger implements CustomMergePolicy {
@Override
public Map<String, Object> combine(List<Map<String, Object>> inputs) {
Map<String, Object> result = new HashMap<>();
// 비즈니스 규칙에 따른 데이터 통합 로직 구현
return result;
}
}
성능 및 운영 안정성 강화
데이터베이스 인덱스 설계
주요 쿼리 성능 향상을 위한 인덱스 추가:
CREATE INDEX idx_proc_status_time ON process_instance(status, created_at);
CREATE INDEX idx_node_flow_key ON node_execution(process_id, element_key);
캐싱 전략
프로세스 인스턴스 조회 시 캐시 적용:
@Cacheable(cacheNames = "processCache", key = "#id")
public ProcessInstanceDetail fetchInstance(String id) {
return instanceRepository.findById(id);
}
문제 진단 및 디버깅
실행 로그 기록
핵심 이벤트에 대해 상세한 로그 출력:
logger.info("[PROC-{}] Node '{}' executed with data: {}",
instanceId, nodeName, objectMapper.writeValueAsString(data));
내장 진단 기능 활용
다음 API를 통해 실시간 상태 확인 가능:
getExecutionPath(String instanceId): 실행 경로 추적
getCurrentActiveNodes(String instanceId): 현재 활성 태스크 확인
fetchRuntimeVariables(String instanceId): 변수 값 조회
운영 성공을 위한 핵심 원칙
- 관심사 분리: 비즈니스 로직은 서비스 태스크로 위임
- 일관된 키 관리: 자동 생성 또는 중앙 집중식 ID 할당 사용
- 표준 스크립트 사용: Groovy 문법을 철저히 준수
- 모니터링 체계 구축: 실패율, 지연 시간, 대기 태스크 등을 실시간 감시