새 작업 종류(사용자 정의 액티비티), 담당자를 정하는 규칙(역할 해석), 모든 작업의 실행 전후 훅(액티비티 필터), 배포 훅, 사용자 정보(IAM), 이벤트 발행을 Java로 바꿔 끼우는 방법입니다. 클래스·인터페이스 이름은 이 브랜치 코드에서 확인했습니다.
무엇을 · 왜#
모델러 패널에 없는 동작이 필요할 때가 있습니다. 사내 신용 조회 시스템을 호출하는 작업, 「요청자의 팀장」처럼 조직도를 따라 담당자를 찾는 규칙, 모든 작업의 완료를 사내 감사 시스템에 기록하는 일 등이 있습니다. uEngine6은 이런 동작을 process-service의 Java 클래스로 구현할 수 있습니다. 정의 파일은 그 클래스를 이름으로 가리키기만 합니다.
| 바꾸고 싶은 것 | 확장 지점 | 정의·설정에서 가리키는 법 | 등록 |
|---|---|---|---|
| 작업이 하는 일 | org.uengine.kernel.Activity 하위 클래스 |
작업 JSON의 _type |
process-service 클래스패스에 두기만 하면 됨 |
| 담당자를 정하는 규칙 | org.uengine.kernel.RoleResolutionContext 하위 클래스 |
레인 JSON의 roleResolutionContext._type |
위와 같음 |
| 모든 작업의 실행 전후 | org.uengine.kernel.ActivityFilter |
— | Spring 빈 |
| 작업 완료(커밋 뒤) 알림 | org.uengine.kernel.IActivityCompletionListener |
— | Spring 빈 |
| 정의 배포 때 | org.uengine.kernel.DeployFilter |
— | Spring 빈 |
| 사용자·그룹·역할 조회 | org.uengine.five.service.IAMService |
환경 변수 IAM_PROVIDER |
IAMServiceFactory.register(...) |
| 이벤트 내보내기 | org.uengine.five.messaging.EventPublisher |
uengine.messaging.mode |
Spring 빈(Kafka/폴링 구현이 이미 있음) |
_type의 뜻. 엔진(org.uengine.five.serializers.BpmnXMLParser)은 BPMN 태그 이름으로 기본 클래스를 정합니다. userTask는 org.uengine.kernel.HumanActivity, scriptTask는 org.uengine.kernel.ScriptActivity, task·manualTask는 org.uengine.kernel.DefaultActivity, 나머지는 org.uengine.kernel.bpmn.<태그 이름>(예: ServiceTask)입니다. 그다음 요소의 <uengine:properties> JSON을 Jackson으로 읽습니다. JSON에 _type이 있으면 그 클래스 이름으로 객체를 만듭니다. 그래서 서비스 작업은 _type에 따라 REST 호출(ServiceTask)도, SQL(SQLTask)도, RPA(org.uengine.five.rpa.RPAActivity)도 됩니다.
// BpmnXMLParser.createTypedJsonObjectMapper() — 이 브랜치 코드
objectMapper.setVisibilityChecker(... .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
.withSetterVisibility(JsonAutoDetect.Visibility.NONE) ...);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.enableDefaultTypingAsProperty(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE, "_type");
이 코드에서 알 수 있는 점은 세 가지입니다.
- JSON은 필드에 바로 들어갑니다(setter를 거치지 않음). 클래스에는 인자 없는 public 생성자가 있어야 하고, 필드 이름이 곧 JSON 속성 이름입니다.
- 알 수 없는 속성은 무시합니다. 철자가 틀려도 오류 없이 빈 값이 됩니다.
- JSON 어딘가(헤더·역할 등)에
_type이 있으면 맨 위에도_type이 있어야 합니다. 파서가 이때 맨 위를 추상Activity로 읽기 때문입니다(REST API 부르기의 「패널 저장 후_type확인」과 같은 이유).
이 장은 글만 있습니다
확장은 process-service를 다시 빌드하고 실행해야 하므로, 여러 사람이 함께 쓰는 시연 스택에서는 실행해 보지 않았습니다. 클래스·인터페이스·메서드 이름과 등록 방식은 이 브랜치(bmt/sds-process-test) 코드에서 확인했습니다. 아래 예제 코드(com.gaon…)는 컴파일·실행해 보지 않았습니다. 이미 있는 구현(RPAActivity, DirectRoleResolutionContext, InstanceNameFilter 등)을 본보기로 쓰세요.
따라 하기#
- 사용자 정의 액티비티 — 동기 작업. 바로 끝나는 작업은
org.uengine.kernel.DefaultActivity를 상속해executeActivity를 구현하고, 끝에fireComplete(instance)를 호출합니다. 변수는instance.get("", "<이름>")으로 읽고, 결과는ProcessVariable필드의set(instance, "", 값)으로 씁니다(ScriptActivity와 같은 방식).실패할 때는 예외를 던집니다. 이 브랜치에서는 서비스 작업과 같이 앞선 사람 작업의 완료 요청 전체가 거절되고, 오류 경계를 붙였으면 그쪽으로 진행합니다(되돌리기와 실패한 작업 다루기, 실패 경로와 되돌리기).package com.gaon.bpm; import java.io.Serializable; import org.uengine.kernel.DefaultActivity; import org.uengine.kernel.ProcessInstance; import org.uengine.kernel.ProcessVariable; /** 공급사 신용 등급 조회(예제). 정의에서는 "_type":"com.gaon.bpm.CreditCheckActivity" */ public class CreditCheckActivity extends DefaultActivity { String supplierVariable; // JSON "supplierVariable": "공급사" ProcessVariable out; // JSON "out": {"name": "신용등급"} public CreditCheckActivity() { setName("신용 조회"); } @Override protected void executeActivity(ProcessInstance instance) throws Exception { Serializable supplier = instance.get("", supplierVariable); String grade = lookup(String.valueOf(supplier)); // 사내 시스템 호출 if (out != null) out.set(instance, "", grade); fireComplete(instance); // 부르지 않으면 작업이 끝나지 않는다 } String lookup(String supplier) { return "A"; } } - 정의에서 가리키기. 모델러에는 이 클래스의 패널이 없으므로 「XML 보기」(
</>)에서 서비스 작업의 JSON을 바꿉니다. 태그는serviceTask든task든 상관없고,_type이 클래스를 정합니다.클래스가 process-service 클래스패스에 없으면 배포(<bpmn:serviceTask id="Credit" name="신용 조회"> <bpmn:extensionElements><uengine:properties><uengine:json> {"_type":"com.gaon.bpm.CreditCheckActivity","supplierVariable":"공급사","out":{"name":"신용등급"}} </uengine:json></uengine:properties></bpmn:extensionElements> </bpmn:serviceTask>POST /definition-changes)가 실패합니다. definition-service는 BPMN 원문만 저장하므로 해당 서비스에는 클래스가 없어도 됩니다. - 사용자 정의 액티비티 — 오래 걸리는 작업. 외부 작업이 끝날 때까지 기다려야 하면
org.uengine.kernel.ReceiveActivity를 상속하고,executeActivity에서는 작업을 맡기기만 하고fireComplete를 호출하지 않습니다. 결과가 오면fireComplete(instance)(실패하면fireFault(instance, e))를 부릅니다. 이 브랜치의org.uengine.five.rpa.RPAActivity가 이 방식입니다.executeActivity에서 RPA 작업을 큐에 넣고, 워커가 결과를 REST로 보고하면onJobResult(ProcessInstance, Map)이 결과를 변수에 매핑한 뒤fireComplete를 호출합니다. 사람이 화면에서 끝내는 작업을 만들려면org.uengine.kernel.HumanActivity를 상속하고getTool(ProcessInstance)를 바꿉니다.FormActivity는"formHandler:<폼 ID>",URLActivity는"urlHandler:<URL>", 기본은"defaultHandler"를 반환합니다. 이 문자열은 업무 목록 행의tool칸에 저장될 뿐이고, 어떤 화면을 표시할지는 프런트가 정합니다(WorkItem.vue가urlHandler·formHandler·그 밖으로 나눔). 서버에 폼 핸들러를 추가하는 인터페이스는 없습니다(org.uengine.kernel.ITool은 있지만 쓰는 곳이 없음). 새 작업 화면은 URL 작업으로 다른 앱 화면을 여는 편이 쉽습니다. - 담당자 규칙 — 역할 해석. 레인의 담당자는
org.uengine.kernel.RoleResolutionContext가 정합니다. 추상 메서드는 둘입니다.사람 한 명을 정하는 규칙이면abstract public RoleMapping getActualMapping(ProcessDefinition pd, ProcessInstance instance, String tracingTag, Map options) throws Exception; abstract public String getDisplayName();DirectRoleResolutionContext처럼RoleMapping에 사용자 이름을 넣어 반환합니다.package com.gaon.bpm; import java.util.Map; import org.uengine.kernel.*; /** 금액이 기준 이상이면 above, 아니면 below 가 결재(예제). */ public class AmountBasedApprover extends RoleResolutionContext { String amountVariable = "금액"; long threshold = 10_000_000L; String below = "yoon"; String above = "kim"; @Override public RoleMapping getActualMapping(ProcessDefinition pd, ProcessInstance instance, String tracingTag, Map options) throws Exception { Object v = instance.get("", amountVariable); long amount = v instanceof Number ? ((Number) v).longValue() : 0L; RoleMapping rm = RoleMapping.create(); rm.setEndpoint(amount >= threshold ? above : below); // 엔드포인트 = Keycloak 사용자 이름 rm.setAssignType(Role.ASSIGNTYPE_USER); return rm; } @Override public String getDisplayName() { return "금액 기준 결재자"; } }엔드포인트는 이메일이 아니라 토큰의{"roleResolutionContext": {"_type": "com.gaon.bpm.AmountBasedApprover", "threshold": 5000000}}preferred_username(예:yoon)이어야 그 사람의 업무 목록에 표시됩니다(사용자·그룹·역할). 그룹·역할로 배정하는 기본 구현org.uengine.five.overriding.IAMRoleResolutionContext(필드groupName·scope)는 사용자를 나열하지 않습니다.RoleMapping에 그룹·역할과 배정 종류(ASSIGNTYPE_GROUP=3,ASSIGNTYPE_ROLE=4,ASSIGNTYPE_GROUP_ROLE=5)만 넣습니다. 작업을 볼 수 있는 사람은 업무 목록 조회(findToDo)가 토큰의groups·realm_access.roles와 비교해 정합니다. 「이 사람이 담당자인가」 검사가 필요하면org.uengine.kernel.IContainsMapping(boolean containsMapping(ProcessInstance, RoleMapping))을 함께 구현합니다. 고객사 패키지org.uengine.hwlife.overriding의BoundRoleResolutionContext(다른 규칙 + 변수 바인딩),RuleBasedRoleResolutionContext(정책 규칙)도 참고할 수 있습니다. - 모든 작업에 훅 걸기 — 액티비티 필터.
org.uengine.kernel.ActivityFilter를 구현한 Spring 빈은 모든 정의의 모든 작업에 적용됩니다. 엔진(ProcessDefinition.getActivityFilters())이GlobalContext.getComponents(ActivityFilter.class), 즉 Spring의getBeansOfType으로 필터를 모읍니다.호출 순서는public interface ActivityFilter extends java.io.Serializable { void beforeExecute(Activity activity, ProcessInstance instance) throws Exception; void afterExecute(Activity activity, ProcessInstance instance) throws Exception; void afterComplete(Activity activity, ProcessInstance instance) throws Exception; void afterFault(Activity activity, ProcessInstance instance, FaultContext faultContext) throws Exception; void onPropertyChange(Activity activity, ProcessInstance instance, String propertyName, Object changedValue) throws Exception; void onDeploy(ProcessDefinition definition) throws Exception; }beforeExecute → executeActivity → afterExecute → fireComplete → (onEvent) → afterComplete입니다(Activity.java주석과 코드).org.uengine.kernel.SensitiveActivityFilter는onEvent(Activity, ProcessInstance, String eventName, Object payload)를 더합니다. 등록은@Bean이 확실합니다.이 브랜치에서 실제로 적용된 필터는@Configuration public class GaonFilters { @Bean public ActivityFilter gaonAuditFilter() { return new ActivityFilter() { public void afterComplete(Activity a, ProcessInstance i) throws Exception { if (a instanceof HumanActivity) log("완료", i.getInstanceId(), a.getTracingTag()); } public void beforeExecute(Activity a, ProcessInstance i) {} public void afterExecute(Activity a, ProcessInstance i) {} public void afterFault(Activity a, ProcessInstance i, FaultContext f) {} public void onPropertyChange(Activity a, ProcessInstance i, String p, Object v) {} public void onDeploy(ProcessDefinition d) {} }; } static void log(Object... x) { /* 사내 감사 시스템으로 */ } }InstanceNameFilter(인스턴스 이름 패턴 적용)와InstanceDataAppendingActivityFilter(서비스·스크립트 작업을 업무 목록 행으로 남기고, 실패하면 Failed 행)입니다. 둘 다@Bean으로 등록돼 있습니다.PayloadFilter는 설정filter.payload.enabled: false로 꺼져 있고,AuditActivityFilter는 빈 등록이 주석 처리돼 있습니다. - 완료 알림 — 커밋 뒤. 필터는 엔진 트랜잭션 안에서 실행됩니다. 외부로 메시지를 보낼 때는 커밋이 끝난 뒤 호출되는
org.uengine.kernel.IActivityCompletionListener(void onActivityCompleted(ProcessInstance instance, Activity activity))가 안전합니다. 이 브랜치의ActivityCompletionBrodcastListener(@Component)가 이 방식으로 사람 작업이 끝날 때마다EventPublisher.send("bpm-brodcast", …, Map.of("type", "TASK_COMPLETED"))를 보냅니다(이벤트로 시스템 잇기). - 배포 훅.
org.uengine.kernel.DeployFilter(beforeDeploy(ProcessDefinition, ProcessTransactionContext, String folder, boolean isNew))를 구현한 Spring 빈은 정의가 배포될 때(POST /definition-changes, 시작 시 정의 동기화) 호출됩니다.EventMappingDeployFilter가 이때 시작 이벤트·이벤트 동기화 작업을BPM_EVENT_MAPPING표에 등록합니다. 정의 검사(「서비스 작업에 URL 이 없으면 거절」 같은)를 추가할 수 있는 지점입니다. - 사용자 정보(IAM). 그룹·역할 확인과 사용자 이름 조회는
org.uengine.five.service.IAMService를 거칩니다. 메서드는getUserById,getUsersByGroup,getUsersByRole,getUserScopes,getUserGroups, 기본 구현hasScope·isInGroup입니다. 구현은KeycloakIAMService(Keycloak 관리 REST)와 고객사용ExternalIAMService가 있고,ProcessServiceApplication이IAMServiceFactory.register("keycloak", …)·register("external", …)로 등록합니다. 어떤 구현을 쓸지는 환경 변수IAM_PROVIDER(또는 속성iam.provider)로 고릅니다. LDAP·사내 인사 시스템을 붙이려면 이 인터페이스를 구현해 같은 방식으로 등록합니다. - 이벤트. 들어오는 이벤트는
BpmMessageDispatcher.dispatch가 헤더type·corrKey·emnb를 읽어AsyncEventListener.wheneverEvent(eventBody, typeHeader, inboxCorrKey, actorEndpoint)로 넘깁니다.BPM_EVENT_MAPPING에서 이벤트 이름으로 정의·작업을 찾아 시작하거나 작업을 끝냅니다. 여기에 추가하는 사용자 정의 핸들러 인터페이스는 없습니다. 다른 처리가 필요하면 그 이벤트를 받는 작업을ReceiveActivity하위 클래스로 만들거나(3단계), 폴링 모드의 진입점POST /inbox를org.uengine.five.messaging.EventInboxProvider(getProviderId(),receiveEvent(String requestBodyJson)) 구현으로 바꿉니다(설정event-inbox.provider). 나가는 이벤트는EventPublisher.send(String channel, Object payload, Map<String,Object> headers)하나로 보냅니다. 구현은 Kafka 모드KafkaEventPublisher, 폴링 모드InboxEventPublisher입니다.
설정 레퍼런스#
확장 인터페이스 요약#
| 형식 | 이름(패키지) | 꼭 구현할 것 | 이 브랜치의 예 |
|---|---|---|---|
| 클래스 | org.uengine.kernel.Activity / DefaultActivity |
protected void executeActivity(ProcessInstance) → fireComplete(instance) |
ScriptActivity, bpmn.ServiceTask, bpmn.SQLTask |
| 클래스 | org.uengine.kernel.ReceiveActivity |
executeActivity에서 맡기기만, 나중에 fireComplete/fireFault |
org.uengine.five.rpa.RPAActivity |
| 클래스 | org.uengine.kernel.HumanActivity |
public String getTool(ProcessInstance) |
FormActivity, URLActivity |
| 클래스 | org.uengine.kernel.RoleResolutionContext |
getActualMapping(...), getDisplayName() |
DirectRoleResolutionContext, five.overriding.IAMRoleResolutionContext |
| 인터페이스 | org.uengine.kernel.IContainsMapping |
containsMapping(ProcessInstance, RoleMapping) |
IAMRoleResolutionContext |
| 인터페이스 | org.uengine.kernel.ActivityFilter / SensitiveActivityFilter |
6개(+onEvent) |
five.overriding.InstanceNameFilter, InstanceDataAppendingActivityFilter |
| 인터페이스 | org.uengine.kernel.IActivityCompletionListener |
onActivityCompleted(ProcessInstance, Activity) |
five.overriding.ActivityCompletionBrodcastListener |
| 인터페이스 | org.uengine.kernel.DeployFilter |
beforeDeploy(...) |
EventMappingDeployFilter, ServiceRegisterDeployFilter |
| 인터페이스 | org.uengine.five.service.IAMService |
조회 메서드 5개 | KeycloakIAMService |
| 인터페이스 | org.uengine.five.messaging.EventPublisher |
send(channel, payload, headers) |
KafkaEventPublisher, InboxEventPublisher |
| 인터페이스 | org.uengine.five.messaging.EventInboxProvider |
getProviderId(), receiveEvent(String) |
DefaultEventInboxServiceImpl |
어디에 두고 어떻게 띄우나#
- 모듈: 루트
pom.xml의uengine-commons,uengine-resource-manager,uengine-core,uengine-five-api,definition-service,process-service,analytics. 엔진 확장은 process-service 실행 jar 안에 있어야 합니다. 가장 쉬운 방법은RPAActivity처럼 process-service 모듈에 두는 것이고, 따로 jar로 만들면process-service/pom.xml의 의존성으로 넣습니다(이 저장소에는loader.path같은 외부 jar 로딩 설정이 없습니다). - Spring 빈 스캔:
ProcessServiceApplication의@ComponentScan(basePackages = {"org.uengine.kernel.bpmn", "org.uengine.five", "org.uengine.hwlife"}). 다른 패키지(com.gaon…)에@Component를 붙이면 스캔되지 않습니다.@Bean으로 등록하거나 스캔 대상에 넣으세요.spring.main.allow-bean-definition-overriding: true라서 같은 이름의 빈으로 기본 빈을 교체할 수도 있습니다. _type으로만 쓰는 클래스(액티비티·역할 해석)는 빈 등록이 필요 없습니다. Jackson이 클래스 이름으로 찾습니다.- 빌드·실행(저장소 문서
docs/local-install-guide.md5-1):
mvn -B -Dmaven.test.skip=true install # 저장소 루트
SPRING_PROFILES_ACTIVE=postgres,keycloak-installed UENGINE_BASEPATH="$(pwd)" \
KEYCLOAK_URI=http://localhost:8280 KEYCLOAK_REALM=uengine ... \
java --add-opens java.base/java.util=ALL-UNNAMED ... \
-jar process-service/target/process-service-1.1-SNAPSHOT.jar \
--spring.config.additional-location=file:docs/local/process-service.local.yml
정의 JSON에서 쓸 수 있는 이름#
| 쓰는 곳 | 이름 | 뜻 |
|---|---|---|
| 스크립트 작업(JavaScript·Java) | instance, activity, definition, util |
실행 중 인스턴스, 이 작업, 정의, 도우미(스크립트로 값 계산) |
| 템플릿(서비스 작업 URL·본문, 인스턴스 이름) | <%=변수%>, <%=instance.instanceId%>, <%=instance.corrkey%> |
변수·인스턴스 속성 치환 |
| 프로세스 변수 형식 | Text, Number, Date, Form |
파서(convertToJavaType)가 받는 형식. 그 밖은 오류 |
알려진 제약#
- 패널이 없습니다. 사용자 정의
_type은 모델러의 「XML 보기」에서 직접 씁니다. 그 요소의 패널을 열어 저장하면 패널이 아는 속성만 남을 수 있으니, 저장한 뒤 XML을 다시 확인하세요. _type은 클래스패스의 어떤 클래스든 가리킬 수 있습니다. 파서가 Jackson 기본 타입 지정을 쓰고, 허용 패키지 목록(PolymorphicTypeValidator)이 코드에 없습니다. 정의를 배포할 수 있는 사람은 서버 코드를 고를 수 있는 사람이라고 보고, 배포 권한을 제한하세요.- 폼·업무 화면 종류는 서버에서 늘릴 수 없습니다(프런트
WorkItem.vue가 정함). - 들어오는 이벤트에 추가하는 핸들러 인터페이스는 없습니다(위 8단계의 두 우회 방법).
- 확인하지 못함:
ActivityFilter.onDeploy를 부르는 곳을 이 브랜치 코드에서 찾지 못했습니다. 배포 훅은DeployFilter를 쓰세요. 프런트 레인 패널이 가리키는org.uengine.five.overriding.GroupRoleResolutionContext,org.uengine.kernel.ExternalCustomerRoleResolutionContext는 백엔드 소스에 없습니다(그 선택지로 배포해 보지는 않았습니다). - 이 장의 예제 코드는 빌드·실행하지 않았습니다.
확인 문제#
사용자 정의 액티비티를 만들었는데 정의를 배포하면 실패합니다. 무엇부터 확인하나요?
① 그 클래스가 process-service 실행 jar 안에 있는지(다른 모듈이면 process-service/pom.xml 의존성), ② JSON의 _type이 패키지까지 맞는지, ③ 인자 없는 public 생성자가 있는지 확인합니다. 정의 저장(definition-service)은 원문만 저장하므로 성공해도, process-service가 읽을 때 실패할 수 있습니다.
모든 사람 작업 완료를 사내 감사 시스템에 보내려면 ActivityFilter와 IActivityCompletionListener 중 무엇을 쓰나요?
IActivityCompletionListener입니다. 커밋이 끝난 뒤 호출되므로, 엔진 트랜잭션이 되돌려진 작업의 완료 알림을 외부로 보내는 일이 없습니다. 필터는 트랜잭션 안에서 실행되므로 같은 DB 내 기록이나 검사에 적합합니다.
「요청자의 팀장」에게 결재를 보내는 레인을 만들려면 어디를 고치나요?
RoleResolutionContext 하위 클래스를 만들어 getActualMapping에서 요청자(시작한 사람)의 팀장을 사내 조직도나 IAMService로 찾아 RoleMapping의 엔드포인트(사용자 이름)로 돌려주고, 레인 JSON의 roleResolutionContext._type에 그 클래스 이름을 적습니다.