본문으로 건너뛰기

워크플로 계약 (CommandType W)

워크플로는 transact가 서버에서 여러 거래를 순서대로 실행하고 단계 사이에 값을 전달하는 계약입니다. 화면이 거래를 여러 번 호출해 이어 붙이는 대신 한 번의 요청으로 단계 실행·매핑·검증을 서버가 맡습니다. 서버 모듈 설정과 일반 거래는 transact 모듈을 보세요.

언제 쓰나​

상황더 단순한 방법
한 SQL 계약 안에서 여러 문장을 한 트랜잭션으로 저장dbclient 계약의 여러 seq와 TransactionScope
같은 모듈 안에서 앞 결과를 뒤 입력으로 연결순차 거래(S)
서로 다른 실행 모듈(D·G·F·P·C)과 하위 워크플로를 섞고 중간 결과를 검증워크플로

단계마다 별개의 요청으로 실행되므로 워크플로 전체가 하나의 DB 트랜잭션으로 롤백되지 않습니다. 단계 안에서 데이터를 바꾸는 서비스를 여러 개 호출하면 부분 성공이 남을 수 있으니 보상 단계나 멱등성을 설계하세요.

구문​

거래 계약(.txn/.json)의 Services[] 항목에 CommandType: "W"와 WorkflowSteps를 둡니다. 파일 구조와 접근 제어(Authorize, Roles, AccessScreenID)는 일반 거래 계약과 같습니다.

{
"ServiceID": "GW01", "Authorize": true, "ReturnType": "Json", "CommandType": "W", "TransactionScope": false,
"AccessScreenID": ["ABC010"],
"WorkflowSteps": [
{
"StepID": "findUser", "CommandType": "D",
"TransactionProjectID": "ABC", "TransactionID": "ABC010", "ServiceID": "GD01",
"IncludeResult": false,
"OutputMappings": [ { "SourceFieldID": "UserNo", "TargetFieldID": "UserNo", "Required": true } ],
"Assertions": [
{ "Assert": "NotEmpty", "Collection": { "Source": "Step", "FieldID": "UserNo" }, "Message": "사용자를 찾을 수 없습니다" }
]
},
{
"StepID": "loadOrders", "CommandType": "D",
"TransactionProjectID": "ABC", "TransactionID": "ABC020", "ServiceID": "LD01",
"InputMappings": [
{ "SourceStepID": "findUser", "SourceFieldID": "UserNo", "TargetFieldID": "UserNo", "TargetInputIndex": 0, "Required": true }
]
}
],
"Inputs": [ { "ModelID": "Dynamic", "Fields": [], "TestValues": [], "DefaultValues": [], "Type": "Row", "BaseFieldMappings": [], "ParameterHandling": "Rejected" } ],
"Outputs": [ { "ModelID": "Dynamic", "Fields": [], "Type": "Form" } ]
}

단계 항목 (WorkflowSteps[])​

이름타입기본값의미
StepID문자열{CommandType}{순번 2자리} (예 D01)단계 이름. 워크플로 안에서 유일해야 하고 SourceStepID가 참조합니다
ApplicationID, TransactionProjectID, TransactionID, ServiceID문자열현재 워크플로 계약·서비스 값실행할 서비스. 다른 계약의 서비스도 호출할 수 있습니다
CommandType문자열대상 서비스의 값D·G·F·P·C는 RoutingCommandUri 규칙으로 실행 모듈에 전달됩니다. W는 하위 워크플로(재귀)이며 ApplicationID|ProjectID|TransactionID|ServiceID 경로로 순환 호출을 감지해 실패시킵니다
ReturnType, TransactionScope, ServiceOutputs대상 서비스 설정이 단계에서만 대상 서비스 설정을 덮어씁니다
IncludeResult불리언true이 단계 결과를 최종 응답에 포함할지 정합니다
InputMappings[]배열빈 배열이 단계 입력에 값을 채웁니다
OutputMappings[]배열빈 배열이 단계 결과 필드에 별칭을 추가합니다
Assertions[]배열빈 배열단계 실행 직후 결과를 검증합니다

매핑 항목 (InputMappings[], OutputMappings[])​

이름의미
SourceStepID값을 가져올 이전 단계. 비우면 지금까지 유지된 요청 payload
SourceFieldID가져올 필드(대소문자 무시). 값이 객체(또는 첫 요소가 객체인 배열)면 Field.Property 또는 Property로도 참조합니다
TargetFieldID채울 필드. 비우면 SourceFieldID와 같음
TargetInputIndex대상 서비스 Inputs 중 값을 넣을 위치(기본 0). 입력 매핑에서만 의미가 있습니다
DefaultValue원본이 없을 때 쓸 값. 입력 매핑에서 SourceFieldID가 비어 있으면 이 값이 상수로 들어갑니다
Required원본도 DefaultValue도 없을 때 true면 단계가 실패합니다. false면 입력 매핑은 빈 문자열을 넣습니다

OutputMappings는 결과 필드의 별칭을 추가합니다. 원래 이름도 계속 참조할 수 있고, 이후 단계의 참조와 Assertions에만 영향을 주며 응답 dataSet은 바꾸지 않습니다. 이름을 바꿔 쓰거나 필수 값을 단계 안에서 확인할 때 씁니다.

실행 순서와 결과​

  1. 요청 payload의 입력이 첫 단계의 기본 입력입니다. 단계가 끝나면 그 단계가 사용한 payload가 다음 단계의 기본 입력이 되고, InputMappings는 이 payload 위에 값을 추가하거나 덮어씁니다.
  2. 단계 결과는 ReturnType에 따라 필드 목록이 됩니다: Json은 [{id, value}…]를 필드 목록으로, Scalar는 Scalar, NonQuery는 RowsAffected, Xml은 Xml 필드로 만들고, 배열이 아니면 Result로 둡니다. 단계 결과 전체는 항상 SourceStepID로 참조할 수 있습니다.
  3. 각 단계 뒤에 Assertions를 순서대로 평가하고 첫 실패에서 워크플로를 중단합니다.
  4. 최종 응답 result.dataSet은 IncludeResult가 true인 모든 단계의 비어 있지 않은 결과를 {StepID}_{FieldID} 이름으로 누적한 목록입니다. null, 빈 문자열, 빈 배열·객체는 제외합니다. 앞 단계 결과를 응답에서 빼려면 IncludeResult: false로 둡니다. 요청의 compressionYN이 Y이면 객체·배열 값은 LZString Base64로 압축됩니다.

실패하면 exceptionText에 StepID '…' Workflow 실행 오류 - … 또는 Workflow 검증 오류 - … 형태로 담깁니다.

검증 (Assertions[])​

값 참조 형식​

Expected, Actual, Value, Min, Max, Collection은 모두 같은 형태입니다.

필드의미
SourceLiteral(기본), Request(워크플로가 처음 받은 요청 payload), Step(단계 결과)
ValueLiteral일 때의 값(문자열·숫자·불리언·객체·배열·null)
FieldIDRequest·Step에서 찾을 필드(대소문자 무시, 객체는 Field.Property)
SourceStepIDStep에서 이전 단계 결과를 참조할 때 지정합니다. 생략하면 현재 단계 결과

Source를 생략해도 FieldID가 있고 Value가 없으면 Step으로 해석합니다(명시하는 편이 읽기 쉽습니다). 값을 찾지 못하면 FieldID '…' 검증 값 확인 필요로 실패합니다.

검증 종류 15종​

Assert사용하는 필드통과 조건
Equal / NotEqualExpected, ActualJSON 값 깊은 비교가 같다 / 다르다
True / FalseValue불리언 또는 "true"/"false"로 변환되는 값
Null / NotNullValueJSON null 여부(빈 문자열은 null이 아님)
Contains / DoesNotContainValue, Collection문자열 포함, 배열 요소 일치, 객체 속성명·값 일치(문자열은 대소문자 구분)
Empty / NotEmptyCollection문자열 길이·배열 개수·객체 속성 수가 0 / 0이 아님(null은 빈 값으로 보지 않음)
SingleCollection길이·개수·속성 수가 정확히 1
InRangeValue, Min, Max숫자 또는 날짜가 Min ≤ Value ≤ Max
ThrowsExceptionType(선택)단계가 실패했음을 기대합니다. 실패했고 타입·메시지에 문자열이 포함되면 성공으로 전환되어 다음 단계로 진행
IsTypeValue, TypeNameJSON 타입 String, Integer/Int, Float, Number, Boolean/Bool, Object, Array, Null
SameExpected, Actual값이 아니라 같은 JSON 토큰 인스턴스(일반 비교는 Equal)

모든 검증에 Message를 둘 수 있고, 실패하면 그 문구가 exceptionText가 됩니다. 알 수 없는 Assert 이름은 Assert '…' 확인 필요로 실패합니다.

"Assertions": [
{ "Assert": "NotEmpty", "Collection": { "Source": "Step", "FieldID": "ServerName" }, "Message": "ServerName 값이 비어 있습니다" },
{ "Assert": "Equal", "Expected": { "Source": "Literal", "Value": "OK" }, "Actual": { "Source": "Step", "FieldID": "ResultCode" } },
{ "Assert": "InRange", "Value": { "Source": "Step", "FieldID": "Score" }, "Min": { "Value": 0 }, "Max": { "Value": 100 } },
{ "Assert": "Contains", "Value": { "Source": "Request", "FieldID": "Role" }, "Collection": { "Source": "Step", "SourceStepID": "loadRoles", "FieldID": "Roles" } },
{ "Assert": "Throws", "ExceptionType": "InvalidOperationException", "Message": "입력 매핑 오류가 발생해야 합니다" }
]

자주 하는 실수​

  • NotEmpty·Empty·Single은 Value가 아니라 Collection 필드를 읽습니다.
  • NotEmpty는 null을 빈 값으로 보지 않아 통과합니다. null을 막으려면 NotNull을 함께 쓰세요.
  • Null·NotNull은 Value를 읽습니다. 빈 문자열을 null로 기대하지 마세요.
  • 같은 이름의 필드가 여러 단계에 있으면 SourceStepID를 명시합니다(생략하면 현재 단계).
  • Throws를 일반 단계에 넣으면 정상 실행 시 검증이 실패합니다. 실패가 기대되는 단계에만 쓰세요.

호출​

  • 엔드포인트는 POST /transact/api/workflow/execute이고 요청 전문은 거래 실행과 같습니다. transaction.commandType을 "W"로 보냅니다.
  • 화면에서는 syn.$w.transactionAction('GW01', { endpoint: 'workflow', commandType: 'W' })처럼 호출합니다. endpoint 옵션이 URL의 /transaction/을 /workflow/로 바꿉니다. 응답 개수가 화면 outputs 개수와 같아야 하는 점은 일반 거래와 같습니다.
  • 요청 검증은 일반 거래와 같습니다: AllowRequestTransactions, PublicTransactions, AccessScreenID, 환경(AvailableEnvironment), SecurityHardening(유량 제한·입력 크기), 인증 토큰.

일회성 워크플로​

헤더 X-Workflow-Contract에 TransactionInfo JSON을 실으면 계약 파일 없이 그 요청에서만 워크플로를 실행할 수 있습니다.

  • BearerToken의 Policy.Claims에 DynamicWorkflow=Y와 DynamicWorkflowTransaction=<ProjectID|TransactionID>가 필요합니다.
  • 서버 module.json의 DynamicWorkflowTransaction·DynamicWorkflowServices가 최종 허용 범위입니다.
  • 헤더의 ServiceID는 요청의 functionID와 같아야 합니다.

서버 측 임의 흐름 실행을 여는 기능이므로 운영에서는 허용 범위를 최소화하고 EnforceWorkflowContractGuard를 켜세요.

설계할 때 확인할 것​

  • 단계로 나눌 서비스가 이미 계약으로 존재해야 합니다. 단계마다 입력 필드 이름과 결과 필드 이름을 표로 적고, 이름이 다르면 TargetFieldID로 맞춥니다.
  • 각 단계에 빈 결과, 필수 값, 범위를 Assertions로 두고 운영자가 원인을 알 수 있게 Message를 씁니다.
  • 단계가 늘수록 장애 범위가 넓어집니다. 단계별 timeout(대상 서비스 설정), 폴백, 검증을 계약에 명시합니다.
  • Authorize·Roles·AccessScreenID는 워크플로 서비스 자체에 적용되고, 각 단계의 대상 서비스에는 단계 실행 경로의 설정이 적용됩니다. 민감한 서비스를 워크플로로 우회 노출하지 않는지 확인하세요.
  • AI 파이프라인(의도 분류 → 함수 선택 → 실행)은 P 단계의 PromptResult를 OutputMappings로 다음 단계 입력에 넘기는 패턴이 샘플에 있습니다. 모델 출력은 비결정적이므로 Assertions로 형식을 검증하고 실패 경로를 두세요.

관련 문서: 거래 호출, 식별자와 파일 경로, transact 모듈.