본문으로 건너뛰기

function

function은 XML 함수 계약을 기준으로 C#, Node.js, Python 함수를 실행하는 모듈입니다. 업무 로직이 단순 SQL로 끝나지 않거나 외부 라이브러리, 파일, 계산 로직이 필요한 경우 transact에서 CommandType=F로 라우팅해 사용합니다.

책임 범위​

  • 함수 계약을 로드하고 ApplicationID|ProjectID|TransactionID|FunctionID 기준으로 실행합니다.
  • C#, Node.js, Python 런타임별 실행 환경과 파일 감시 정책을 관리합니다.
  • FunctionSource의 데이터 원본과 작업 디렉터리를 함수 실행 컨텍스트에 제공합니다.
  • BeforeTransaction, AfterTransaction, FallbackTransaction 같은 계약 흐름을 처리합니다.
  • 실행 로그를 런타임별 파일 경로와 logger 모듈로 분리해 남깁니다.

주요 API​

메서드경로용도
GET/function/api/execution/has함수 계약 존재 여부를 확인합니다.
GET/function/api/execution/refresh함수 계약 캐시를 갱신합니다.
GET/function/api/execution/retrieve함수 계약 원문 또는 상세 정보를 조회합니다.
GET/function/api/execution/meta입력/출력 메타 정보를 반환합니다.
POST/function/api/execution함수 계약을 실행합니다.

has, refresh, retrieve, meta의 파라미터와 인증 규칙(실패 시 400)은 transact 모듈의 계약 관리 API 공통 규칙을 보세요.

핵심 구현​

  • Areas/function/Controllers/ExecutionController.cs: execution API 진입점입니다.
  • DataClient/FunctionClient.cs: 런타임 선택, 계약 실행, 결과 변환을 담당합니다.
  • Builder: C# 함수 동적 컴파일과 실행을 담당합니다.
  • Entity/ModuleConfigJson.cs: Node/C#/Python 런타임 설정과 FunctionSource 스키마입니다.
  • Contracts/function: 샘플 함수 계약과 featureMain.*, featureMeta.json 구조를 제공합니다.

계약과 식별자​

계약은 기본적으로 다음 위치에 둡니다.

contracts/function/{ApplicationID}/{ProjectID}/{TransactionID}.xml

함수 ID는 다음 형식입니다.

ApplicationID|ProjectID|TransactionID|FunctionID

계약에는 EntryType, EntryMethod, FunctionSource, 입력/출력 모델, 전후 처리 거래가 포함됩니다. 실제 함수 코드는 계약이 가리키는 디렉터리의 featureMain.cs, featureMain.js, featureMain.py와 보조 메타 파일로 관리합니다.

단일 파일 계약 (.fnc)​

위의 폴더형(featureMain.*, featureMeta.json) 대신 헤더, 스크립트, featureSQL, command를 한 파일에 담는 형식입니다. IDE가 만드는 함수가 이 형식이며 서버는 .xml과 .fnc 모두 읽습니다(계약 확장자). 위치는 contracts/function/{ApplicationID}/{ProjectID}/{TransactionID}.fnc입니다.

<?xml version="1.0" encoding="UTF-8"?>
<mapper xmlns="contract.xsd">
<header>
<application>HDS</application>
<project>ABC</project>
<transaction>ABC020</transaction>
<datasource>DB01</datasource>
<language>csharp</language> <!-- csharp | javascript | python -->
<referenceModuleID></referenceModuleID> <!-- 업무 모듈의 Entity·설정을 참조할 때 모듈 ID -->
<isHttpContext>false</isHttpContext>
<use>Y</use>
<desc>설명</desc>
</header>
<script><![CDATA[
/* 함수 본문 */
]]></script>
<featureSQL> <!-- 선택: 함수에서 쓸 SQL --> </featureSQL>
<commands>
<command id="GF01" seq="0" use="Y" timeout="0" desc="설명">
<entryType></entryType>
<entryMethod></entryMethod>
<beforeTransaction></beforeTransaction>
<afterTransaction></afterTransaction>
<fallbackTransaction></fallbackTransaction>
<param id="ServerDate" type="String" length="0" value="" required="false" />
</command>
</commands>
</mapper>
위치항목의미
headerapplication, project, transaction함수 ID ApplicationID|ProjectID|TransactionID|FunctionID의 앞 세 부분
datasource함수에서 쓸 데이터 원본 ID (FunctionSource의 DataSourceID)
languagecsharp, javascript, python
referenceModuleID업무 모듈의 엔티티를 쓰는 함수가 지정하는 모듈 ID
isHttpContext기본 false
use, desc사용 여부(기본 Y), 설명
commandidServiceID(CommandType: "F")와 같은 이름. C#은 메서드 이름, Node.js는 module.exports의 키
seq, use, timeout, desc순번(기본 0), 사용 여부(기본 Y), 제한 시간, 설명
entryType, entryMethod진입 클래스와 메서드를 직접 지정할 때
beforeTransaction, afterTransaction, fallbackTransaction전후 처리·폴백 거래. 대부분 비워 둡니다
paramid, type, length, value, required입력 파라미터. SQL 계약과 달리 @를 붙이지 않는 것이 관례이고 type 기본값은 String입니다. required는 /meta 응답에만 쓰이고 실행 때 강제 검증하지 않으므로 필수 값은 함수 안에서 검사하세요
outputmetavalue출력 메타 정보

한 파일에 command를 여러 개(GF01, LF01, MF01 …) 둘 수 있고 모두 같은 <script>를 공유합니다. 서버는 JavaScript와 Python의 <script>를 같은 폴더의 {TransactionID}.g.* 파일로, <featureSQL>을 {TransactionID}.g.featureSQL로 만들어 실행합니다. 이 .g.* 파일은 서버가 다시 만드는 산출물이므로 직접 고치지 말고 원본 계약을 수정하세요.

진입점 규칙​

  • C#: command id가 메서드 이름이고, entryType과 entryMethod를 비워 두면 클래스는 namespace {ApplicationID}.Function.{header의 project} 안의 class {TransactionID}입니다. 폴더 이름이 아니라 header의 <project>·<transaction> 값 기준입니다. 다른 클래스나 메서드를 쓰려면 <entryType>과 <entryMethod>를 둘 다 채우세요. entryType만 채우면 메서드 이름이 비어 EntryType, EntryMethod 확인 필요가 납니다.
  • Node.js: module.exports = { GF01: (callback, moduleID, parameters, dataContext) => { … } }이고 command id가 키입니다. 서버는 네 값(callback, moduleID, 입력 파라미터 목록, dataContext)을 넘기므로, 오래된 함수의 (callback, moduleID, dynamicParameters, isTransaction, functionSeq) 시그니처에서 네 번째 인자 isTransaction은 실제로는 dataContext입니다.
  • Python: class {TransactionID}의 def GF01(parameters, dataContext)가 진입점이며 parameters는 딕셔너리, dataContext는 JSON 문자열입니다. 서버가 .g.py를 모듈 파일로 복사해 임포트합니다.
  • header: datasource에 쉼표로 여러 데이터 원본을 둘 수 있고 dataContext.connectionString에 |로 이어 전달됩니다. isHttpContext가 true이면 클래스를 HttpContext를 받는 생성자로 만듭니다(생성자가 없으면 호출이 실패). use가 N이면 비활성입니다.
  • 동적 컴파일: C# 함수는 서버가 동적으로 컴파일하므로, 모듈 csproj가 Contracts/function/**를 Compile에서 제외하는 경우가 많고 모듈 빌드 성공이 함수 컴파일 성공을 뜻하지 않습니다. 컴파일된 어셈블리는 파일 경로 단위로 캐시되므로 같은 프로세스에서는 계약을 갱신(refresh, 파일 감시, 재시작)하기 전까지 이전 결과가 쓰일 수 있습니다.

C# 함수​

using System;
using System.Collections.Generic;
using System.Data;
using HandStack.Core.ExtensionMethod;
using HandStack.Web.Extensions;
using HandStack.Web.MessageContract.DataObject;

namespace HDS.Function.ABC // {ApplicationID}.Function.{header project}
{
public class ABC020 // {TransactionID}
{
public DataSet? GF01(List<DynamicParameter> dynamicParameters, DataContext dataContext)
{
string typeMember = "ABC.ABC020.GF01";
DataSet result = new DataSet();
result.BuildExceptionData(); // 첫 테이블: Exception(정상 상태)

string key = dynamicParameters.Value("Key").ToStringSafe();
if (string.IsNullOrEmpty(key) == true)
{
result.BuildExceptionData("Y", "Warning", "필수 요청 정보 확인 필요", typeMember);
return result;
}

DataTable table = new DataTable();
table.Columns.Add("Result", typeof(string));
table.Rows.Add("값");
result.Tables.Add(table);
return result;
}
}
}
  • 메서드는 DataSet?(또는 async Task<DataSet?>)를 반환하고 (List<DynamicParameter> dynamicParameters, DataContext dataContext)를 받습니다. 업무 모듈 엔티티를 쓰면 referenceModuleID를 지정하고 해당 모듈 네임스페이스를 using합니다.
  • dataContext에서 functionHeader.Configuration(모듈 설정), globalID, environment, featureSQLPath, logger를 쓸 수 있습니다. 키 값은 로그에 남기지 마세요.
  • 클래스 이름이나 네임스페이스가 header의 project·transaction과 다르면 EntryType, EntryMethod 확인 필요 오류가 납니다. 어긋난 기존 함수는 entryType을 명시했을 수 있으니 그대로 복사하지 마세요.

Node.js 함수​

var syn = require('syn');
var axios = require('axios');

module.exports = {
GF01: (callback, moduleID, parameters, dataContext) => {
(async () => {
var result = { DataTable1: [{ ErrorMessage: null }] };
try {
var key = $array.getValue(parameters, 'Key');
// 외부 호출, syn.$s.executeQuery(moduleID, 'L01', sqlParameters, cb) …
result.DataTable1[0].Value = key;
return callback(null, result);
}
catch (error) {
syn.$l.moduleEventLog(moduleID, 'GF01', 'Message: {0}'.format(error.message), 'Error');
result.DataTable1[0].ErrorMessage = error.message;
return callback(null, result);
}
})();
}
}
  • 반환은 { TableName: [행…], … } 객체이고 서버가 DataSet으로 역직렬화합니다. 키 하나가 결과 테이블 하나이고 선언 순서가 테이블 순서이므로 화면 outputs 순서와 맞추세요.
  • 업무 오류는 callback(error)로 던지기보다 Exception 테이블로 돌려주는 쪽이 화면 기대에 맞는 경우가 많습니다. 호출 화면을 확인하세요.
  • callback은 한 번만 호출합니다. 성공·실패·예외 경로에서 return callback(...) 패턴으로 중복을 막으세요.

Python 함수​

import json

class ABC020:
def GF01(parameters, dataContext):
data = json.loads(dataContext)
result = {"DataTable1": [{"globalID": data["globalID"]}]}
return json.dumps(result, ensure_ascii=False)

반환은 JSON 문자열(테이블 이름 → 행 배열)입니다. 서버 설정에서 Python DLL 사용이 켜져 있어야 하며(Python 런타임 메모), IDE 언어 선택지에 Python이 있다고 서버에 런타임이 준비된 것은 아닙니다.

Exception 출력​

Exception 테이블을 첫 결과로 내보내는 서비스에서는 다음을 지킵니다.

  1. 호출 화면 outputs의 첫 항목이 Form: Exception인지, 거래 계약의 Outputs가 있는지 확인합니다.
  2. C#은 result.BuildExceptionData()로 정상 상태를 먼저 만들고, 실패하면 BuildExceptionData("Y", level, message, typeMember)로 덮어씁니다. 컬럼은 Error("Y"/"N" 문자열), Level, Message, StackTrace, TypeMember입니다.
  3. 데이터 테이블이 아직 없는 실패 경로에서도 화면이 기대하는 개수가 되도록 빈 DataTable을 추가합니다.
  4. SQL 서비스의 Exception(Error가 오류 번호 숫자)과 함수의 Exception("Y"/"N")은 값 형태가 다릅니다. 화면은 실제 반환 형을 확인하고 처리하세요.
  5. 메시지에 토큰, 연결 문자열, 전체 개인정보를 붙이지 마세요.

featureSQL​

<featureSQL>
<mapper xmlns="mybatis-3-mapper.dtd" namespace="feature">
<select id="L01" desc="조회">
<![CDATA[ SELECT Col FROM T WHERE Key = @Key; ]]>
</select>
<update id="U01"><![CDATA[ UPDATE T SET Col = @Col WHERE Key = @Key; ]]></update>
</mapper>
</featureSQL>

함수 코드가 id(L01)로 호출하며 dataContext.featureSQLPath가 가리킵니다. 파라미터는 객체의 속성 이름으로 전달하고, 값은 바인딩하며 식별자를 문자열로 이어 붙이지 마세요. featureSQL은 dbclient 계약과 별개인 MyBatis형 문법이므로 <param>·<if>를 섞어 쓰지 않습니다.

증상별 점검​

증상확인
EntryType, EntryMethod 확인 필요C# 클래스·네임스페이스가 {App}.Function.{header project}.{transaction}이고 메서드가 command id인지, 또는 entryType·entryMethod를 명시했는지
수정해도 예전 결과원본 저장 → .g.* 재생성 → 배포 경로 → 중복 계약 → 캐시·파일 감시
화면이 비어 있음결과 테이블 개수와 순서, Exception 포함 여부, 컬럼명 대소문자
Node.js 요청이 끝나지 않음callback을 호출하지 않는 경로(예외, 분기 누락)
외부 API 오류타임아웃, 설정에서 읽는 URL, 응답 코드별 Exception 처리
C# 컴파일 오류서버 로그의 동적 컴파일 메시지(모듈 빌드 로그가 아님), using, referenceModuleID

주요 설정​

설정설명
ContractBasePathfunction XML 계약 루트 목록입니다.
NodeFunctionConfigNode.js localStorage, 로그, timeout, 단일/멀티 프로세스, 파일 감시 설정입니다.
CSharpFunctionConfigC# 함수 로그와 파일 감시 설정입니다.
PythonFunctionConfigPython DLL 사용 여부, DLL 경로, 로그와 파일 감시 설정입니다.
FunctionSource함수에서 사용할 데이터 원본, 연결 문자열, 작업 디렉터리입니다.
IsContractFileWatching함수 계약 파일 변경 감시 여부입니다.

FunctionSource 예시는 다음과 같습니다.

{
"ApplicationID": "HDS",
"ProjectID": "*",
"DataSourceID": "FN01",
"DataProvider": "SQLite",
"ConnectionString": "URI=file:../sqlite/HDS/dbclient/HDS.db;Journal Mode=Off;BinaryGUID=False;DateTimeFormat=Ticks;Version=3;",
"IsEncryption": "N",
"WorkingDirectoryPath": "../tmp/HDS/function/HDS_FN01"
}

ack 또는 rdy의 AppSettings:IsConfigurationWatching이 true이면 실행 중 실제로 로드된 module.json의 FunctionSource 변경을 감지해 FunctionMapper.FunctionSourceMappings를 다시 구성합니다. 새 목록 전체가 유효하고 필요한 작업 디렉터리를 준비할 수 있을 때만 기존 캐시를 교체하며, 오류가 있으면 이전 설정과 캐시를 유지합니다. 교체 완료 후 시작하는 요청부터 새 함수 데이터 원본을 사용하고 빈 배열은 캐시 삭제로 반영됩니다.

transact 연동​

transact 거래 계약에서는 CommandType을 F로 둡니다.

{
"ServiceID": "FN010",
"CommandType": "F",
"ReturnType": "Json"
}

라우팅 예시는 다음과 같습니다.

{
"HDS|*|F|D": "http://localhost:8421/function/api/execution",
"HDS|*|F|P": "http://localhost:8421/function/api/execution",
"HDS|*|F|T": "http://localhost:8421/function/api/execution"
}

Python 런타임 메모​

Python 함수는 PythonFunctionConfig.EnablePythonDLL=true일 때 지정된 Python DLL을 로드합니다. Windows에서는 python313.dll 또는 설치 버전에 맞는 DLL 경로를, Linux/macOS에서는 libpython 경로를 실제 환경에 맞춰 지정합니다.

{
"PythonFunctionConfig": {
"EnablePythonDLL": true,
"PythonDLLFilePath": "C:/anaconda3/envs/myenv/python313.dll"
}
}

운영 주의사항​

  • 함수 코드는 서버에서 실행되므로 외부 패키지, 파일 접근, 네트워크 접근 권한을 배포 단위로 검토합니다.
  • EnableFileWatching은 개발 생산성에는 좋지만 운영 환경에서는 의도치 않은 reload를 막기 위해 신중히 사용합니다.
  • Node.js 단일 스레드 설정과 timeout은 장시간 작업 또는 CPU 작업에서 병목이 될 수 있습니다.
  • 작업 디렉터리에는 임시 파일과 실행 결과가 남을 수 있으므로 정리 정책을 둡니다.