본문으로 건너뛰기

서버리스 Function

서버리스 Function은 특정 이벤트에 응답하여 실행되는 작고 독립적인 코드 조각입니다. 이는 개발자들이 서버, 운영 체제 등의 인프라를 직접 관리하지 않아도 되는 기능 코드에만 집중할 수 있게 해주는 클라우드 컴퓨팅 모델입니다.

서버리스 아키텍처는 구글, 마이크로소프트, 아마존 등 다양한 클라우드 기반의 **Function-as-a-Service (FaaS)**를 포함하며, 이를 통해 개발자들은 개별적이고 독립적인 함수들로 애플리케이션을 구성할 수 있습니다. FaaS 제공자는 각 함수를 호스팅하며, 트래픽 요구 사항에 따라 자동으로 스케일링할 수 있습니다.

서버리스 Function은 백엔드 코드를 작성하는 새로운 접근 방식으로, 백엔드를 작성하지 않고도 비즈니스 코드를 작성할 수 있게 해줍니다.

장점 및 단점

Serverless Functions의 장점과 단점은 다음과 같습니다

장점

  1. 스케일링: 서버리스 Function은 애플리케이션의 트래픽에 따라 적은 비용으로 스케일링이 가능합니다. 이는 비즈니스에 따라, 더 많은 함수 인스턴스가 로드를 처리하기 위해 부하를 분산하여 사용 가능해집니다.
  2. 서버 관리의 간소화: 서버리스 모델의 주요 장점은 서버를 다루지 않아도 된다는 것입니다. 서버를 운영하려면 서버가 안전하고 성능이 좋게 설정되어 있는지 확인하기 위해 많은 시간과 전문 지식이 필요합니다.

단점

  1. 디버깅의 어려움: 기본적으로 함수들은 독립적으로 개발되고 운영됩니다. 애플리케이션의 함수 수가 증가하면 관리 및 디버깅하기 어려울 수 있습니다.
  2. 아키텍처의 복잡성: 서버리스 Function은 비즈니스 로직을 수행하는 작고 독립적인 코드 조각입니다. 이는 애플리케이션의 아키텍처를 복잡하게 만들 수 있습니다.

이러한 장단점을 고려하여 서버리스 Function가 개발자의 요구 사항과 가장 잘 맞는지 결정해야 합니다. 서버리스 Function은 모든 상황에 적합하지는 않을 수 있습니다.

한계

서버리스 Function은 마이크로 컨테이너 (Docker)에서 실행되며, 이는 짧은 실행 시간을 가진 프로세스에 대한 클라우드 사업자에 의해 요금 청구를 염두에 두고 설계되었습니다.

서버리스 Function은 GB-초 단위로 청구되며, 최소 청구 기간은 밀리초 단위입니다. 이는 작업 비용이 전통적인 서버 인스턴스보다 서버리스 Function로 실행하는 것이 훨씬 저렴하다는 것을 의미합니다.

이것을 클라우드 사업자 입장이 아닌 개발자의 입장에서 보면, 서버리스 Function은 더 높은 비용을 지불할 수 있습니다.

  • 하나의 기능을 독립적인 배치 프로그램을 만들어 비즈니스 업무를 처리하는 것과 동일하기 때문에 코드의 중복이 발생할 수 있습니다.
  • 표준화된 관리 방안을 만들지 않으면 개발 및 유지보수 비용이 높을 수 있습니다.
  • 서버리스 Function은 실행 시간에 따라 청구되므로, 실행 시간이 길면 비용이 높아집니다.

HandStack 서버리스 Function function 모듈

서버리스 Function은 백엔드 코드를 작성하면 서버 호스팅 앱과 함께 제공하는 모든 이점을 얻을 수 있게 하며, 설정 및 유지 관리 비용이 크게 줄어듭니다. 대신 개발 비용이 늘어납니다.

HandStack 서버리스 Function은 ack 프로그램에 위에 function 모듈의 기능으로 실행되는 함수로서, 다음과 같은 특징을 가지고 있습니다.

  • 서버리스 Function은 컴파일 과정 없이 텍스트 파일로 만들어지며 간단한 배포를 통해 쉽게 배포할 수 있습니다.
  • C#, Node.js 언어를 지원하며, 개수에 제한 없이 서버리스 Function를 관리할 수 있습니다.
  • 서버리스 Function의 개발 및 디버깅, 운영 비용이 줄어듭니다.

C# 서버리스 Function의 예

using System;
using System.Collections.Generic;
using System.Data;
using System.Net;

using HandStack.Core.ExtensionMethod;
using HandStack.Core.Extensions;
using HandStack.Web.MessageContract.DataObject;

using handsup;
using handsup.Entity;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using RestSharp;

namespace HDS.Function.SYS
{
public class SYS020
{
public DataSet? LF01(List<DynamicParameter> dynamicParameters, DataContext dataContext)
{
string typeMember = "SYS.SYS020.LF01";
using (DataSet? result = new DataSet())
{
result.BuildExceptionData();

string userWorkID = dynamicParameters.Value("UserWorkID").ToStringSafe();
string applicationID = dynamicParameters.Value("ApplicationID").ToStringSafe();
string serverID = dynamicParameters.Value("ServerID").ToStringSafe();
string globalID = dynamicParameters.Value("GlobalID").ToStringSafe();
string environment = dynamicParameters.Value("Environment").ToStringSafe();
string projectID = dynamicParameters.Value("ProjectID").ToStringSafe();
string serviceID = dynamicParameters.Value("ServiceID").ToStringSafe();
string transactionID = dynamicParameters.Value("TransactionID").ToStringSafe();
string startedAt = dynamicParameters.Value("StartedAt").ToStringSafe();
string endedAt = dynamicParameters.Value("EndedAt").ToStringSafe();

if (string.IsNullOrEmpty(userWorkID) == true
|| string.IsNullOrEmpty(applicationID) == true
|| string.IsNullOrEmpty(startedAt) == true
|| string.IsNullOrEmpty(endedAt) == true
)
{
result.BuildExceptionData("Y", "Warning", "필수 요청 정보 확인 필요", typeMember);
goto TransactionException;
}

try
{
var logServerListUrl = dataContext.functionHeader.Configuration?["LogServerListUrl"].ToStringSafe();
string url = $"{logServerListUrl}?applicationID={applicationID}&serverID={serverID}&globalID={globalID}&environment={environment}&projectID={projectID}&serviceID={serviceID}&transactionID={transactionID}&startedAt={startedAt}&endedAt={endedAt}";
var client = new RestClient();
var request = new RestRequest(url, Method.Get);
request.Timeout = 3000;
request.AddHeader("AuthorizationKey", ModuleConfiguration.AuthorizationKey);
RestResponse response = client.Execute(request);
if (response.StatusCode != HttpStatusCode.OK)
{
result.BuildExceptionData("Y", "Warning", $"{url} 로그 서버 실행 정보 확인 필요", typeMember);
}
else
{
using var table = JsonConvert.DeserializeObject<DataTable>(response.Content.ToStringSafe());
if (table != null)
{
result.Tables.Add(table);
}
else
{
result.BuildExceptionData("Y", "Warning", $"{url} 로그 서버 응답 정보 확인 필요", typeMember);
}
}
}
catch (Exception exception)
{
result.BuildExceptionData("Y", "Error", exception.Message, typeMember);
goto TransactionException;
}

TransactionException:
if (result.Tables.Count == 1)
{
result.Tables.Add(new DataTable());
}

return result;
}
}
}
}

코드) 거래 이력정보 조회 예제

Node.js 서버리스 Function의 예

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

module.exports = {
IF01: (callback, moduleID, parameters, dataContext) => {
(async () => {
var result = {
DataTable1: [{
ErrorMessage: null
}]
};

var sqlParameters = {
USER_ID: '',
MESSAGE_CONTENT: '',
MESSAGE_TEMPLATE_ID: '',
SEND_YN: 'N',
RETURN_RESULT: ''
};

try {
var companyNo = $array.getValue(parameters, 'COMPANY_NO');
var userID = $array.getValue(parameters, 'USER_ID');
var toPhoneNumber = $array.getValue(parameters, 'PHONE_NO').replace(/-/g, '');
var contents = $array.getValue(parameters, 'MESSAGE_CONTENT');
var messageTempleteID = $array.getValue(parameters, 'MESSAGE_TEMPLATE_ID');
var isForceEasyworkSenderKey = $string.toBoolean($array.getValue(parameters, 'FORCE_EASYWORK_SEND'));
var moduleConfig = qaf.getModuleLibrary(moduleID).config;

sqlParameters = {
USER_ID: userID,
PHONE_NO: toPhoneNumber,
MESSAGE_CONTENT: contents,
MESSAGE_TEMPLATE_ID: messageTempleteID,
SEND_YN: 'N',
RETURN_RESULT: ''
};

var config = {
method: 'get',
url: 'http://localhost:8000/easywork/api/message-sender/SendAlarmTalkDirect?companyNo={0}&userNo={1}&templateCode={2}&phoneNo={3}&isForceEasyworkSenderKey={4}&content={5}'
.format(companyNo, userID, messageTempleteID, toPhoneNumber, isForceEasyworkSenderKey, encodeURI(contents))
};

axios(config)
.then(function (response) {
if (response) {
var kakaoResponse = response.data;

sqlParameters.SEND_YN = (kakaoResponse.statusCode == 200 || kakaoResponse.statusCode == 202) ? 'Y' : 'N';
sqlParameters.RETURN_RESULT = JSON.stringify(kakaoResponse);

if (sqlParameters.SEND_YN == 'N') {
result.DataTable1[0].ErrorMessage = 'transport - ' + sqlParameters.RETURN_RESULT;
}
}
else {
sqlParameters.RETURN_RESULT = '알림톡 발송 실패';
}

syn.$s.executeQuery(moduleID, 'I01', sqlParameters, function (error, queryResult) {
if (error) {
result.DataTable1[0].ErrorMessage = 'executeQuery I01 - ' + error.message;
syn.$l.moduleEventLog(moduleID, 'IF01', 'Message: {0}, Error: {1}'.format(sqlParameters.RETURN_RESULT, error.message), 'Error');
}

return callback(null, result);
});
})
.catch(function (error) {
var errorMessage = 'Url: {0} - Message: {1}'.format(config.url, error.message);
syn.$l.moduleEventLog(moduleID, 'IF01', errorMessage, 'Error');
sqlParameters.RETURN_RESULT = errorMessage;

syn.$s.executeQuery(moduleID, 'I01', sqlParameters, function (error, queryResult) {
if (error) {
result.DataTable1[0].ErrorMessage = errorMessage;
syn.$l.moduleEventLog(moduleID, 'IF01', 'Message: {0}, Error: {1}'.format(sqlParameters.RETURN_RESULT, error.message), 'Error');
}

return callback(null, result);
});
});
} catch (error) {
var errorMessage = 'Unhandle Message: {0}'.format(error.message);
syn.$l.moduleEventLog(moduleID, 'IF01', errorMessage, 'Error');
result.DataTable1[0].ErrorMessage = errorMessage;
sqlParameters.RETURN_RESULT = errorMessage;

syn.$s.executeQuery(moduleID, 'I01', sqlParameters, function (error, queryResult) {
if (error) {
result.DataTable1[0].ErrorMessage = 'executeQuery I01 - ' + error.message;
syn.$l.moduleEventLog(moduleID, 'IF01', 'Message: {0}, Error: {1}'.format(sqlParameters.RETURN_RESULT, error.message), 'Error');
}

return callback(null, result);
});
}
})();
}
}

코드) 알림톡 발송 예제