정윤진 피보탈 프린시플 테크놀로지스트

▲ 정윤진 피보탈 프린시플 테크놀로지스트

[컴퓨터월드] 함수형 서비스란
서버리스(Serverless)로 대표되는 함수 레벨의 서비스는 언급된 지가 좀 됐다. 실질적으로 시장의 대규모 호응을 얻었던 도구는 아마존웹서비스의 람다(Lambda)라고 봐도 무방할 듯 하다.

서버리스나 함수형 애플리케이션 서비스의 가장 큰 특징은 ‘이벤트 드리븐’이다. 아마존웹서비스에서 소개한 람다의 첫 번째 사용 데모 역시 이벤트를 통해 호출된 함수가 특정 작업을 수행하는 방식이었다. 대표적인 것이 바로 S3에 업로드된 사진 이미지에 대한 썸네일을 만들어 S3에 저장하는 데모였다.
 
S3의 특정 버켓에 새로운 이미지가 업로드 되면, 이 이벤트를 바탕으로 특정 람다 함수를 호출한다. 이때 이벤트와 함께 전달되는 메세지 페이로드에는 S3에 새로 업로드된 파일의 위치 정보가 포함된다. 람다 서비스가 이 정보를 함수에 전달해서 구동하면, 함수는 원본 파일을 받아다가 썸네일을 만드는 코드가 동작한다. 그리고 새로운 썸네일 전용의 S3 버켓에 저장한다.
 
이때 주의할 점은 원본 S3 버켓에 결과물을 다시 저장하면 무한 루프에 빠지기 때문에 조심해야 한다는 점이다. 업로드한 썸네일 이미지가 다시 새로운 이미지 업로드 이벤트로 취급되어 썸네일의 썸네일을 만드는 무한 루프로 동작하기 때문이다. 이 데모는 다음의 링크에서 람다 서비스 입문용으로 매우 잘 설명되어 있다.
▲https://docs.aws.amazon.com/lambda/latest/dg/with-s3-example.html
 
FaaS(Function as a Service) 로 대변되는 함수 레벨의 서비스 구현에는 다양한 장점이 있다. 완전히 기능 위주의 간략한 코드를 만들어 배포할 수 있으며, 아마존 웹 서비스와 같이 클라우드 기반의 생태계에서 매우 높은 효율을 자랑한다. 실제로 아마존 웹 서비스는 이를 위해 자사의 다양한 서비스에서 람다 함수 호출을 위한 방법을 편리하게 제공하고 있다.
 
하지만 조금 더 생각해 보면 이런 생태계 내에서 사용이 편리한 도구들은 다시 그 생태계를 벗어나면 사용에 일부 제약이 따른다는 점도 존재한다. 추가적으로 클라우드 서비스 공급자들은 대부분의 경우 Limit(Quota로 이해해도 좋다)을 두어 전체 사용량을 관리한다. 동시에 호출할 수 있는 함수의 숫자를 제한한다던지, 실행 시간에 제약을 둔다던지, 앞에 API 게이트웨이를 연동하려는 경우 해당 클라우드 서비스 공급자의 구조에 대한 자세한 이해가 없다면 어디서 문제가 발생하고 있는지 추적이 힘들 수 있다는 점 등이다.
 
그럼에도 불구하고 서버리스 컴퓨팅, 이벤트 주도, 제작과 배포의 규모가 작은, 그리고 다양한 비용할인 혜택과 ‘관리형’ 서비스라는 장점에 힘입어 수많은 개발자들로 부터 관심을 받고 있다. 따라서 각 클라우드 서비스 공급자들, AWS, 애저, GCP등은 이 부분에 있어 상당한 투자를 진행하고 있기도 하다.
 
 
프로젝트 리프riff
프로젝트 리프(https://projectriff.io/)는 오픈소스 프로젝트로서, 피보탈의 클라우드 파운드리 체계에 함수형 서비스 제공을 위해 시작된 프로젝트다. 각 클라우드 서비스 공급자가 제공하는 함수형 도구들과는 또 다른 범용성을 지니고 있는데, 다양한 환경에서 함수형 서비스의 장점을 누릴 수 있도록 하는 것이 목적이기 때문이다. 몇 가지 특징을 소개하면 다음과 같다.

■ 개발자들은 함수를 작성하면 된다.
■ 작성된 함수형 서비스는 컨테이너를 사용해 패키징된다.
■ 다양한 언어 지원을 위해 이벤트 브로커로 Go로 작성된 사이드카가 제공된다.
■ 함수와 토픽은 쿠버네티스(Kubernetes) 리소스로 취급된다.
■ 이벤트의 양에 따라 함수 역시 확장된다.
■ 스트림 처리가 가능하다.

위의 장점들은 몇 가지 부분에서 충격적이다. 특히 기존 서버리스 컴퓨팅을 제공하는 대부분의 함수형 서비스 도구들은 ‘실행 시간 제약’ 이라는 측면에 있어 스트림 처리가 쉽지 않다. 아울러 쿠버네티스 위에서 사용할 수 있다는 점은 프라이빗, 퍼블릭을 가리지 않고 사용할 수 있다는 장점 역시 제공한다. 또한 기본적으로 프락시 패턴이 사용된 사이드카를 통해 다양한 언어와 프로토콜을 사용하는 환경, 현재로서는 gRPC와 HTTP를 통해 이벤트를 처리할 수 있다는 것 역시 매우 뛰어난 강점 중의 하나다. 이런 다양한 장점들을 하나씩 살펴보도록 하자.
 
개발자들은 함수를 작성하면 된다.
함수는 다양한 언어로 작성 가능하다. 이는 스코프를 아주 작은 단위로 유지할 수 있는 장점을 제공한다. 자바스크립트를 예로 들면, 하나의 함수를 하나의 파일을 사용해서 작성할 수 있다.

// square.js
module.exports = (x) => x**2

자바의 경우 함수의 스코프에서 필요한 패키지, 예를 들면 ‘java.util.Funciton’을 JAR로 패키징 하는 방법으로 구현할 수 있다.
 
함수는 컨테이너로 패키징 된다.
작성된 코드는 기본 템플릿을 사용해서 컨테이너 이미지로 만들어진다. 이때 어떤 기본 이미지를 사용할지의 여부는 어떤 언어를 사용했는가에 따라 자동으로 호출되는 방식을 사용한다. 현재 프로젝트 리프에서는 쉘, 자바스크립트, 파이썬, 자바의 기본 이미지를 제공하지만, 로드맵에는 사용자가 직접 기본 이미지를 만들 수 있는 방법에 대해 문서화를 통해 제공할 예정이다. 이는 기본적으로 템플릿 패턴의 적용이며, 프로젝트 팀은 이를 제어반전(Inversion of Contorl) 이라고 설명한다.
 
함수와 이벤트 브로커의 연결은 사이드카로 처리한다.
사이드카 패턴은 일반적으로 동작하는 메인 애플리케이션과 플랫폼 도구 사이의 연동을 위해 사용하는 패턴이다. 프로젝트 리프에서는 서로 다른 언어로 작성된 함수들이 동일한 방법으로 이벤트를 주고받을 수 있도록 사이드카를 함께 제공한다. 함수 컨트롤러(Function Controller)는 사이드카를 컨테이너로 배포된 함수가 동작하는 포드(Pod)와 동일한 포드에 함께 배포한다. 이는 프락시 패턴을 사용한 또 다른 제어 반전의 형태라고 볼 수 있다. 사이드카의 핵심적인 역할은 호출자, 즉 각각의 함수를 가장 간단한 형태로 유지할 수 있는 장점을 제공한다.
 
사이드카는 함수와 이벤트 브로커 사이의 모든 통신을 관장한다. 주로 사용되는 대부분의 프로토콜을 통해 이벤트를 주고받을 수 있도록 설계 되어 있다. 현재로서는 HTTP와 gRPC, 네임드 파이프(named pipe)를 지원하며 더 많은 도구들을 제공할 예정이다.
 
여러 개의 함수가 바라보고 있는(subscribed) 토픽의 메시지를 전달하는 방법 중 하나는 프로젝트 리프에서 제공하는 HTTP 게이트웨이를 사용하는 것이다. 게이트웨이는 URI 경로와 매칭하는 토픽의 이름으로 메세지를 전달한다. ‘/messages’은 이벤트를 전달만 하는 경우에 사용하며, ‘/requests’의 경우에는 요청과 응답 모두를 처리하기 위해 사용한다.
 
조금 더 자세히 살펴보면, HTTP 게이트웨이는 ‘/messages’로 수신된 요청을 트랜스폼 해서 브로커로 전달한다. ‘/requests’로 포스트(post)된 메시지에도 동일한 처리를 하지만, 이 경우에는 브로커로부터의 응답을 기다리고 응답이 오면 이를 http 응답으로 전달한다. 이 게이트웨이에는 더 많은 옵션이 추가될 예정이다. 프로젝트 리프의 HTTP 게이트웨이에 대한 더 자세한 설명과 코드는 아래에서 확인할 수 있다.
▲https://github.com/projectriff/riff/tree/master/http-gateway
 
함수와 토픽은 쿠버네티스의 리소스로 취급된다.
쿠버네티스는 커스텀 리소스 정의(Custom Resource Definitions)를 통해 확장(Extensions)을 지원한다. 리프에서는 두개의 확장 리소스 정의를 사용하는데, 바로 펑션(Function)과 토픽(Topic)이다. 이 리소스들은 YAML을 사용해서 정의된다. 이는 쿠버네티스 내에서 리소스의 상태 변화, 즉 추가, 수정, 삭제를 살펴볼 수 있는 기능을 제공한다.
 
펑션 컨트롤러는 펑션 리소스를 살피며, 토픽 컨트롤러는 토픽 리소스의 상태를 살피는 역할을 한다. 새로운 함수가 추가되면 펑션 컨트롤러는 새롭게 추가된 함수와 사이드카를 포함한 배포와 확장 등의 역할에 관여한다. 토픽 컨트롤러는 아파치 카프카(Kafka)를 사용해 이벤트 브로커에서 사용할 새로운 토픽을 등록하며, 이후 더 많은 도구가 추가될 예정이다.
 
이벤트의 양에 따른 함수 확장이 가능하다.
펑션 컨트롤러는 동작하는 함수 외에도 이벤트의 동작을 함께 살핀다. 함수가 관여된 이벤트가 발생하는 즉시 0에서 1로 확장한다. 이후 1-N-1의 방식으로 확장하는데, 이때 기준값은 토픽에 있는 이벤트의 수와 처리중인 이벤트의 숫자 사이의 지연시간이다. N값은 함수의 YAML 설정값 ‘maxReplicas’를 통해 조절할 수 있으며, 기본값은 함수의 입력 토픽의 파티션 개수가 기본값이 된다. 펑션 컨트롤러는 활성화된 이벤트가 0인 순간(idle-timeout)에는 함수의 숫자를 0으로 유지한다. 이 역시 YAML 설정을 통해 수정할 수 있으며, 기본값은 10초로 지정된다.
 
함수는 스트림을 처리할 수 있다.
프로젝트 리프의 가장 중요한 목표중 하나는 이벤트 스트림 프로세싱을 지원하는 것이다. 한번에 하나의 요청만 처리하는 구조는 매우 제한적이다. 리프 팀은 다양한 언어와 라이브러리를 사용하여 일관성과 스트림 지원을 제공하고자 한다. 자바의 경우 리액터(Reactor)와 플럭스(Flux) 타입을 인풋과 아웃풋에 사용할 수 있다. 스프링원 플랫폼 이벤트에서 리프 팀은 60초간 유입되는 투표의 카운트 결과를 매 2초마다 제공하는 데모를 공개했다

public Flux<Map<String, Object>> windows(Flux<String> words) {
return words.window(Duration.ofSeconds(60), Duration.ofSeconds(2))
??.concatMap(w -> w.collect(VoteAggregate::new, VoteAggregate::sum)
??.map(VoteAggregate::windowMap), Integer.MAX_VALUE);
}

 
로드맵
2017년 12월에 발표된 리프 팀의 로드맵은 다음의 내용을 포함한다.

■ Function Controller를 Go로 포팅
■ 카프카(Kafka)외의 이벤트 브로커 사용을 위한 추상화
■ 사이드카와 펑션 호출자(invoker)를 위해 r소켓(rsocket) 사용을 검토
■ 사전에 구동된(pre-warmed) 리소스 풀을 사용한 동적 함수 로딩 프로토타이핑
■ 스케일링을 위한 알고리즘과 설정 방식의 고도화
■ 함수에서 설정해 사용 가능한 볼륨(Volume), 즉 디스크 지원

 
사용해 보기 - Docker on OSX
개발 환경으로 맥을 사용하는 사람이 많으므로 이를 기준으로 설명한다.

1. 도커 스토어(docker store)에서 도커 포 맥 CE 엣지(Docker for Mac CE Edge) 버전을 다운받아 설치한다. 이 버전은 쿠버네티스를 포함하고 있다.
2. 도커 설정에서 메모리를 4기가바이트로 변경해 준다.
3. 설정탭 상단의 쿠버네티스에서 ‘Enable Kubernetes’를 선택한다. 만약 미니쿠버(minikube) 등을 사용하고 있다면 스킵하자.
4. helm 환경을 설정한다.
5. helm에 ‘projectriff repository’를 추가하고 ‘helm repo update’를 수행하자.
6. helm을 이용해 리프 환경을 쿠버네티스에 배포한다.
7. 리프 CLI를 설치한다.
8. 샘플 코드를 작성한다.
9. 리프 CLI를 통해 작성된 코드를 함수로 배포한다.
10. 리프 CLI를 통해 이벤트를 생성하고, 함수가 적절히 동작하는지 확인한다.

주의할 점은, helm 2.9.0 버전에서는 ‘~/.kube/config’의 정보를 제대로 참조하지 못하는 현상이 발생할 수 있다는 것이다. 이때는 ‘helm init —service-account default’ 커맨드 또는 2.9.1 버전으로의 업그레이드를 활용하자. brew를 사용한 ‘kubernetes-helm’은 글을 작성하는 현재 시점에서 2.9.0이 기본 버전이다.)
 
위의 순서대로 설치를 진행하면 별 무리없이 riff를 사용할 수 있다. 자세한 설명은 다음의 프로젝트 리프 페이지를 참고하도록 하자.
▲https://projectriff.io/docs/getting-started-on-docker-ce-edge-for-mac/
 
 
데모 함수의 구동 - NodeJS
프로젝트 리프가 쿠버네티스 위에 성공적으로 설치되면, ‘kubectl’ 커맨드로 리프 시스템(riff-system) 네임스페이스 배포를 확인할 수 있다.

$ kubectl get po,deploy --namespace riff-system
NAME READY STATUS RESTARTS AGE
po/projectriff-kafka-689687dd94-dxcxb 1/1 Running 2 21d
po/projectriff-riff-function-controller-6c957d5b-lpfhm 1/1 Running 3 21d
po/projectriff-riff-http-gateway-9ff9cc545-fncx4 1/1 Running 3 21d
po/projectriff-riff-topic-controller-5c9c7cc559-kzx8j 1/1 Running 1 21d
po/projectriff-zookeeper-586757b55c-gkv87 1/1 Running 0 21d

NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
deploy/projectriff-kafka 1 1 1 1 21d
deploy/projectriff-riff-function-controller 1 1 1 1 21d
deploy/projectriff-riff-http-gateway 1 1 1 1 21d
deploy/projectriff-riff-topic-controller 1 1 1 1 21d
deploy/projectriff-zookeeper 1 1 1 1 21d

다음 페이지에서 리프 명령줄 도구를 다운로드해 설치할 수 있다.
▲https://github.com/projectriff/riff/releases
 
‘riff-system’에서 각 언어별 함수의 실행은 인보커(invoker)가 담당한다. 현재 리프에서는 Go, 자바, 노드, 파이썬 2·3, 그리고 명령줄 도구를 지원한다. 각각의 인보커를 다음의 명령어를 통해 준비할 수 있다.
 
$ riff invokers apply -f https://github.com/projectriff/command-function-invoker/raw/v0.0.6/command-invoker.yaml
$ riff invokers apply -f https://github.com/projectriff/go-function-invoker/raw/v0.0.2/go-invoker.yaml
$ riff invokers apply -f https://github.com/projectriff/java-function-invoker/raw/v0.0.6/java-invoker.yaml
$ riff invokers apply -f https://github.com/projectriff/node-function-invoker/raw/v0.0.8/node-invoker.yaml
$ riff invokers apply -f https://github.com/projectriff/python3-function-invoker/raw/v0.0.6/python3-invoker.yaml
 
각 언어별 샘플 코드는 프로젝트 리프의 인보커 깃허브 페이지에서 찾을 수 있다.
▲자바: https://github.com/projectriff/java-function-invoker/tree/master/samples/
▲노드: https://github.com/projectriff/node-function-invoker/tree/master/samples
 
아래와 같이 샘플 코드를 작성하고, 리프 명령어를 사용해서 배포해 보자.

# square.js
module.exports = (x) => x ** 2

$ cat square.js
module.exports = (x) => x ** 2

$ riff create node --name square --input numbers --filepath .
Initializing /Users/younjinjeong/Workspace/riff/sample/square-topics.yaml
Initializing /Users/younjinjeong/Workspace/riff/sample/square-function.yaml
Initializing /Users/younjinjeong/Workspace/riff/sample/Dockerfile
Skipping existing file /Users/younjinjeong/Workspace/riff/sample/.dockerignore - set --force to overwrite.
Building image ...
[STDOUT] Sending build context to Docker daemon 6.144kB
[STDOUT] Step 1/3 : FROM projectriff/node-function-invoker:0.0.8
[STDOUT] ---> 6b42d1262422
[STDOUT] Step 2/3 : ENV FUNCTION_URI /functions/square.js
[STDOUT] ---> Using cache
[STDOUT] ---> d665991c630c
[STDOUT] Step 3/3 : ADD square.js ${FUNCTION_URI}
[STDOUT] ---> Using cache
[STDOUT] ---> c82ee8223129
[STDOUT] Successfully built c82ee8223129
[STDOUT] Successfully tagged younjinjeong/square:0.0.1
Applying resources in .

function "square" created
topic "numbers" created


이제 기본 네임스페이스에 배포된 함수와 토픽을 살펴보는 명령어를 구동하고, 토픽에 숫자를 넣어 올바른 값이 나오는지 확인해 보자.

# 1초마다 배포한 함수와 토픽을 확인
$ watch -n 1 kubectl get functions,topics,pods,deployments

# numbers 토픽에 입력값을 퍼블리싱
$ riff publish --input numbers --data 10 --reply
Posting to http://127.0.0.1:30511/requests/numbers
100

# 동일한 명령어를 짧게
$ riff publish -i numbers -d 10 -r
Posting to http://127.0.0.1:30511/requests/numbers
100

# 동일한 명령을 반복적으로 계속 실행해 보도록 하자.

 
동작 방식
먼저 시스템 측면에서 살펴보면, 프로젝트 리프는 맥 운영체제의 도커 도구를 통해 동작하는 쿠버네티스위에 helm을 통해 배포됐다. ‘Kubectl’ 명령어를 사용해 리프 시스템으로 배포를 확인할 수 있다. 그리고 리프 시스템은 카프카 도구와 함께 배포됐다는 점을 기억하자.
 
지원되는 언어를 바탕으로 함수를 작성하고 리프 명령어를 배포했다. 위의 예제에서는 노드함수였는데, 입력값을 숫자로 받으면 제곱을 수행하는 함수를 ‘square.js’로 저장하고 함수를 생성하는 명령어를 사용했다.

$ riff create node --name square --input numbers --filepath .

먼저, ‘riff create’ 명령어 다음에 사용할 인보커로 노드를 지정했음을 알 수 있다. 그리고 리프 시스템에서 사용할 이름을 지정하고, ‘--input’을 통해 입력값을 받을 카프카 토픽을 명시한다. 그리고 ‘--filepath’는 실제 함수가 저장된 파일의 위치를 리프에 제공한다. 이 명령어를 수행하면 디렉토리에 ‘square.js’ 외에 3개의 파일이 더 생성되는 것을 확인할 수 있다. 각 파일의 용도와 내용을 살펴보자.

Dockerfile
$ cat Dockerfile
FROM projectriff/node-function-invoker:0.0.8
ENV FUNCTION_URI /functions/square.js
ADD square.js ${FUNCTION_URI}

프로젝트 리프는 제공된 함수를 도커 이미지로 패키징해 사용한다. 이때 원본이 되는 다커 이미지가 ‘FROM’절에서 지정됐다. 이후 리프를 통해 제공된 ‘square.js’ 함수가 저장된 위치를 환경 변수에 지정한다.

square-function.yaml
$ cat square-function.yaml
---
apiVersion: projectriff.io/v1alpha1
kind: Function
metadata:
name: square
spec:
container:
image: younjinjeong/square:0.0.1
input: numbers
protocol: grpc

‘square-function.yaml’ 파일은 조금 더 흥미롭다. ‘name’에서는 함수에 사용할 이름 외에는 특이한 부분이 없으므로 넘어가도록 하자.
 
‘protocol.’은 사이드카와 함수 인보커 사이에 사용할 프로토콜을 지정한다. 기고문을 쓰는 현재 gRPC, http, stdio를 지원하고 있다. 함수형 서비스를 사용하는 가장 중요한 이유가 바로 이벤트 주도로 자원을 효율적으로 사용하고, 특정 목적의 기능을 단순하게 처리하며, 개발자 친화적인 사용성을 제공하기 위해서다. 사이드카의 역할은 메시지 버스인 카프카의 ‘numbers’ 토픽에 퍼블리싱된 입력값을 함수를 실행하는 인보커에 전달하는 역할을 한다.
 
‘input’은 함수가 사용할 이벤트 토픽이다. 이 경우에는 ‘numbers’ 토픽을 사용하고 있다. 예제에서는 ‘riff publish --input numbers --data 10 —reply’ 명령어를 통해 ‘numbers’ 토픽에 10이라는 데이터를 퍼블리싱 한다. 그러면 사이드카가 카프카의 ‘numbers’ 토픽에 유입된 10을 받아 인보커를 통해 함수에 전달한다.
 
‘image’는 이 함수가 적재되어 사용할 컨테이너 이미지다. ‘riff create’ 구문을 사용할 때 이 이미지는 자동으로 만들어지고, 함수가 처음 호출될 때 그리고 이벤트의 양이 많아지면 더 많은 컨테이너를 만들어야 할 때 사용한다.

square-topics.yaml
$ cat square-topics.yaml
---
apiVersion: projectriff.io/v1alpha1
kind: Topic
metadata:
name: numbers

이 파일은 함수가 사용할 토픽을 명시한다. ‘numbers’ 토픽이 ‘square’ 함수를 위한 데이터를 받는 역할을 한다. 즉, 카프카에 어떤 토픽을 어떻게 준비해야 하는지 알려주는 역할을 한다. 다음과 같이 파일에 내용을 추가하고 함수를 업데이트 해 보자.

$ cat square-topics.yaml
---
apiVersion: projectriff.io/v1alpha1
kind: Topic
metadata:
name: numbers
spec:
partitions: 3

$ riff update ?n square

위의 명령을 사용하면 달라지는 것은, ‘numbers’에 유입되는 메시지가 많아지면 카프카의 토픽을 최대 3개까지 파티셔닝 한다는 것이다. 즉 이벤트의 유입량에 따라 이를 처리하기 위한 메시지 브로커의 처리량도 확장할 수 있다는 것이다. 이러한 확장은 마치 오토스케일링과 같이 지정한 숫자 범위 내에서 리프 시스템을 통해 자동으로 이루어진다.
 
 
함께 보기 - 스프링 클라우드 펑션
스프링에 친숙한 분들이라면, 더욱 쉽게 함수형 서비스를 사용할 수 있다. 스프링 클라우드 펑션(Spring Cloud Functions)은 현재 AWS의 람다, 마이크로소프트 애저, 그리고 아파치 오픈위스크(OepnWhisk)를 지원하고 있다. 위의 NodeJS 샘플 애플리케이션과 동일하게, 스프링 클라우드 펑션을 사용한 간단한 스프링 함수 애플리케이션을 리프에 배포해 보도록 하자. 배포에 사용한 애플리케이션은 아래 링크에 있다.
▲https://github.com/projectriff/java-function-invoker/tree/master/samples/greeter

package functions;

import java.util.function.Function;

public class Greeter implements Function<String, String> {

public String apply(String name) {
return "Hello " + name;
}
}


다른 것 없이, ‘String’이 유입되면 여기에 ‘Hello’를 앞에 붙여 리턴한다. 예제를 ‘mvn clean package’를 통해 빌드하고 ‘riff create’를 사용해서 배포한다.

$ riff create java ?-input names ?-handler functions.Greeter
Initializing /Users/younjinjeong/Workspace/riff/java-sample/java-function-invoker/samples/greeter/greeter-topics.yaml
Initializing /Users/younjinjeong/Workspace/riff/java-sample/java-function-invoker/samples/greeter/greeter-function.yaml
Initializing /Users/younjinjeong/Workspace/riff/java-sample/java-function-invoker/samples/greeter/Dockerfile
Building image ...

Applying resources in .

function "greeter" created
topic "names" created

NodeJS를 사용했을 때와 다른 점은, ‘--handler’ 를 사용해서 엔트리 포인트를 지정해 준다는 점이다. 그 외의 컨셉은 모두 동일하다. ‘Kubectl’ 명령어를 통해 함수와 토픽이 잘 생성되었는지 확인해 보자.

$ kubectl get functions,topics,pods,deployments
NAME AGE
functions/greeter 9m

NAME AGE
topics/names 9m

NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
deploy/greeter 0 0 0 0 9m

‘names’ 토픽에 데이터를 퍼블리싱 해 보자.

$ riff publish --input names --data 'Computer World!' --reply
Posting to http://127.0.0.1:30511/requests/names
Hello Computer World!

‘--reply’ 없이 퍼블리싱 해 보자.

$ riff publish --input names --data 'Computer World'
Posting to http://127.0.0.1:30511/messages/names
Message published to topic: names

무엇이 다른지 발견 할 수 있는가? 바로 HTTP 게이트웨이에서 받는 URL이 앞서 설명한 바와 같이 달라진다.
 
최초로 데이터를 전달하면, 처음 실행할 때는 두번째 실행할 때보다 시간이 오래 걸린다. 이는 이벤트가 처음 유입 되었을 때 리프 프로젝트 내부적으로 함수를 실행할 리소스를 준비하기 때문이다. 한 번 준비가 되고 나면 두 번째 응답부터는 빠르게 진행되며, 이벤트의 양이 많아지게 되면 리프 시스템은 함수를 실행할 리소스를 확장한다.
 
사용된 토픽과 함수를 제거하고자 한다면 ‘$ riff delete —all greeter’ 명령어를 사용하자.
 
 
한걸음 더
아래는 riff 프로젝트를 주도하고 있는 피보탈의 블로그에 소개되고 있는 ETL 사용 사례다. 원문은 다음의 링크에서 확인이 가능하다.
▲https://content.pivotal.io/blog/building-functions-with-riff
 
▲ 프로젝트 리프 구성도

그림을 보면 리프가 동작하는 방식에 대해 조금 더 쉽게 이해할 수 있다. 예제에서는 지정된 카프카 토픽에 데이터를 전달하기 위해 리프 명령줄 도구를 사용했다. ‘riff publish’ 명령을 사용하면 항상 ‘Posting to http://127.0.0.1:30511/requests/numbers’이라는 메시지를 확인할 수 있을 것이다.
 
이 메시지의 의미는 로컬 머신에서 동작하는 쿠버네티스 위에 리프 시스템의 http 게이트웨이에 데이터를 포스팅 하면, 이 게이트웨이가 카프카의 적절한 토픽에 데이터를 전달한다는 것이다. 그럼 사이드카는 이 토픽에 유입된 데이터와 함께 지정된 함수 인보커에 전달하고, 인보커는 컨테이너 안에 있는 함수를 실행한다. 이때 ‘--reply’ 가 있다면 함수 실행 결과에 대한 응답을 기다리고, 없다면 기다리지 않는다.
 
위의 그림에서는 다양한 데이터 소스로 부터 동일한 동작을 리프를 통해 처리할 수 있음을 보여준다. 각각의 서비스에서는 HTTP 게이트웨이로 데이터를 전달하고, 이를 통해 지정된 역할을 하는 함수를 호출해서 처리한다. 심지어 여기에 적용한 것은 일종의 사가 패턴(Saga Pattern)으로 볼 수 있는데, 하나의 토픽으로 유입된 메시지를 통해 특정 로직을 처리하는 함수가 호출되고, 이 함수는 결과를 다른 토픽으로 퍼블리싱 한다. 그러면 다른 토픽을 보고 있는 함수를 통해 다시 다른 작업이 처리되는 형태이며, 결과적으로 하나의 데이터 소스에 처리한다.
 
이와 같은 사용성은 보통 데이터 마이크로서비스로 불리는 파이프라인을 구현할 때 자주 사용되는 흐름이다. 하지만 프로젝트 리프를 사용하면 함수형으로 처리가 가능하며, 경우에 따라서는 이 모든 작업을 스트림으로 처리할 수도 있을 것이다.
 
 
결론
다양한 클라우드 서비스에서 함수형 서비스들을 선보이고 있다. 이들은 각각 특징이 있는데, 하나를 꼽으라면 자사가 제공하는 서비스들에서 발생한 이벤트 연동이 매우 쉽다는 점이다. S3를 사용하면 람다 함수를 호출하기 쉬운 것이 대표적인 사례다. 반면 대표적인 단점은 규모의 확장 또는 실행 시간 등에 제약이 발생할 수 있다는 점이다. 이런 제약들은 시간이 가면서 나아질 수 있고, 경우에 따라서는 해당 클라우드 서비스의 지원을 통해 해결할 수 있는 경우도 있다.
 
프로젝트 리프가 제공하는 장점은 바로 쿠버네티스 서비스 위에서 동작하는 함수형 서비스라는 점이다. 물론 현재는 개발 단계이며, 실 서비스에 적용하는 것은 다소 무리일 수 있다. 하지만 이벤트 주도 서비스를 함수를 기반으로 한 간단한 형태로 구현할 수 있다는 점은 이후 실 서비스에서 사용 가능할 때 대단한 장점이 될 수 있다. 마이크로서비스를 지나 마이크로코드의 형태로 서비스를 디자인하고, 빠르게 구현해서 배포가 가능한 것이다.
 
다른 장점은 개발자 입장에서는 완전히 하위 레벨의 동작에 대해서는 잊어도 된다는 점이다. 경우에 따라서 이것은 단점이 될 수도 있지만, 토픽의 확장이나 함수의 실행 규모에 대해서 개발자는 걱정할 필요가 없다. 다만 애플리케이션 수준에서의 동시성 처리나, 코드 내에는 보이지 않는 무한 루프의 가능성 등은 조심할 필요가 있다.
 
현존하는 대부분의 메시지 브로커는 메시지 전달에 1:N, 또는 1:1을 적용하고 있으므로, 클러스터 수준에서 다수의 컨슈머, 이 경우에 함수가 존재할 가능성이 있는 경우를 염두해 둔다면 클라우드와 같이 확장성 있는 환경에서 매우 탁월한 사용성을 보일 것이다.
 
무한 루프의 경우, 만약 ‘input’ 토픽과 ‘output’ 토픽을 동일하게 사용한다면 코드 내에서는 발견하기 쉽지 않은 루프에 빠질 가능성이 있다. 이는 단일 함수 내에서라면 쉽게 파악이 가능하겠지만, 다수의 함수가 연결된 형태에서 발생한다면 매우 심각한 문제에 빠질 수 있을 것이다.
 
프로젝트 리프는 피보탈 클라우드 파운드리 패밀리에서 PAS(Pivotal Application Service), PKS(Pivotal Container Service)와 함께 PFS(Pivotal Function Service)의 상용 OSS로 등장할 것이다. 오픈소스를 사용하고자 하는 기업은 리프를, 이와 같은 함수형 서비스를 사용하고자 하는 기업은 PFS를 검토해 볼 수 있을 것이다.
저작권자 © 아이티데일리 무단전재 및 재배포 금지