What is gRPC?

Intro

grpc를 이용하여 몇 개의 프로젝트를 했음에도 불구하고 아직 gRPC가 어떤 것인지 명확하게 머리 속에 자리잡지 않은 것 같아서 gRPC 개념에 대해서 정리해보려고한다.

gRPC의 개념에 대해서 한 문장으로 정리하자면 HTTP/2 기반의 RPC 프로토콜이라고 할 수 있다. 그래서 이 개념에 대해서 이해하기 위해서는 RPCHTTP/2 프로토콜 대해서 먼저 알아야한다.

RPC

RPC는 Remote Procedure Call의 약자로 의미 그대로 원격 컴퓨터나 프로세스에 존재하는 함수를 호출하는 프로토콜 이름이다. 기존 로컬 주소 공간에 존재하는 함수(procedure)를 호출하는 것을 확장하여 다른 주소 공간에 존재하는 함수(procedure)를 호출할 수 있는 프로토콜이다. 여기서 다른 주소 공간이라함은 같은 머신 내의 다른 프로세스를 말할 수도 있고 네트워크가 연결되어 있는 다른 머신의 프로세스를 말할 수도 있다. 그렇기 때문에 RPC로 클라이언트-서버 구조의 어플리케이션을 만들 수 있다

RPC는 일반적으로 request parameter과 response parameter를 알아야한다. 그렇기 때문에 미리 서버와 클라이언트 양쪽의 인터페이스를 맞춰야 하는데 Interface Definition Language라고 부르는 IDL로 함수명, 인자, 반환 값에 대해서 정의한 뒤 rpcgen 컴파일러를 이용하여 stub 코드를 자동 생성한다. gRPC는 자신만의 rpcgen 컴파일러가 존재하는데 이는 뒤에서 살펴보겠다.

이 때 생성되는 stub 코드는 개발자들에게 친숙한 go, java와 같은 원시소스코드 형태로 만들어지고 클라이언트, 서버 프로그램에 포함하여 빌드한다.

mechanism

gRPC mechanism

RPC 프로토콜은 다음과 같은 과정을 거쳐서 이뤄진다.

1. 클라이언트는 client stub procedure를 호출하는데 호출하는 입장에서는 평소 로컬 함수를 호출하듯이 함수 인자를 함수에 전달한다. client stub은 클라이언트 주소 공간에 존재한다.

2. client stub은 전달받은 인자들을 marshal(pack)하여 메세지를 만든다. marshal하는 과정에서 데이터형을 XDR(eXternal Data Representation) 형식으로 변환하여 바꾼 인자들을 메세지에 포함시킨다. 이렇게 XDR로 변환하는 이유는 integer, float과 같은 기본 데이터 타입에 대해서 머신마다 메모리 저장 방식(i.e. little endian, big endian)이 CPU 아키텍처 별로 다르며, 네트워크 전송 과정에서 바이트 전송 순서를 보장하기 위해서이다.

3. 이렇게 marshaling을 거친 메세지들은 transport layer에 전달하는데 transport layer은 리모트 서버 머신에 메세지를 전달한다.

4. 서버에서는 전달받은 메세지를 server stub에 전달하고 unmarshal(unpack) 과정을 거처서 클라이언트가 전송한 인자들을 얻어내고 클라이언트가 호출하려고하는 함수를 서버에서 찾아내어 서버의 함수로 실행시킨다. 실행 결과는 다시 server stub에 전달되고 클라이언트가 서버에 함수 인자를 전달하는 방식과 마찬가지로 결과 값이 클라이언트에 반환된다.

RPC라는 개념이 존재하기 전부터 소켓 프로그래밍을 통해서 네트워크에 연결되어있는 다른 서비스를 호출할 수는 있다. 그렇지만 네트워크를 타는 통신은 서버가 응답하지 않는 등의 장애가 발생할 수 있다. 이때 소켓 프로그래밍으로 구현했다면 이와같은 장애에 대해서 개발자가 직접 핸들링해야 한다.

RPC는 이러한 번거로움을 덜어준다. 네트워크 통신과 같은 번거로운 로직들을 감추고 추상화된 통신 인터페이스를 제공해준다. 또한 HTTP 기반의 REST가 유행하면서 RPC는 많이 사라졌는데, REST의 경우 request parameter와 response 값이 명시적이지 않기 때문에 오류가 생길 여지가 있고 JSON을 HTTP를 통해서 쏘기 때문에 속도가 비교적 떨어진다는 단점을 가지고 있다.

Protocol Buffers

gRPC는 protobuf를 IDL로 사용할 수 있다. Protobuf는 구글에서 만들고 현재 사용되고 있는 데이터 직렬화 라이브러리다. protobuf 파일의 예시는 아래와 같다.

IDL로 정의된 메세지를 기반으로 데이터 접근을 위한 코드를 생성하기 위해서 protoc 컴파일러를 사용한다. 간단한 명령어로 개발자가 원하는 프로그래밍 언어로 stub 코드를 생성할 수 있고 getter/setter 등 서버, 클라이언트 인터페이스가 자동 생성된다.

HTTP/2

HTTP/1은 기본적으로 클라이언트가 서버에 요청을 보내고, 서버가 요청에 대한 응답을 다시 보내는 구조이다. 그렇기 때문에 클라이언트가 서버에 요청을 할 때마다 커넥션이 맺어지고 서버를 왕복해야한다. 또한 헤더의 크기는 불필요하게 큰 편이다. 이와 같은 이유로 HTTP/1은 느리다.

구글에서는 이러한 문제점을 해결하기 위해서 SPDY를 만들었고 이를 기반으로 HTTP/2 표준이 만들어졌다.

SPDY

과거에 비해 오늘날에는 훨씬 더 많은 리소스를 주고 받고 있으며 보안이 훨씬 중요한 이슈가 되고 있다. 이렇게 바뀐 환경을 고려하여 구글에서는 느린 HTTP를 보완한 SPDY 프로토콜을 발표했다. SPDY 프로토콜은 latency 문제를 주로 해결하고자 하였다.

SPDY 프로토콜의 특징은 다음과 같은 것들이 있다.

항상 TLS(Transport Layer Security) 위에서 동작

따라서 HTTPS로 작성된 웹에서만 적용 가능하다.

Header Compression

HTTP 통신을 할 때 메세지에 포함되는 HTTP 헤더에는 요청마다 반복되는 내용들이 매우 많다. 그렇기 때문에 헤더 압축만으로도 리소스를 엄청 절약할 수 있다. 구글 발표 자료에 따르면 HTTP 헤더 압축을 할 경우, 최초 요청 시에는 압축을 안했을 때에 비해 10~30%를 줄일 수 있고 long-lived connection과 같이 여러 번 요청을 주고 받을 경우에는 80~97%까지 헤더 크기를 감소시킬 수 있다.

Binary protocol

HTTP/1.1에 존재하지 않았던 프레임이라는 새로운 단위가 생겼는데 이는 통신상의 제일 작은 정보 단위를 말한다. SPDY 프로토콜은 프레임을 텍스트가 아닌 바이너리로 구성하여 파싱이 더 빠르고 오류 발생 가능성이 낮다.

Multiplexing

HTTP에서는 하나의 커넥션에서 한 번에 하나의 요청을 처리하며 요청에 대한 응답이 순차적으로 이뤄졌다. 이와 달리 SPDY에서는 하나의 커넥션 안에서 여러 개의 독립적인 스트림을 동시에 처리한다. 그렇기 때문에 적은 수의 커넥션으로 다수의 요청, 응답을 동시에 처리할 수 있다. 또한 HTTP에서는 요청들이 FIFO로 처리되기 때문에 하나의 요청에 대한 응답이 지연되면 나머지 응답도 모두 늦어지는 HTTP pipelining과는 달리 SPDY에서는 각각의 요청, 응답이 모두 독립적으로 처리된다.

Full-duplex interleaving, stream priority

스트림이 진행 중일 때 다른 스트림이 끼어드는 것을 허용하고 스트림에 대해서 우선 순위 설정도 가능하기 때문에 우선 순위가 낮은 데이터를 전송 중일 때 우선 순위가 높은 데이터가 끼어들어서 더 빨리 전달할 수 있다.

Server push

Long-polling과는 다르게 클라이언트의 요청이 없어도 서버에서 컨텐츠를 직접 push할 수 있으며 리소스 캐싱이 가능하다. 하지만 server push를 구현하기 위해서는 어플리케이션 로직에 추가 구현이 필요하다.

Server push와 같은 경우를 제외하면 SPDY를 적용하기 위해서 추가적으로 작업해야할 필요는 없다. 한 가지, 브라우저와 서버가 SPDY를 지원해야한다. 그리고 spdy:// 와 같이 프로토콜 scheme이 없고 브라우저에서도 SPDY 프로토콜 사용 여부에 대해서 어떤 표시도 하지 않기 때문에 사용자는 굳이 SPDY 사용여부를 알 필요 없이 투명하게 적용 가능하다.

HTTP/2는 SPDY를 기반으로 만들어졌기 때문에 위에서 언급했던 SPDY의 특징을 가지고 있다.

gRPC

gRPC는 HTTP/2 특징을 기반으로 하는 RPC 프로토콜이다. 그렇기 때문에 양방향 스트리밍이 가능하고 HTTP보다 통신 속도가 빠르다. 그리고 RPC의 특징처럼 네트워크 통신에 대한 추상화를 제공해주어서 사용자가 네트워크 프로그래밍에 대해 신경을 쓰지 않게 되고 비즈니스 로직 개발에 집중할 수 있도록 도와준다.

그리고 과거에 gRPC의 단점 중 하나였던 gRPC에 대해 api-gateway 도입이 어렵다는 것이 있었는데 현재는 grpc-gateway 프로젝트를 통해 이는 극복한 것 같다.

필요한 코드의 양이 적고 여러 프로그래밍 언어를 지원한다는 장점이 있지만 프로토콜의 한계상 데이터 포맷 변환이 런타임에 자유롭지 못하다는 단점이 있다.

Reference

  • https://medium.com/@goinhacker/microservices-with-grpc-d504133d191d
  • https://www.tutorialspoint.com/remote-procedure-call-rpc
  • https://www.geeksforgeeks.org/operating-system-remote-procedure-call-rpc/
  • https://leejonggun.tistory.com/9
  • https://d2.naver.com/helloworld/140351
  • https://www.chromium.org/spdy/spdy-whitepaper/
  • https://grpc.io/docs/guides/
  • https://grpc.io/docs/guides/concepts/

About RSA Algorithm

Overview

RSA는 기본적으로 public key cryptosystem이다. 1977년 Rivest, Shamir 그리고 Adleman에 의해서 만들어졌다.

다른 public key cryptosystem과 마찬가지로 RSA도 trapdoor function에 크게 의존하고 있는데 RSA도 마찬가지로 trapdoor function 중의 하나인 integer factorization problem을 이용해서 안전성을 확보하고 있다.

trapdoor-function concept

trapdoor function은 간단히 말하면 위의 그림과 같이 x를 가지고 f(x)를 구하는 것은 쉽지만 f(x)를 알고 있을 때 x를 구하는 것은 어려운 함수를 말한다.

대표적인 예로 RSA에서 사용하고 있는 integer factorization problem을 예로 들 수 있다. integer factorization problem은 어떤 f(x1, x2, …, xn)의 값을 알고 있을 때 그 값을 이루는 x1, x2, … xn을 찾는 문제를 말한다.

이 때 x1, x2, … xn을 알고 있다면 f(x1, x2, … xn)의 값을 찾는 것은 쉽지만 반대로 f(x1, x2, … xn)을 알고 있을 때 x1, x2, … xn을 특정 주어진 시간 동안 찾는 것은 쉽지 않다. 이렇게 한쪽 방향으로 해를 구하는 것은 쉽지만 반대 방향으로 해를 구하는 것은 쉽지 않기 때문에 integer factorization problem을 trapdoor function이라고 볼 수 있다.

Background

relative prime

relative prime은 두 정수를 두고 불리는데 예를 들어 a와 b가 relative prime이라면 (a, b)=1임을 뜻한다.

Euler’s Φ(n) function

Fermat’s little theorem에 따르면 p를 양의 소수라고 할 때 p와 relative prime인 양의 정수 a에 대해서 다음과 같은 식이 성립한다.

복잡할 거 없이 mod 의 의미대로 위의 식을 읽어보면, ap-1은 p로 나눠떨어진다는 뜻을 담고 있다. 여기서 우리는 위의 식을 한 걸음 더 나아가서 Euler’s Φ(n) function으로 일반화시킬 수 있다.

식이 무지막지하다. 첫 번째 식과 비교했을 때 달라진 것은 a의 지수가 Φ(n)로 바뀌었다는 것인데, Φ(n)이 Euler’s Φ(n) function에서 핵심이다. Φ(n) function은 n보다 작거나 큰 양의 정수 n’ (0<n'<=n)에 대해서 n과 relative prime인 n’의 개수를 그 값으로 가진다.

예를 들어보자. Φ(5)에서 5보다 작은 양의 정수는 1, 2, 3, 4가 있는데 이들 중 5와 relative prime인 숫자의 개수는 4개이다. 따라서 Φ(5)=4이다. Φ(8)은 어떨까? 8보다 양의 정수는 {1, … ,7}이 존재하는데 이때 8과 relative prime인 숫자는 1, 3, 5, 7 총 4개이므로 Φ(8)=4이다. 마지막으로 Φ(7)에 대해서 생각해보면 7보다 작은 양의 정수는 {1…6}이 존재하고 이들 중 7과 relative prime은 {1…6} 전체이므로 Φ(7)=6이다.

이와 같이 계산을 하다보면 n이 소수인 경우는 Φ(n)=n-1인 규칙을 가지는데 생각해보면 당연하다. 왜냐하면 소수의 정의가 n보다 작은 정수 n’에 대해서 (n, n’)=1이기 때문이다. 우리는 이런 특징을 RSA 알고리즘에 사용할 것이다.

Modular Inverse

어떤 정수 a에 대해서 a와 어떤 수 x를 곱했을 때 1을 만드는 x를 우리는 a의 곱셈 역원(multiplicative inverse)이라고 부르고 보통 a-1로 나타낸다.

모듈러 연산에서도 곱셈 역원과 비슷한 개념이 있다. 어떤 정수 a와 m에 대해서 a와 어떤 수 x를 곱한 것에 대해 (mod m)한 결과가 1이 되는 x를 우리는 모듈러 역원(modular inverse)이라고 부르고 a-1로 나타낸다.


위의 수식을 보면 중요한 점을 하나 느낄 수 있는데 a와 m이 relative prime일 때만 a가 modular inverse를 가진다.

그렇다면 이제 궁금해지는 것은 a-1는 어떻게 구할 수 있을까? 한 가지 방법은 brute-force 방법을 사용할 수 있다. [0, m-1] 중에 정수를 하나씩 가져와서 모듈러 연산을 해볼 수 있을 것이다.

여기서 한 가지 주목해야할 부분은 brute-force 방법을 이용해서 a-1 구할 때의 시간 복잡도이다. [0, m-1]의 정수에 대해서 하나씩 가져오기 때문에 O(m)이라고 생각할 수 있다. 그런데 cryptosystem에서 다루는 input은 입력값 그 자체가 아니라 입력값을 바이트(or 비트)를 다룬다. 그렇기 때문에 전체 시간 복잡도는 O(2m)이 된다.

이와 같이 어떤 정수 a와 m이 있을 때 a와 m의 a에 대한 모듈러 역원을 구하기 위해서는 m의 지수적인 시간이 걸린다. 하지만 a와 a의 모듈러 역원이라고 생각하는 a’가 있다면 a’가 정말로 a의 모듈러 역원인지 확인하는 것은 어려운 문제가 아니다. 그래서 modular inverse를 구하는 것trapdoor function으로 볼 수 있다.

RSA Algorithm

이제 RSA 알고리즘에 대해서 살펴보려고 한다. 여기에서 RSA 암호화 방식이 어떤 방식으로 진행되는지 알아보고 각 단계에 대해서 짚고 넘어가보자.

  • 먼저 2개의 큰 소수 p, q를 생성한다. 소수를 생성하는 방법 중 하나인 Rabin-Miller 알고리즘을 사용할 수도 있다.
  • n=pq를 계산해야한다. 이 때 계산한 n은 나중에 public key와 private key의 modular 연산에 사용되고 n을 bit로 나타냈을 때 그 길이는 RSA 알고리즘의 키길이가 된다.
  • public key e 파라미터를 구해야하는데, 이 때 e는 (e, Φ(n))=1을 만족해야한다. Φ(n)는 Euler’s Φ(n) function에서 살펴보았듯이 Φ(n)=(p-1)(q-1)로 계산할 수 있다.
  • 다음으로 private key d 파라미터를 구하기 위해서는 public key e의 modular inverse를 구해야한다. 이를 수식으로 나타내면 다음과 같다.

이렇게 d, e를 구하게되면 사용자는 (e, n)을 public key로 사용하고 (d, n)을 private key로 사용하게 된다. 그렇다면 이를 이용해서 어떻게 평문을 암호화하고 복호화할 수 있을까?

먼저 평문을 바이트 배열로 바꾼 뒤 n보다 작은 바이트 블럭들로 나눈다. 그리고 이렇게 나눈 각 블럭들에 대해서

을 이용해서 평문을 암호화할 수 있고 반대로 복호화를 할 때는

을 이용해서 암호화된 메세지를 다시 복호화할 수 있다.

Example

그럼 이제 이해를 돕기위해 실제 숫자를 넣어서 RSA 알고리즘 과정을 따라가보자.

  • 먼저 두 개의 큰 소수 p, q를 생성해야하는데 이번 예시에서는 p=17, q=23이라고 해보자.
  • 다음으로 n=pq=17 * 23 = 391을 구하고 Φ(n)=(p-1)(q-1)=(17-1)(23-1)=352 를 구할 수 있다.
  • public key 파라미터 e를 구하기 위해서 (e, Φ(n))=1을 만족하는 e를 구해야하고 이를 계산하면 e=21이다.
  • private key 파라미터 d는 e의 modular inverse를 통해서 구할 수 있고 이를 계산하면 d=285이다.
  • 사용자는 public key/private key 파라미터 e, d를 이용해 public key와 private key를 구할 수 있고 각각 (e, n)=(21, 391), (d, n)=(285, 391) 로 계산할 수 있다.
  • 이렇게 구한 public key/private key를 이용해서 사용자가 ‘a’란 문자열을 암호화하고 싶다고 해보자. 우선 ‘a’의 ASCII 코드는 97이다. 이를 이용해서 ‘a’의 ciphertext를 구하고 다시 복호화하기 위해서는 다음과 같은 수식을 통해 계산할 수 있다.

Cracking

공격자가 RSA 알고리즘으로 암호화된 메세지를 복호화하려고 하는 경우에 대해 쓰고 이번 글을 마무리하려고 한다. 먼저 공격자는 e, n을 알고 있다. 왜냐하면 e, n은 퍼블릭한 정보기 때문이다. 이제 공격자의 목표는 private key인 (d, n)을 구하는 것이다.

공격자는 RSA 알고리즘이 어떻게 돌아가는지 알고 있고 그래서 d * e mod Φ(n) = 1을 통해 d를 구하고자한다. 그런데 d를 구하는 문제는 modular inverse를 문제이고 이를 푸는데 걸리는 시간은 위에서 설명했다시피 O(2Φ(n))이다. 따라서 Φ(n)이 크면 클수록 RSA 알고리즘이 안전하다는 것을 알 수 있다.

AES? — Step by Step 정리

이번 포스트에서는 AES 알고리즘이 어떤 방식으로 진행되는지 살펴보고 python AES 모듈을 이용해서 어떻게 메세지를 암호화하고 복호화 할 수 있는지에 대해서 알아보려고 합니다.

Background

AES의 등장 배경에는 DES가 있습니다. Data Encryption Standard(DES)를 간단하게 소개하자면 1970년 초 IBM에서 Horst Feistel의 디자인을 기초로 고안되었습니다. 그래서 DES는 Feistel-structure라고 불리기도 합니다. AES와 마찬가지로 대칭키 암호화 방식의 block cipher이고 56-bits 길이의 키를 가지고 있습니다. block cipher는 plaintext를 몇 개의 블럭으로 나누어서 암호화하는 방식을 말하는데 뒤에 가서 좀 더 설명하도록 하겠습니다.

그런데 지금은 DES가 중요한 정보를 암호화하는데는 적합하지 않다고 알려져있습니다. 1999년 초 DES Challenge III에서 DES로 암호화된 메세지가 brute force attack에 의해 해독되는데 22시간밖에 걸리지 않았습니다! 이렇게 DES가 안전하지 않은 주요한 원인 56-bits의 짧은 key size 때문입니다.

이와 같은 문제때문에 좀 더 안전한 cryptosystem이 필요했고, 2001년 Vincent Rijmen과 Joan Daemon에 의해서 AES가 만들어졌습니다. AES의 주요 강점은 DES보다 훨씬 긴 키 사이즈를 가진다는 것인데 AES는 128-bit, 192-bit, 256-bit 키를 가질 수 있고 이것은 DES의 56-bit key보다 기하급수적으로 더 강력합니다.

또한 AES는 DES보다 encryption 속도가 빠르기 때문에 낮은 레이턴시(latency)와 높은 처리율(thorughput)을 요구하는 소프트웨어 어플리케이션이나 펌웨어에 적합합니다. 그래서 다양한 프로토콜에 사용되고 있는데 SSL/TLS에도 사용되고 있고 방화벽이나 라우터 등에도 사용되고 있습니다.

Algorithm

AES algorithm overview
AES algorithm overview

AES 알고리즘의 큰 흐름은 위와 같습니다.

  • Add round key
  • Substitute bytes
  • Shift rows
  • Mix columns
  • Add round key

이제부터 각 단계에서 어떤 식으로 동작하는지 살펴보도록 하겠습니다.

Represent data as metrixes

block cipher represents data as metrixes
block cipher represents data as metrixes

먼저 알고리즘에 대해서 설명하기 전에 위에서 언급했던 block cipher에 대해서 짚고 넘어가려고 합니다. block cipher란 데이터 bit마다 알고리즘을 적용하는 것이 아니라 몇 개의 bit들을 묶어 하나의 block으로 만들고 block에 대해서 알고리즘과 키를 적용하는 cryptosystem을 말합니다.

AES도 block cipher로 평문, 암호문 그리고 키와 같은 데이터들을 위와같이 block들의 집합으로 나타냅니다. 한 블럭은 1byte (8bit)이고 총 16×8=128bit입니다. 키 길이가 128bit이기 때문에 평문 128bit 에 대해서 AES를 적용하게 됩니다. 그리고 p0, p1 … 순서를 확인하면 알 수 있겠지만 컬럼(column)별로 데이터를 나타내고 한 컬럼은 1word(32bits) 사이즈를 가집니다.

Add round key

add round key is XOR operation
add round key is XOR operation

먼저 AES를 진행하면 평문과 키를 가지고 add round key operation을 진행하게되는데 이는 XOR operation을 말합니다. 128bit 길이의 평문과 128bit의 키를 가지고 bit 별로 XOR operation을 진행합니다. 위의 다이어그램을 보면알 수 있지만 output 각각의 bit가 0 또는 1 될 확률은 50%입니다. 따라서 add round key operation을 진행하고 나면 평문값은 0과 1을 비슷한 비율로 가지게 됩니다.

Round function

전체 알고리즘 다이어그램에서 여러 개의 박스를 묶어서 Round function으로 나타낸 부분이 있습니다. DES와 마찬가지로 AES에서도 round라는 개념을 가지는데 묶어 놓은 알고리즘을 한 번 실행하는 것을 1 round가 진행된 것이라고 이해할 수 있습니다. 보통 128-bit key를 대상으로는 10 round 동안 알고리즘이 진행되고 192-bit key를 대상으로는 12 rounds, 256-bit keys를 대상으로는 14 rounds 동안 진행됩니다.

S-BOX: substitute bytes

S-BOX overview
S-BOX overview

s-box
https://en.wikipedia.org/wiki/Rijndael_S-box

다음은 S-BOX를 이용해 각 block의 bit들을 다른 bit들로 바꾸는 과정입니다. S-BOX는 간단히 말해서 lookup table이라고 생각할 수 있습니다. 해당 block의 bit들을 다른 것으로 바꾸는 과정은 다음과 같습니다. 위에서도 말했듯이 한 블럭엔 8-bit의 데이터가 담겨있습니다. 이 때 앞의 4bit를 row index로 보고 뒤의 4bit를 column index로 봐서 해당 S-BOX의 row, column의 값으로 바꾸게 됩니다.

shift rows

how shift rows work
how shift rows work

다음은 shift rows 단계입니다. 여기서는 각 row별로 circular left shift을 하는데 shift하는 step을 맨 윗줄 부터 0, 1, 2 그리고 3으로 잡고 위 그림과 같이 바꿉니다. 첫 번째 줄은 왼쪽으로 circular shift를 0만큼 하여 s0, s4, s8, s12이 원래 데이터였다면 shift rows를 마치고 나면 그대로 s0, s4, s8, s12가 되고 두 번째 줄은 왼쪽으로 circular shift를 1만큼 하여 s1, s5, s9, s13이 원래 데이터였다면 결과물은 s1자리에 s5가 오고 s5 자리에 s9가 오고 s9 자리에 s13가 오고 s13자리에 s1이 와서 s5, s9, s13, s1이 됩니다.

mix columns

mix columns 단계에서는 컬럼 별로 행렬곱(matrix-vector multiplication)을 수행합니다. 현재 matrix에서 컬럼을 가져와서 미리 정의된 circulant MD5 matrix에 곱하게됩니다.

mix columns
mix columns

보다시피 mix columns 단계에서는 bit 수준에서 덧셈과 곱셈을 할 수 있어야 합니다. 이 때 2와 3을 곱하는 연산을 해야하는데 우리는 여기서 2를 곱하는 것은 bit 수준에서 left shift로 생각할 수 있고 두 bit간의 덧셈은 두 bit의 XOR 연산으로 생각할 수 있습니다. 그렇다면 3을 곱하는 것은? 한 번의 left shift와 한 번의 XOR 연산의 조합으로 생각할 수 있습니다.

matrix-vector multiplication
matrix-vector multiplication

이와 같은 과정을 거쳐서 mix columns 단계를 마치게 됩니다. 이 때 주의해야할 점 중 하나는 mix columns 과정은 마지막 round에서는 시행하지 않습니다.

Add round key

이제 round의 마지막 단계인 add round key입니다. 이 때 첫 번째로 수행했던 add round key에서는 private key로 add round key를 수행했다면 이 때는 private key로부터 subkey를 만들어서 operation을 수행합니다.

subkey generation

subkey generation - 1
subkey generation – 1

1 — 먼저 위와 같이 이차원 배열로 나타난 private key를 가지고 있습니다. 위에서 말했듯이 한 block은 1byte를 가집니다.

subkey generation - 2
subkey generation – 2

2 — 먼저 가장 오른쪽에 있는 컬럼을 골라 컬럼 방향으로 circular upward shift를 수행합니다.

subkey generation - 3
subkey generation – 3

3 — 그리고 substitute bytes에서 사용했던 S-BOX를 이용하여 이전과 똑같은 방식으로 shift한 block byte들을 바꿉니다.

subkey generation - 4
subkey generation – 4

4 — 그리고 이전 단계에서 만든 컬럼의 값들과 Ki-4 컬럼 값들을 XOR 하고 미리 정의되어있는 rcon table에서 현재 round에 해당하는 값을 가져와 다시 XOR 연산을 하게됩니다. 그 결과 값이 다음 subkey의 첫번째 컬럼이 됩니다.

subkey generation - 5
subkey generation – 5

5 — 두 번째, 세 번째 그리고 마지막 column 값을 구하는 것은 간단합니다. Ki-1 컬럼과 Ki-4 컬럼을 XOR하면 끝입니다.

subkey generation - 6
subkey generation – 6

6 — 이와 같은 방식으로 128-bit subkey 생성을 마치고 나면 이제 만든 subkey로 add round key 과정을 진행합니다.

Example code

예제 코드는 python Crypto.Cipher 모듈에서 제공하는 AES를 이용하여 실제로 평문을 encrypt하고 decrypt하는 과정을 살펴보려고 합니다.

Import modules

Random 모듈은 우리가 간단하게 private key를 만들기 위해 사용할 것이고, binascii 는 나중에 우리가 encrypt한 데이터를 알아보기 쉬운 hexcode로 인코딩하기 위해서 사용할 것입니다.

Solve padding problems

이전에 언급했듯이 평문을 128-bit씩 나눠서 metrix를 만들고 AES 암호화를 진행합니다. 그런데 만약 평문이 128-bit 보다 작으면 어떻게 될까요? (더 큰 경우는 일단 없다고 가정하고 예제를 작성하였습니다) 이렇게 plaintext가 128-bit로 떨어지지 않는 문제가 있을 수 있기 때문에 128-bit로 나누어 떨어질 수 있게 padding value를 붙여줍니다.

그래서 위와 같이 append_space_paddingremove_space_padding 함수를 정의했습니다. append_space_padding 은 데이터를 encrypt 하기 전에 padding 값을 붙여주기 위해 필요하고 remove_space_padding 은 decrypt 했을 때 padding value를 없애주기 위해 정의했습니다.

어떤 것을 padding value로 할 지에는 몇 가지 옵션이 있습니다. 임의의 bit를 붙일 수도 있고 공백을 추가할 수도 있습니다. 그러나 이 예제에서 사용할 방법은 “a”라는 CMS(Cryptographic Message Syntax)를 정해서 필요한 padding 갯수만큼 추가하는 것입니다.

Encrypt, Decrypt

encrypt, decrypt 가 이번 예제의 비즈니스 로직이라고 할 수 있습니다. 이 함수에서 MODE_ECB 모드를 가진 새로운 인스턴스를 만들고 인스턴스의 메소드를 사용합니다.

ECB가 무엇인지는 이 포스트에서 자세하게 다루지 않습니다. 간단하게 ECB는 “Electronic Codebook”의 약자이고, ECB는 AES에서 데이터를 block으로 나누어서 알고리즘을 적용할 때 각 block에 대해서 독립적으로 알고리즘을 적용하는 방식을 말합니다. 이와 대조되는 모드 중 하나가 CBC 모드인데요. CBC 모드에서는 체이닝 메커니즘을 하용하여 각 block은 이전의 모든 block들에게 영향을 받아서 알고리즘이 진행됩니다.

Entry point

Results !

결과에서 우리는 paddedtext 의 길이가 a 문자가 붙어서 128로 나누어 떨어지는 숫자가 된 것을 볼 수 있습니다. 그리고 hexified ciphertext 는 AES 결과이고 마지막으로 decrypted text 에서 plaintext 와 동일한 Hello, AES! 문자를 출력하는 것을 확인할 수 있습니다.

Conclusion

지금까지 오늘날에도 다양한 곳에서 사용되고 있는 Advanced Encryption Standard(AES)에 대해서 살펴보았습니다. AES는 DES와 마찬가지로 block cipher이기 때문에 데이터를 block으로 나누어 encryption을 진행합니다. AES 알고리즘은 크게 다음과 같은 순서로 진행됩니다.

  • Add round key
  • Substitute bytes
  • Shift rows
  • Mix columns
  • Add round key

Reference

Hyperledger Fabric Add New Org to Network- eyfn 뜯어보기

이번 포스팅에서는 네트워크 확장과 관련된 eyfn 튜토리얼에 대해서 살펴보려고한다. 이전 byfn 튜토리얼 뜯어보기 포스팅에서는 최초로 organization들이 모여서 네트워크를 구성하는 것에 대해서 살펴보았다면 eyfn에서는 이미 구성된 네트워크에 새로운 organization을 추가하기 위해서 어떻게 하는지를 알려준다. 그래서 이번 포스팅에서는 Hyperledger Fabric 네트워크에서 어떻게 새로운 organization을 추가할 수 있는지를 위주로 작성해보려고 한다.

Generate Crypto Config

eyfn.sh 를 살펴보면 up 명령어를 사용해서 시작했을 때 엔트리 포인트가 networkUp 함수인 것을 알 수 있다. networkUp 에서 하는 일은 먼저 다음과 같다:

generateCerts

First-network 튜토리얼에서는 CA를 따로 사용하지 않으므로 crypto material을 생성하기 위해서 cryptogen 을 사용한다. 다음에 CA를 이용해서 어떻게 인증서를 발급받고 그것을 이용하는지 살펴보도록 하겠다.

org3-crypto.yaml에 명시되어있는 TemplateUser 만큼 인증서를 생성한다. generateCerts 를 마치고 나면 org3-artifacts 디렉토리에 crypto-config가 생긴다.

generateChannelArtifacts

이 함수에서는 configtxgen 툴을 사용하게되는데 기본적으로 configtxgen 은 Fabric 네트워크 channel config와 관련된 파일들을 생성할 수 있게 도와준다. 그리고 configtxgen 이 작동하기 위해선 사용자가 자신이 어떻게 config를 구성할 것인지 먼저 포맷에 맞게 yaml파일을 작성해야하는데 configtxgen 은 이 yaml 파일을 환경변수 FABRIC_CFG_PATH 에서 찾는다.

eyfn에서 org3과 관련된 네트워크 config yaml 파일이 org3-artifacts 디렉토리에 config.yaml로 있기 때문에 해당 경로에 맞게 환경변수를 설정해준다.

다음으로 configtxgen -printOrg 를 이용해서 config.yaml에 Org3MSP에 해당하는 organization의 설정을 org3.json으로 쓴다. 나중에 이 org3.json 을 이용해서 기존에 존재하던 mychannel의 config를 수정하고 org3이 mychannel에 들어갈 수 있도록 할 것이다.

마지막으로 orderer의 MSP를 org3의 crypto crypto-config 로 복사를 하는데 이는 나중에 org3에서 ordering service를 이용하기 위해서이다.

Create Updated ConfigTx

이제 네트워크 밖에서 할 수 있는 사전작업은 모두 마쳤다. 다음으로 해야할 일은 mychannel의 config를 수정하고 mychannel에 속해있는 organization들에게 새로운 organization인 org3이 들어올 수 있도록 허가를 받아야한다. Hyperledger Fabric이 private chain이고 PoA이기 때문에 이와 같이 채널의 기존 그룹으로부터 허가를 받아야 들어올 수 있다.

그렇기 때문에 이제 위의 명령어처럼 도커로 직접 들어가서 작업하게된다. (여기서 조금 악랄한 부분이있다.. 좀 쉽게 만들 수 있었을 거 같은데..)

step1org3.sh에서 위와 같이 먼저 필요한 모듈들 임포트하고 jq 패키지를 설치한다.

fetchChannelConfig

먼저 mychannel 채널의 config를 수정하기 위해서 채널의 config block을 가져와서 그 중에서도 config를 담고 있는 부분만 가져온다.

채널의 configuration block을 가져오는 부분이다. 현재 우리는 cli 컨테이너에 들어와있고 네트워크에 떠있는 orderer MSP로 동작하기 위해서 setOrdererGlobals 를 사용했다. 그리고 peer channel을 통해서 mychannel의 config block을 가져온다. 그리고 tls를 사용한다면 위와 같이 orderer의 tls certification도 같이 제출해야한다.

위의 명령을 실행시키고 현재 디렉토리를 확인해보면 config_block.pb 의 이름으로 config block이 존재하는 것을 확인할 수 있다.

configtxlator는 기본적으로 json또는 protobuf 형식으로 되어있는 fabric transaction을 서로 반대의 형식으로 변환시켜준다. 또는 서로 다른 config transaction의 델타를 구하는데에도 사용된다. 이 두 번째 기능은 좀 더 나중에 사용된다.

지금은 먼저 configtxlator를 이용해서 protobuf 형태로 되어있는 config block을 JSON 형태로 바꾼다. 그리고 JSON 파싱을 도와주는 jq를 이용해서 config 값을 담고 있는 부분만 파싱해서 ${OUTPUT}에 저장한다.

위의 명령을 실행시키고 현재 디렉토리를 확인해보면 config.json 이 생성된 것을 확인할 수 있는데 vim이나 more을 통해서 확인해보면 org1, org2의 설정이 담겨있는 것을 확인할 수 있다.

Append the new organization to current configuration

이전 단계에서 현재 채널의 configuration을 가져왔으니 이제 generateChannelArtifacts 단계에서 만든 org3 configuration 파일인 org3.json 을 여기에 추가해야한다.

이 단계에서도 이전에 사용한 jq를 사용해서 JSON 파일을 수정하게된다. 위의 jq 명령어를 간단히 설명하면 우선 jq -s '<JSON_FILE_1> * <JSON_FILE_2>' 은 두 개의 JSON 파일의 합집합을 만들게 된다. 그래서 두 개의 JSON 파일을 비교해서 서로에게 없는 부분은 추가하게된다. 여기서 <JSON_FILE_1>.[0] 이 되고 <JSON_FILE_2>{"channel_group":{"groups":{"Application":{"groups": {"Org3MSP":.[1]}}}}} 이 된다. 그리고 [0], [1] 은 각각 config.json./channel-artifacts/org3.json 으로 치환된다.

그래서 결과적으로 기존의 configuration에 channel_group.groups.Application.groups.Org3MSP 를 키로 가지고 org3.json 을 벨류로 가지는 필드를 추가하게된다.

createConfigUpdate

여기까지 우리가 생성한 파일을 정리해보면 다음과 같다.

  • config_block.pb: 현재 채널의 configuration block

  • config.json: 현재 채널의 configuration block에서 추출한 channel configuration tx

  • modified_config.json: org3 organization의 정의를 추가한 channel configuration tx

이제 createConfigUpdate 에서 하는 일은 (1) 먼저 config.jsonmodified_config.json 의 차이를 가지는 JSON 형태의 transaction을 만들고 (2) 두 번째로는 이 transaction을 ordering service에 제출하기 위해서 envelope 형태로 만들게 된다. 이 다음 단계에서 우리는 현재 mychannel에 속해있는 organization들에게 서명을 받게된다.

config.jsonmodified_config.json 의 차이를 가지는 transaction을 만들기 위해서 먼저 각각의 JSON 파일들을 protobuf 형태로 바꾼다. 그리고 각각은 original_config.pb, modified_config.pb 의 이름을 가지게된다.

위의 스크립트는 이전에 말한 과정을 풀어낸 것이다. 먼저 org3을 추가하기 이전과 추가한 후의 차이를 가지는 transaction을 configtxlator compute_update 를 통해서 구한 뒤 configtxlator proto_decode를 통해서 JSON 형식으로 바꾼다. 다음으로 transaction을 ordering service에 제출하기 위해서 envelope 형태로 바꾸고 configtxlator proto_encode 를 통해서 protobuf 형식으로 바꾼다. 위 스크립트를 실행시키면 현재 디렉토리에 결과물인 org3_update_in_envelope.pb가 생성된다.

이제 남은 과정은 org3_update_in_envelope.pb 에 mychannel에 속한 organization들에게 서명을 받은 뒤 ordering service에 제출하면 새로운 org3가 mychannel에 들어올 준비가 끝난다.

Signing to config transaction

setGlobals 를 통해서 MSP를 바꾼다. 같은 cli 컨테이너 안이지만 MSP를 바꿈으로써 다른 노드(peer, orderer)의 역할을 할 수 있다. 먼저 setGlobals 0 1 을 통해서 org1이 이전 단계에서 만든 envelope에 peer channel signconfigtx 를 통해서 서명을 한다. 그리고 setGlobals 0 2 를 통해서 org2 MSP로 바꾸고 org1이 서명한 envelope을 ordering service에 제출하게 된다. 이 때 tls를 사용한다면 위의 명령어처럼 orderer의 ca certificate도 --cafile 옵션에 넣어서 같이 제출해야한다.

유효한 envelope을 만들었다면 ordering service에 envelope을 제출했을 때 위와 같이 성공했다는 메세지가 나온다.

Join Org3’s peers to network

이제 eyfn의 핵심적인 내용은 거의 마무리되었다. 위의 과정이 엄청 복잡하지만 잘 생각해보면 결국에는 Org3의 정의를 담은 transaction을 만들고 orderer에게 제출하기 위한 envelope을 만들었다. 그래서 이제 mychannel 채널 configuration에 Org3이 등록되었기 때문에 org3이 직접 mychannel에 join 요청을 보낼 수 있다.

그래서 이제 Org3cli 컨테이너로 들어간다. Hyperledger Fabric에서 채널로 들어가기 위해서는 해당 채널의 genesis block을 이용해야하다.

그래서 위와 같이 mychannel의 genesis block을 가져오기 위해서 orderer에게 요청을 한다. 이 때도 마찬가지로 tls를 사용한다면 orderer의 ca certificate도 같이 제출하여야한다. 위의 명령어를 실행하면 현재 디렉토리에 mychannel.block 이 생성된다. 이것이 mychannel의 genesis block이다.

이제 setGlobals 로 MSP를 바꿔가면서 peer channel join 명령어를 실행시킨다. 그리고 실제로 피어들이 추가됐는지 확인하기 위해서 다음과 같은 방법을 쓸 수 있다.

위와 같이 setGlobals로 확인하고 싶은 피어의 MSP를 설정한 뒤 peer channel list 로 현재 피어가 들어가 있는 채널을 확인해볼 수 있다.

Conclusion

이번 포스팅에서는 어떻게하면 Hyperledger Fabric 네트워크에 새로운 organization을 추가할 수 있는지에 대해서 eyfn 튜토리얼을 통해서 살펴보았다. Organization을 추가하기 위한 config transaction을 만드는 부분이 조금 복잡했다. 첫 번째로 기존의 channel config transaction에서 새로운 organization의 config를 추가했다. 두 번째로는 이전 것과 새로운 것의 델타를 이용해 config envelope을 만들었다. 마지막으로는 현재 채널에 존재하고 있는 organization들에게 서명을 받은 뒤 ordering service에 제출하였다. Hyperledger Fabric은 이와같이 프라이빗 체인이고 PoA이기 때문에 기존의 네트워크에서 허가를 해주어야 새로운 노드가 참여할 수 있다.

다음 포스팅에서는 생각해둔 Hyperledger Fabric와 관련된 튜토리얼이 있는데 그것에 대해서 작성해볼 예정이다.

Filecoin: A Decentralized Storage Network Protocol Overview

최근 SWIM에 발을 들이면서 같이 틈틈히 공부하고 있는 Decentralized Storage Network이다. 개인적으로 생각하기에 현재 작업 중에 있는 SWIM 네트워크 프로젝트를 마무리하고 이 위에 올렸을 때 효용성이 있는 서비스들 중에 하나가 아닐까하고 생각하고 있다. 무엇보다 재미있어보인다. 현재는 작업 시간 중 7-8할을 SWIM에 쏟고 있지만 이 프로젝트가 얼추 마무리되면 좀 더 관심을 가지고 개인적으로든 마음 맞는 사람이든 같이 구현해서 올려보는 것이 목표이다.

Introduction

IPFS와 같이 분산 시스템으로 데이터 콘텐츠를 처리하는 것의 유용성은 수천만개의 파일들을 p2p 네트워크로 서빙하면서 입증해왔다.

Filecoin은 클라우드 저장소를 알고리즘 시장으로 바꾼 분산 저장소 네트워크(Decentralized Storage Network)이다. 이 시장은 Filecoin이라 불리는 토큰의 블록체인 위에서 돌아간다. 마이너들은 클라이언트들에게 저장소를 제공함으로써 토큰을 채굴하게 된다. 반대로 클라이언트들은 파일과 데이터들을 분산 저장 하기 위해 마이너들을 고용하고 토큰을 대가로 주게된다. 비트코인과 마찬가지로 Filecoin 마이너들은 블럭을 채굴하기위해 다른 마이너들과 경쟁하게 되는데 이때 채굴 파워는 마이너들 각자가 가지고 있는 현재 활용되고 있는 저장소에 비례하게 된다. 그렇기 때문에 마이너들은 자연스럽게 클라이언트들에게 더 좋은 서비스를 제공하는 동기를 부여한다. 이러한 프로토콜은 막대한 양의 자원들이 유실되거나 파괴되더라도 다시 원 상태로 복구 되는 self-healing 저장소를 만들게 된다. 또한 네트워크는 분산된 데이터들을 복제함으로써 그 견고함을 유지하게되고 데이터 암호화를 하면서 end-to-end 보안을 하게 된다.

Filecoin은 Proof-of-Spacetime을 기반으로한 프로토콜 토큰이다 그리고 블럭들은 데이터들을 저장하고 있는 마이너들에 의해서 생성된다. Filecoin 프로토콜은 데이터 저장소를 제공하고 저장소에서 파일을 다운 받을 수 있는 서비스를 제공하는데 이는 한 명의 중재자를 통해서 제공되는 것이 아니라 데이터들을 저장하고 있는 여러 명의 독립적인 저장소 제공자들에 의해서 제공된다.

  1. 클라이언드들은 데이터를 저장하거나 다운 받기위해서 토큰을 지불해야한다.
  2. 저장소 마이너들은 클라이언트들에게 저장소를 제공함으로써 토큰을 얻게 된다.
  3. 데이터회수 마이너들은 클라이언트들에게 파일을 제공함으로써 토큰을 얻게 된다.

Elementary Components

Filecoin 프로토콜은 크게 다음과 같은 네 가지 컴포넌트들로 구성된다

  1. Decentralized Storage Network (DSN): Filecoin은 저장소 제공자에게 저장소를 제공하고 저장소에서 파일을 제공할 수 있게 DSN이라는 추상화된 네트워크를 제공한다.

  2. Proofs-of-Storage: Filecoin은 두 가지의 새로운 Proofs-of-Storage 자격증명 방식을 제시한다.

    1. Proof-of-Replication: Proof-of-Replication은 데이터 저장소 제공자들이 각자 자신이 가지고 있는 고유 물리적 저장소에 데이터를 저장 및 복사했다는 것을 증명할 수 있게한다. Proof-of-Replication은 확인자(verifier)가 증명자(prover)의 저장소에 데이터들을 복사해놓았는지를 확인한다.
    2. Proof-of-Spacetime: Proof-of-Spacetime은 데이터 저장소 제공자들이 특정 지정된 시간동안 특정 데이터를 저장해놓았는지를 증명하게 한다.
  3. Verifiable Markets: Filecoin은 데이터 저장 요청과 데이터 회수 요청을 두개의 decentralized verifiable market들의 주문(order)으로 모델링하였다. verifiable market은 데이터 저장 혹은 회수 서비스가 제대로 행해졌을 때에만 그 대가를 지불할 수 있게 한다. 그래서 두 개의 Verifiable Market(데이터 저장 요청을 처리하는 Storage Market과 데이터 회수 요청을 처리하는 Retrieval Market)에서 클라이언트와 마이너들은 각각 데이터 저장 요청을 보내고 데이터 회수 요청을 보낼 수 있다.

  4. Proof-of-Work: Filecoin은 비트코인과 다르게 Proof-of-Spacetime을 기반으로한 Proof-of-Work를 제공한다. 마이너들은 쓸데없는 계산파워를 사용하지 않고 데이터를 저장소에 저장하는 것으로 그것을 대신할 수 있다.

Protocol Overview

  • Filecoin 프로토콜은 분산 저장소 네트워크를 만들기 위한 프로토콜이다. Filecoin 프로토콜은 Filecoin 토큰을 기반으로한 블록체인 위에 만들어진다. 클라이언트들은 데이터를 저장하고 회수하기위해 토큰을 사용하고 마이너들은 데이터 저장과 회수 서비스를 제공하면서 토큰을 받게 된다.
  • Filecoin DSN(Decentralized Storage Network)는 데이터 저장과 데이터 회수 요청을 각각 두 개의 Verifiable market이란 개념을 통해 핸들링하게된다. 데이터 저장 서비스는 Storage Market에서 이루어지게되고 데이터 회수 서비스는 Retrieval Market에서 행해진다. 클라이언트들과 마이너들은 각각의 서비스에 대해서 가격을 매겨놓는다. 클라이언트의 경우는 자신이 이 서비스를 수행하기 위해서 얼마의 토큰을 지불할지를 명시해놓고 마이너의 경우는 서비스를 제공하기 위해서 그 대가로 얼마의 토큰을 받을지를 명시한다. 그리고 이 가격들과 요청은 각각 Verifiable Market으로 들어가게 된다.
  • Verifiable Market은 Filecoin DSN 위에서 동작하는데 내부적으로는 Proof-of-Spacetime과 Proof-of-Replication에 의해서 동작한다. 이 두가지 자격증명은 마이너들이 자기가 저장하겠다고 한 데이터를 제대로 저장하였는지를 확인한다.
  • 마이너들은 블록을 만들기 위해서 참여하는데 다음 블럭을 만들기 위한 마이너들의 영향력은 현재 각 마이너들의 저장소가 전체 네트워크에서 얼마나 사용되고 있는지에 비례하게 된다.

Filecoin Protocol Sketch

다음은 Filecoin 프로토콜이 전체적으로 어떻게 작동되는지를 묘사하기 위한 스케치이다.

filecoin logic flow overview
Filecoin Protocol을 다이어그램으로 나타난 것이다. 특히 클라이언트와 마이너의 상호작용을 나타내었다. Storage Market과 Retrieval Market은 각각 블록체인 아래에 나타내었고 왼쪽에서 오른쪽으로 시간이 흘러가는데 왼쪽은 Order Matching phase이고 오른쪽은 Settlement phase이다. 주목해야할 점은 데이터 회수의 대가로 micropayment를 하기전에 클라이언트는 반드시 자신의 펀드에 lock을 걸어야한다.

Network

네트워크에서 시간 t초동안 ledger안에서 다음과 같은 일이 일어난다.

  1. 새로운 블럭에 대해서는:

    1. 블럭이 제대로 된 포맷인지 확인한다.
    2. 블럭안의 트랜젝션이 유효한 것인지 확인한다
    3. 트랜젝션의 주문이 모두 유효한 것인지 확인한다.
    4. 자격증명이 유효한지 확인한다.
    5. 만약 위의 조건들 중 하나라도 만족하지 못하면 블럭을 버린다.
  2. t초 동안 새로운 주문 O에 대해서:

    1. O를 Storage Market의 오더북에 추가한다
    2. if O is bid: lock O.funds
    3. if O is ask: lock O.space
    4. if O is deal: run Put.AssignOrders
  3. Storage Market의 오더북에 있는 각각의 O에 대하여:

    1. check if O has expired (or canceled):

      1. remove O from the orderbook
      2. return unspent O.funds
      3. free O.space from AllocTable
    2. if O is a deal, check if the expected proofs exist by running Manage.RepairOrders

      1. if one missing, penalize the M(miner)’s pledge collateral(담보)
      2. if proofs are missing for more than delta fault, cancel order and re-introduce it to the market
      3. if the piece cannot be retrieved and re-constructed from the network, cancel order and re-fund the client

Client

client는 매순간 다음과 같은 일을 할 수 있다:

  1. 파일 저장요청을 Put.AddOrders를 통해 제출한다.

    1. find matching orders via Put.MatchOrders
    2. send file to the matched miner M
  2. 파일 회수요청을 Get.AddOrders를 통해 제출한다.

    1. find matching orders via Put.MatchOrders
    2. create a payment channel with M

저장소 마이너 M으로부터 deal을 하겠다는 주문 O를 받으면:

  1. deal 주문 O에 사인한다.
  2. 사인한 deal O를 Put.AddOrders를 통해서 블록체인에 제출한다.

데이터 p를 회수 마이너 M으로부터 받으면:

  1. p가 유효한 데이터인지 확인하고 클라이언트가 요청한 데이터가 맞는지 확인한다.
  2. 대가를 M에게 지불한다.

Storage Mine

마이너들은 매순간 다음과 같은 일을 할 수 있다.:

  1. 만기가 된 pledge들을 Manage.PledgeSector를 통해 갱신한다.
  2. 새 저장 공간을 Manage.PledgeSector를 통해서 담보를 잡는다.
  3. Put.AddOrder를 통해서 새 ask order를 제출한다.

매 시간 t 마다:

  1. 오더북에 있는 각각의 Oask에 대해서:

    1. Put.MatchOrders를 통해 매치되는 오더를 찾는다
    2. 매칭된 오더를 찾으면 해당 클라이언트와 컨택을해서 딜을 시작한다.
  2. 각각의 데이터들에 대해서:

    1. Manage.ProveSector를 통해서 해당 저장공간에 대해 자격증명 생성한다.
    2. delta proof epochs 시간 동안 자격증명시간이 끝나면 생성한 자격증명을 블록체인에 제출한다.

데이터 p를 클라이언트 C로부터 받으면:

  1. 데이터 p가 bid 오더에 명시되어있는 파일 사이즈가 맞는지 확인한다.
  2. 마이너는 deal 오더를 생성 및 사인을하고 서명을 클라이언트에게 보낸다
  3. 데이터를 저장 공간 섹터에 저장한다.
  4. if sector is full, run Manage.SealSector

Retrieval Mine

매 순간마다 다음과 같은 일을 한다:

  1. 가십으로 ask 오더를 네트워크에 전파한다.
  2. 네트워크의 bid 오더를 리슨하고 있는다.

클라이언트 C로부터 회수 요청이 오면:

  1. C와 payment channel을 시작한다.
  2. 데이터를 여러 파트로 나눈다.
  3. 페이가 올 때마다 데이터 파트를 보내준다.

Reference

  • Protocol Labs, Filecoin: A Decentralized Storage Network, 2017

How DDD Concept can be applied to Project

I’ve translated blog post (korean version) which is about 7 important concepts which is about DDD(Domain-Driven Design).

  • Ubiquitous language
  • Layers
  • Bounded contexts
  • Anti-Corruption Layer
  • Shared Kernal
  • Generic subdomain

After translating post I’ve done projects which apply DDD concepts. One is blockchain engine project, it-chain. You can see the codes on this link

In this post, I’m going to show how DDD concepts can be applied to real-world project codes and code snippets which will be shown in this post are based on it-chain project I’ve introduced above.

Ubiquitous language

Ubiquitous language is a language that define terms which are matching business requirements on Application and technology for implementing it. For example, blockchain component in it-chain project needs to manage blocks (save blocks, create blocks…). Managing blocks is business requirements for blockchain component. But the problem is the block is not all the same, there can be several different states on each block. For example, there can be the block which is just created with transactions, this block is not saved to repository and even not concensused with members of network. This ‘just created’ block should not be treated as block which is consensused successfully by members and saved to repository.

So the developers who are work on blockchain component think that they need to define terms about block states: ‘Created’, ‘Staged’, ‘Commited’. Then why did they defined those terms? One problem when developers work on big project, they cannot understand the codes developed by others easily. Even worse, one developer may misunderstand the code! This problem will getting worse as code base grow.

By defining common language(Ubiquitous language) before work on codes or before develop some features, the other developers can understand what this code is doing now and why this code is placed here. it-chain developers defined term ‘Created’, ‘Stateged’, ‘Commited’ right after they think they need to distinguish block states. Next time as new contributor came in blockchain component and looked at CreatedBlock in method. He/She can easily figure out ‘Oh, this method is about just created block which is not consensused and saved to repository’. Because we defined common language about block states.

Layers

Layering concept is used in other designs, but I imported several layers identified by DDD:

  • User Interface

    User interface responsible for draw screen which makes user to interface with Application, convert user’s input into Application commands. Important point here is that user in User Interface is not human. But Application also can be user if that Application use the other Application’s API.

    In it-chain project, we can see User Interface layer in cmd package.

    This code is about joining peer node to existing network. You can see it just receive user’s input (node ip addresss) and call rpc client.Call for joining into network. We cannot see application service logic, it just receives and then pass.

  • Application Layer

    Application Layer orchestrate domain objects for carrying out Application business. So Application Layer should not contain business logic.

    This code is about txpool component CreateTransaction API function. As function name imply it just create transaction with tx data and save it to repository. And there’s no business logic, TransactionApi use TransactionRepository, and domain’s function CreateTransaction. Application Service is abstracted with domain object. All the detailed (business logic) encapsulated inside domain object, function.

  • Domain Layer

    As DDD(‘Domain driven development’) name says domain layer is key part of Application. Domain object such as service, repository, factory, model contains all the business logic.

    This code is about txpool domain layer Transaction, CreateTransaction. We’ve seen CreateTransaction domain function is used inside Application Layer (TransactionApi). CreateTransaction says how to create transaction with txData. We can see all the detail business logic is encapsulated into Domain layer.

  • Infrastructure

    Infrastructure layer contains the techinical capabilities which support the layers above.

    This code is about blockchain component infrastructure layer. One easy example of infrastructure layer is database. BlockRepository have database library yggdrasill which helps save data to level-db. In Save function you can see br.BlockStorage.AddBlock(&block) which works as wrapper for external library and helps to carrying out blockchain component Application business.

Bounded Contexts

As Application’s domain grow, there can be more developers who work on the same code base. But this situation have problems. As developers who work on same code base grow, the codes each developer should understand larger and understanding may can be hard. And this increases the possibility of bugs or errors. Furthermore, as code base developers work on grow, managing each developer’s work can be hard.

One way to solve these problems is separating one huge code base and “bounded context” help this. Bounded context is context which can separate domains based on its own concern.

it-chain logical architecture
it-chain logical architecture

it-chain project separate whole domain into several bounded contexts based on its own concern. and we called each bounded context as “Component”. it-chain has following components:

  • Client Gateway

    Client gateway provides REST API for client application of it-chain

  • gRPC Gateway

    gRPC gateway is service for communication for nodes of network. Communication needs for blockchain synchronize, consensus etc.

  • TxPool

    TxPool temporarily save transactions which are not saved into block

  • Consensus

    Consensus component is for consensus of block, currently Consensus component provides PBFT algorithm

  • Blockchain

    Blockchain component helps to create, save block and synchronize blockchain

  • IVM

    IVM component manage it-chain’s smart contract called iCode

Anti-Corruption Layer

Anti-corruption layer basically work as middleware between two different bounded context. So instead of each bounded context communicate directly, they use anti-corruption layer. Then why anti-corruption layer?

If two different component communicate directly without anti-corruption layer, one change in a component can affect the others. And this can be a disaster as project grows, a small fix in a component can break the whole system. What if we use anti-corruption layer and each component communicates with anti-corruption layer? A change in a component only affects anti-corruption layer, and if we communicate with anti-corruption layer with its interface, there may no affects on the other system except its own component!

it-chain logical architecture
it-chain logical architecture

In it-chain project each component only communicates with RabbitMQ Interface. And this helps developers make only cares about its own component. Nothing outside my component is my concern.

This code is about sending transactions from TxPool component to EventService. Instead of directly send transactions to Blockchain component, txpool publish ProposeBlockEvent to message queue. So both txpool and blockchain don’t need to care about other side, just publish and subscribe.

Shared Kernel

However we separate whole system into bounded contexts, sometimes it is much more resonable to share some domain objects. With shared kernel each component strongly coupled with shared kernel but still make decoupled with the other components.

In it-chain project shared kernel is located in common package. One big decision we made recently is that place event and command which are used to communicate different components into shared kernel (common package) and every event, command must use primitive type.

  • before

  • After

The reasons are as followed:

  • Before event and command is located inside each component, the other component should reference other component’s event and command type for communication with other component. And it looks like we broke up the bounded context.
  • Before event and command using primitive type, they use its own domain type inside. And this feel we completely broke up bounded contexts, for example, if blockchain component wants to communicate with txpool component blockchain component should know about txpool domain type because blockchain should make txpool‘s event or command type.

Conclusion

In this post, we’ve looked at how DDD key concepts can be applied to real-world projects. With these examples, I hope you can get hint what is DDD.

Hyperledger Fabric: Transaction Flow

이번 포스팅에서는 Hyperledger Fabric의 transaction이 어떻게 생성되고 ledger에 최종적으로 commit되는지 이해가 잘 되도록 예시를 들어 설명되어 있는 글을 Hyperledger Fabric doc에서 발견해서 번역해보았다. 원본 링크: https://hyperledger-fabric.readthedocs.io/en/release-1.2/txflow.html

Scenario

상품 거래를 하는 시나리오를 가지고 어떻게 transaction이 발생하고 ledger에 commit되는지 그 과정에 대해서 설명하려고 한다. 예시로 사용할 시나리오에서는 A, B라는 두 명의 client가 있고 각각은 당근을 사고 팔려고 한다. client A, B 각각 자신의 네트워크에 피어를 가지고 있고 그 네트워크를 통해 각각의 transaction을 보내고 ledger에 기록한다.

Assumptions

이 시나리오에서는 channel이 세팅되어있다고 가정한다. 그리고 어플리케이션의 user들은 각 organization의 CA에 register & enroll하고 cryptomaterial materal을 생성했다. 그리고 user들은 이것을 이용해서 network authentication에 사용한다.

각 피어들에게 chaincode (당근 시장의 초기 상태를 나타내는 key & value pair들이 포함되어있다.)가 installed되었고 channel에 instantiated되었다. chaincode에는 transaction instructions set이 정의되어있다. Endorsement policy 또한 이 chaincode에 세팅되어 있고 이 때 policy는 모든 transaction에 대해서 피어 A, B 모두 endorse해야한다고 정의된다.

1. Client A initiates a transaction

먼저 구매자인 client A가 당근을 구매하겠다는 request를 보낸다. 이 때 이 request는 peerApeerB를 타겟으로 삼는다. 왜나하면 channel이 만들어질 때 정의된 endorsement policy에 따르면 모든 피어들이 transaction에 대해서 endorse 해야하기 때문이다.

그 다음, transaction proposal이 생성된다. SDK를 사용하고있는 어플리케이션에서는 SDK API 함수를 이용해서 transaction proposal을 생성한다. 이 proposal은 chaincode를 invoke 해달라는 요청인데, 결과적으로 이 proposal을 이용해서 ledger를 읽거나 쓸 수 있다. 여기서 사용하는 SDK는 transaction proposal을 gRPC를 통해 전송될 수 있는 포맷으로 패키징하는 역할을 해준다. 그리고 이 패키징에 user의 cryptographic credential을 이용해서 이 transaction proposal의 유일한 signature를 만들게 된다.

2. Endorsing peers verify signature & execute the transaction

그 다음 단계로 endorsing peer들은 이전 단계에서 만들어진 transaction proposal을 받게 되는데 다음과 같은 사항들을 확인한다.: (1)첫 번째로 이전 단계에서 만들어진 transaction proposal들이 잘 만들어졌는지 확인하고 그리고 (2)똑같은 transaction proposal이 이전에 요청되었는지도 확인한다. (replay-attack protection) (3) 세 번째로 signature가 유효한지 MSP를 이용해서 확인하고 (4) 마지막으로 요청을 전송한 submitter(현재 시나리오에서는 Client A이다.)가 현재 channel에 proposed operation을 할 수 있는 권한을 가지고 있는지 확인한다. 다시 말하면 submitter가 현재 channel의 Writers policy를 충족하는지 확인한다.

endorsing peer들은 transaction에서 invoke 되고 있는 chaincode의 함수 인자를 가지고 다시 chaincode를 현재 데이터베이스 상태에 대해서 실행시킨다. 그리고 그것들을 실행시킨 결과로 response value, read set 그리고 write set을 만들게 된다. 그런데 중요한 점은 chaincode의 함수를 실행시켜본 것이지 그 결과를 이용해서 ledger를 업데이트 시킨 것은 아니다. 아까 말한 세 가지 결과와 endorsing peer의 signature를 합쳐서 proposal response를 만들고 이것을 다시 SDK에 전송하게 된다. 어플리케이션에서는 payload를 파싱해서 사용하면 된다.

MSP

MSP는 client로부터 전달되는 transaction request들을 피어들로 하여금 검증할 수 있게 해주고 transaction를 검증한 결과에 대해서 sign 할 수 있게 해주는(endorsement) peer component이다. 이 때 writing policy는 channel이 생성될 때 정의되고 어떤 user들이 channel에 transaction을 제출할 수 있는지 명시한다.

3. Proposal responses are inspected

어플리케이션에서는 endorsing peer의 signature들을 검증하고 proposal response들을 가지고 서로 같은지 비교한다. 이 때 chaincode가 ledger를 조회하는 작업만 했다면 어플리케이션에서는 조회 결과만 확인하고 transaction을 Ordering Service에 제출하지 않는다.

만약 client 어플리케이션이 transaction을 Ordering Service에 ledger를 업데이트 시키기 위해 제출하려면 endorsement policy가 충족되었는지(peerApeerB 모두 endorse해야한다.)를 먼저 확인해야한다. 만약 어플리케이션 단에서 response를 확인하지 않고 unendorsed한 transaction을 보내거나 혹은 endorsement policy를 충족시키지 않는 transaction을 제출하게되면 commit validation 단계에서 reject 하게 된다.

4. Client assembles endorsements into a transaction

어플리케이션은 transaction proposal과 “transaction message”의 response를 Ordering Service에 보내게 된다. 그리고 transaction은 read/write set, endorsing peer의 signature들과 channel ID를 포함하고 있다. 이 때 Ordering Service는 transaction 전체를 살펴볼 필요가 없다. Ordering Service에서는 단순히 네트워크 상의 모든 channel에서 transaction을 받고 channel에 따라서 정렬한 다음 channel 별로 block을 생성하면 된다.

5. Transaction is validated and committed

위에서 생성된 block은 해당 channel의 모든 피어들에게 전송된다. 그리고 block에 있는 transaction들은 endorsement policy를 충족시키는지 검증하게되고 read set들에 대해서는 ledger state에 변화가 생기지 않았는지 확인한다. 이 검증 결과에 따라서 block의 transaction들은 valid나 invalid라는 태그를 달게된다.

6. Ledger updated

마지막으로 모든 피어들은 channel의 chain에 block을 연결시키게 되고 valid한 transaction의 write set들은 database에 commit된다. 그리고 client application에 transaction이 chain에 붙었다는 event가 전파되고 그 transaction이 검증된 것인지 아닌지도 알리게 된다.

Sequence Diagram

마지막으로 모든 step들을 sequence diagram으로 표현하면 다음과 같다.

https://hyperledger-fabric.readthedocs.io/en/release-1.2/txflow.html

Ports & Adapters Architecture

Hexagonal Architecture로 알려져있는 Ports & Adapters Architecture는 2005년에 Alistair Cockburn 블로그에 소개되었다. 거기서 그는 Ports & Architecture의 목표를 한 문장으로 정리했다.:

Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases. – Alistair Cockburn 2005, Ports and Adapters

Ports & Adapters Architecture에 관한 글들 중에 layer들에 대해서 많이 얘기를 하는데 실제 Alistair Cockburn가 작성한 포스트에서는 layer에 관해서는 별로 언급이 없었다.

핵심은 application을 외부 기술적인 부분들과 분리시켜놓는 것이다. 그리고 그 둘 사이의 input과 output의 전달은 port를 통해서만 하게 된다. Application 입장에서 보았을 때 application은 누가 어떤 input을 보내고 output을 받는지 모르는 것이 좋다. 왜냐하면 application이 사용하는 기술적인 부분들과 비즈니스적인 요구사항들을 분리시켰을 때 기술적인 부분과 비스니스 측면에서 변화가 생기더라도 application 내부에서는 그 변화와 상관없이 독립적으로 개발할 수 있기 때문이다.

이번 포스팅에서는 다음 주제에 대해서 다뤄보려고 한다.

과거 방식의 문제점

과거의 접근 방식은 front-end와 back-end 쪽 둘 다에서 문제를 가지고 있었다. Front-end에서는 비즈니스 로직을 다루는 쪽에서의 문제점을 UI까지 끌고 오거나(예를 들어, use case logic을 controller나 view쪽에 넣어서 다른 UI 화면에서 재사용이 불가능하게 만들어버린다.) 아니면 반대로 UI에서 결함이 있는 부분을 비즈니스 로직에서 해결하려고 한다.(template에서 필요한 로직을 수행하기 위해 entity에 method를 추가해버린다.)

external technology corrupts application business

Back-end는 외부 라이브러리나 db와 같은 기술적인 부분의 문제점을 비즈니스 로직으로 끌고올 수 있다. 예를 들어, 외부 라이브러리를 직접적으로 참조(reference)한다던가 외부 라이브러리의 타입을 비즈니스 로직 내부에서 참조하고 더 심각하게는 비즈니스 로직 내부에서 외부 라이브러리 클래스를 인스턴스화 하는 것이다.

Layered Architecture으로부터의 발전

2005년 쯤에 EBIDDD가 소개되고 그 덕분에 전체 system과 관련이 깊은 것은 다름아닌 안쪽에 위치하고 있는 layer라는 것을 알게되었다. 안쪽의 layer에 모든 비즈니스 로직들이 들어가야했고(들어가는 것이 좋다.) 그 비즈니스 로직이 다른 어플리케이션과 실제로 다른 부분이 되었다.

그런데 Alistair Cockburn은 최상층부와 최하층부의 layer가 단순히 데이터들이 어플리케이션으로부터 나가고(exit point), 어플리케이션으로 들어오는 부분인 것(entry point)을 알게되었다. 최상층부와 최하층부는 실제로 다르지만 그 두 개가 비슷한 목표와 역할을 가지고 디자인적인 부분에서 대칭적이라는 것을 또한 알게되었다. 또한 만약 우리가 어플리케이션의 비즈니스 레이어와 분리시키고 싶다면 entry/exit points들을 이용해서 분리시키면 되었다.

port diagram

일반적인 layering diagram과는 다르게 이번엔 entry/exit point들을 각각 위와 다이어그램의 아래가 아니라 왼쪽과 오른쪽에 둘 것이다. 그런데 그림과 같이 Application 양쪽에 entry/exit point들이 여러개 있을 수 있다는 것을 알 수 있다. 실제로 entry/exit point가 여러개 있는 경우가 많은데 예를 들면, API와 UI는 왼쪽 편에 두 개의 다른 entry/exit points로 그려질 것이다. 반면에 ORM과 search engine의 경우 오른쪽 편에 그려질 것이다. Application이 여러개의 entry/exit points를 가질 수 있다는 것을 나타내기 위해서 application diagram을 다각형으로 그렸고 다이어그램은 얼마든지 다른 다각형이 될 수 있었다. 그런데 일반적인 다이어그램의 형태가 육각형(hexagon)이 되었기 때문에 “Hexagonal Architecture”으로 불린다.

hexagonal diagram

Ports & Adapters Architecture는 port와 adapter로 구현된 abstraction layer을 이용해서 이 문제를 해결했다.

Port란 무엇인가?

Port는 application 입장에서 consumer, 또는 application에서 나가거나/들어오는 끝 부분이라고 볼 수 있다.

많은 프로그래밍 언어에서 port는 interface로 표현된다. 예를 들어, search engine에서 검색을 수행하는 역할의 interface가 있을 수 있다. Application 비즈니스 로직에서 우리는 이 interface를 search engine의 구체적인 구현은 모른체 search engine을 사용하기 위한 entry/exit point로 사용할 것이다.

Adapter란 무엇인가?

Adapter는 한 interface를 다른 interface로 바꿔주는 클래스를 말한다.

예를 들어, 한 adapter가 interface A를 구현하고 그 adapter에서 interface B를 주입받는다고 생각해보자. 그 adapter는 인스턴스화할 때 constructor에서 interface B를 구현한 객체를 주입받을 것이다. 외부에서는 interface A가 필요할 때 이 adapter를 주입한다. 그리고 ineterface A의 method가 호출될 때마다 adapter 내부의 interface B의 객체가 호출된다.

두 가지 종류의 adapter

위의 다이어그램에서 왼쪽에 있는 adapter들은 Primary 또는 Driving Adapters라고 부른다. 왜냐하면 Driving Adapter에서 주로 application의 동작을 시작하기 때문이다. Driving Adapter에는 주로 UI 쪽이 들어간다.

반대로 오른쪽 편에는 back-end와 연결되는 부분이 들어간다. 오른쪽의 adapter들은 Secondary 또는 Driven Adapters라고 불리는데 항상 Primary adapter에 의해서 반응하고 동작하기 때문이다.

또한 두 종류의 adapter에서 어떻게 port와 adapter가 쓰이는지도 차이점이 있다.:

  • 왼쪽의 adapter는 port에 의존하고 있다. 실제로 사용될 때는 구체적인 port의 구현체가 adapter에 주입된다. 이 때 왼쪽에서 port와 그 구현체는 application 내부에 속하게 된다.
  • 오른쪽의 adapter는 port의 구체적인 구현체가 되고 application의 비즈니스 로직에 주입된다. 그렇기 때문에 Application 비즈니스 로직의 입장에서는 port의 interface만 알고 있다. 이 때 port는 application 내부에 속하지만 port의 구체적인 구현체는 application 외부에 속하게 된다. 그리고 그 구현체는 외부 tool(SMS lib, ORM 등)을 감싸고 있다.

diagram that describe how adapters and ports work

Port & Adapter Design의 장점은 무엇인가?

Port & Adapter Design을 사용하면 우리 application을 중심에 두고 외부적 요소와는 분리시킬 수 있다는 장점이 있다. 여기서 외부적 요소는 ORM이나 ES lib과 같은 외부 라이브러리 등이 있다. 우리는 이런 외부적인 요소들을 분리시키면서 구체적인 구현체는 몰라도 쉽고 빠르게 테스트할 수 있고 다른 곳에서 재사용할 수 있다.

구현체와 기술적인 부분의 분리

Context

예를 들어 우리가 SOLR을 search engine으로 사용하는 application을 만드려고 한다고 생각해보자. 그리고 우리는 우리의 application과 SOLR을 오픈소스 라이브러리를 사용해서 연결하고 조회라는 요구사항을 수행하려고 한다.

Traditional approach

과거의 방식을 사용하면 우리는 외부 라이브러리를 우리 코드베이스에 직접 사용하게 될 것이다. 외부 라이브러리의 타입을 직접 가져다 쓰고 구현체와 상위 클래스들을 직접 가져다 쓴다.

Ports and adapters approach

Ports and adapters design을 적용하면 우리는 먼저 interface를 만든다. 예를 들어 UserSearchInterface를 만든다고 생각해보자. 우리는 외부 라이브러리의 타입을 직접 가져다 쓰는 것이 아니라 UserSearchInterface를 대신 사용할 것이다. 그 다음으로 UserSearchSolrAdapter이라는 SOLR의 adapter를 만들자. 그리고 이것으로 UserSearchInterface의 함수들을 구현하게 된다.이것을 구현할 때는 SOLR 라이브러리를 wrapping하는 형식으로 만들어지고 나중에 실제 라이브러리를 외부에서 주입받게 된다. 그리고 UserSearchSolrAdapter의 함수를 호출할 때마다 내부의 SOLR 라이브러리가 호출된다.

Problem

시간이 지나면서 우리는 search engine을 SOLR에서 Elasticsearch로 바꾸고 싶어졌다. 그리고 같은 search 기능에 대해서 런타임때 한번씩은 SOLR을 이용해서 수행하고 다른 때에는 Elasticsearch 이용하고 싶다.

그런데 과거의 방식으로는 SOLR에서 Elasticsearch로 라이브러리 전환이 힘들다는 것을 알 수 있다. 왜냐하면 현재 SOLR이 사용되고 있는 부분을 찾아서 Elasticsearch 라이브러리로 교체를 해주어야하는데 SOLR과 Elasticsearch 라이브러리는 각각 다른 함수 이름을 가지고 있고 함수의 인자와 리턴값도 제각각이기 때문에 SOLR에서 Elasticsearch로 교체하는 것이 쉬운 일이 아님을 느낄 수 있다. 런타임때 두 개의 라이브러리를 번갈아가면서 쓰는 것은 불가능에 가깝다.

Ports & Adapters design에서는 반면에 이 문제를 해결하기 위해서 단순히 Elasticsearch 라이브러리를 위한 새로운 adapter를 만들면 된다. 그 새로운 adapter를 UserSearchElasticsearchAdapter라고 해보면 그 adapter를 SOLR adapter 대신 주입해주면 된다. 그리고 런타임때 어떤 라이브러리를 사용할지 결정하는 것은 Factory를 사용해서 어떤 adapter룰 주입할지 정해주면 된다.

전달 메커니즘의 분리

위의 예시와 비슷한 방식으로 이제 web GUI, CLI, web API를 사용하는 application이 있다고 생각해보자. 그리고 이 application에서 아까 나열한 총 세 가지의 UI에서 공통적으로 사용되는 기능이 있을 수 있는데 여기서는 예시로 그 기능이 UserProfileUpdate라고 해보자.

Ports & Adapters design에서 이 기능은 interface 형태로 되어있는 application service에 UserProfileUpdate 의 인자와 리턴 값이 명시되어 있을 것이다.

각각의 UI들은 각자의 controller(또는 console command)를 가지고 있을 것이다. 그리고 그 controller는 UserProfileUpdate를 가지고 있는 application service를 사용해서 함수를 호출할 것이며 그 구체적인 구현체는 외부에서 주입될 것이다. 이 때 controller(혹은 console command)가 adapter가 된다.

결국 우리는 UserProfileUpdate라는 비즈니스 로직은 건드리지 않은 채 UI를 바꿀 수 있게 된다.

테스트

Ports & Adapters Architecture에서는 테스트가 훨씬 쉬워 진다. 첫 번째 예시의 경우 port에 해당하는 interface를 이용해서 mock 객체를 만들어서 테스트를 할 수 있다. 그러면 실제 SOLR이나 Elasticsearch 라이브러리를 사용하지 않아도 된다.

두 번째 예시의 경우, 우리는 모든 UI를 application service와 분리한 채로 테스트 할 수 있다. application service에 해당하는 interface를 mock 객체로 만들면 되기 때문이다.

마무리

지금까지 살펴본 Ports & Adapters Architecture에서는 한 가지의 목표를 가지고 있다.: 전체 시스템에서 application의 비즈니스 로직을 외부 라이브러리 및 툴(tools)로부터 분리(isolate) 시키는 것이다. 그리고 이 분리는 interface를 사용해서 달성할 수 있다.

UI(the driving adapters)에서는 application의 interface를 사용하는 adapter를 만들었다. (controller)

Infrastructure side(the driven adapters)에서는 application의 interface를 구현하는 adapter를 만들었다.(repositories)

Sources

1992 – Ivar Jacobson – Object-Oriented Software Engineering: A use case driven approach

200? – Alistair Cockburn – Hexagonal Architecture

2005 – Alistair Cockburn – Ports and Adapters

2012 – Benjamin Eberlei – OOP Business Applications: Entity, Boundary, Interactor

2014 – Fideloper – Hexagonal Architecture

2014 – Philip Brown – What is Hexagonal Architecture?

2014 – Jan Stenberg – Exploring the Hexagonal Architecture

2017 – Grzegorz Ziemoński – Hexagonal Architecture Is Powerful

2017 – Shamik Mitra – Hello, Hexagonal Architecture

본 글은 원작자의 허가를 받고 번역한 글입니다. 의역과 오역이 있을 수 있습니다. 원본 링크: https://herbertograca.com/2017/09/14/ports-adapters-architecture/#implementation-and-technology-isolation

DDD – Entities, Value Objects, Aggregates

Domain-Driven Design 모델링을 할 때 알아야할 가장 기본적인 요소들에 대해서 포스팅해보려고 한다. DDD의 전체적인 개념은 여기에 소개되어있다.

이번 포스팅에서는 세 가지 개념을 설명하려고 한다:

  • Entities
  • Value Objects
  • Aggregates and Roots

Entities

Eric Evan의 Domain-Driven Design 책에서:

Many objects are not fundamentally defined by their attributes, but rather by a thread of continuity and identity.

보통 object-oriented 방식으로 디자인할 때는 model들의 nouns와 verbs들을 정하면서 출발하게 된다. DDD에서 모델링을 할때는 먼저 Ubiquitous Language를 정하고 그에 맞춰서 model의 naming를 정하고 identity를 드러내게한다.

예를 들어서, Person이라는 객체에 대해서 생각해보자. Person이라는 struct엔 Name이라는 attribute가 있고 그 밖의 다양한 attribute들이 있을 수 있다. 여기서 만약 두 개의 Person객체가 있는데 두 객체의 Name이라는 attribute가 같다고 그 두개는 같은 객체라고 할 수 있을까? 꼭 그렇지만은 않을 것이다. Name이 같다고 할지라도 다른 Address, Age 등 다른 attribute가 다를 수도 있다.

위와 같은 상황에서 Person이라는 객체의 identity는 Name, Address 등과 같은 attribute에 의해서 정해지는 것이 아니다. Person이라는 객체는 그 객체를 유일하게 식별할 수 있는 ‘어떤 값’에 의해서 정해져야할 것이다. 그 ‘값’을 정하는 방법은 여러가지 방법이 있을 수 있고 또 시스템마다도 다를 것이다.

어떤 객체가 Entity인지 확인할 수 있는 가장 간단한 질문이 있다:

만약 같은 class의 두 instance가 다른 attribute들을 가지고 있지만 같은 identity 값을 가지고 있다면, 그 두 인스턴스는 같은 instance일까?

만약 이 질문의 대답이 “같다” 라면 instance와 그에 해당하는 class는 Entity로 생각해야한다.

Value Objects

Eric Evan의 Domain-Driven Design 책에서:

Many objects have no conceptual identity. These objects describe characteristics of a thing

모델링을 하다보면 Entity와는 다르게 어떤 object의 identity가 중요하지 않을 때도 있다. 그럴 땐 value object을 생각하면 좋다. 예를 들어, 만약 어떤 시스템에서 PaintBucket를 모델로 삼아야할 필요가 있을 때 Color와 같은 object를 value object로 생각하면 좋다.

PaintBucketColor를 확인할 때, Color의 identity 값은 필요 없다. 두 개의 Color의 색깔 attribute가 같다면 두 객체를 같은 것으로 생각해도 충분하다.

Entity는 attribute를 바꿔도 identity 값이 변하지 않는 이상 별로 문제가 없는 반면에 value object에서는 attribute를 바꾸는 것이 문제가 될 수도 있다. 왜냐하면 attribute 자체가 그들의 identity가 되기 때문이다. 그렇기 때문에 value object를 immutable하게 만드는 것도 value object를 모델링하는 방법이 될 수 있다. 또한 value object도 Entity와 마찬가지로 Ubiquitous Language에서 정의한 것들 중에서 개념적인 것을 나타내야하고 남용하지 않는 것이 중요하다.

Aggregates

실제 세계에서 많은 부분들은 서로 관계를 가지고 있다. 예를 들어, 어떤 사람이 신용 카드를 여러 장 가지고 있을 수 있다. 그런데 각 신용카드마다 소유주가 있고, 발급해준 회사가 다를 수도 있고 또 각 회사마다는 또 여러 계좌 정보를 가지고 있을 수도 있다. 이러한 수 많은 관계들을 전부 class로 나타내면 엄청 복잡해질 것이다.

이러한 각각의 개념적인 부분들을 entity로 나타낼 수 있을 것이다. 그런데 위와 같이 entity 사이의 관계가 복잡해지면 각각의 entity를 관리하기 힘들어진다. entity ‘A’가 entity ‘B’를 가지고 있는 구조가 있을 수 있다. 그런데 ‘A’가 operation을 하게 되면 ‘B’가 변하지 않는다는 보장을 쉽게 할 수 있을까? 갯수가 적을 때는 괜찮겠지만 조금만 복잡해지면 domain에서 난리가 날 수 있다.

aggregates는 한 개 또는 다수의 entity를 가지고 entity보다 큰 boundary를 만들어준다. aggregates가 감싸고 있는 모든 entity들은 변하지 않아야 하고 entity들이 operation을 했을 때 aggregate 내부의 entity들이 변하지 않아야한다. aggregate 마다 root entity가 있는데 aggregate 외부 객체들이 참조할 수 있는 유일한 entity이다. Eric Evan의 Domain-Driven Design 책에서 다음과 같은 규칙들을 지켜야한다고 명시했다:

  • Root entity는 global한 identity 값을 가지고 있고 aggregate 내부의 다른 entity들이 변하지 않았는지 체크해야한다.
  • Root entity는 global한 identity를 가지고 있다. aggregate 내부의 다른 entity들은 local한 identity를 가지고 있고 이 identity는 aggregate 내부에서만 유일하다.
  • Aggregate 외부에서는 root entity만 접근 가능하다. aggregate 내부의 다른 entity에 대해 접근하고 싶으면 root entity를 통해서 접근해야한다.
  • Aggregate Roots들은 database query를 통해서만 얻을 수 있다.
  • Aggregate에 대해서 delete operatation을 하면 aggregate 내부의 모든 entity를 삭제해야한다.

이런 aggregate들이 entity보다 더 큰 boundary를 만들어 줌으로써 모델링이 한결 간단해진다. 왜냐하면 위와 같은 규칙들을 지키도록 하면서 관계를 만들어야 하기 때문이다.

모든 관계들이 association을 통해서 만들어질 필요는 없다. 예를 들어 EmployeeManager의 관계에서 ManagerEmployee를 통해서 얻을 수 있고, Manager를 통해서 Employee를 얻으려면 EmployeeRepository를 통해서 가져오게 만들 수 있다. Employee를 aggregate root라고 하면 Manager를 직접 reference하는 것이 가능하기 때문이다.

Modeling and simplification

모델링을 할 때 우리는 실세계의 개념과 사물들을 모델로 삼고 각각의 그것들을 Entity나 Value Object(그리고 Service)들로 나타낼 것이다. 그리고 이러한 것들을 더욱 단순화하기 위해서 우리는 Aggregate와 Root를 사용한다. 그리고 각각의 모델들은 Ubiquitous Language로 표현할 수 있어야한다. 그래야 같이 개발하는 사람들이 다른 사람들이 작업한 코드를 보았을 때, 이 코드가 어떤 일을 하는지 알기 쉬워진다.

Reference

Domain-Driven Design Key Concepts

현재 오픈소스로 활동하고 있는 it-chain-Engine에서 적용 중인 아키텍쳐, 디자인에 대해서 조금 더 자세하게 알아보고 개념에 대해서 포스팅해보려고 한다. 그 중에 첫 번째가 DDD(Domain-Driven-Design)이다.

본 글은 원작자의 허가를 받고 번역한 글입니다. 의역과 오역이 있을 수 있습니다.

원본 링크: https://herbertograca.com/2017/09/07/domain-driven-design/

Domain-Driven Design과 관련해서 엄청나게 많은 중요한 컨셉들이 있지만 여기서 그것들에 대해서 모두 다루는 것은 아니고 중요하다고 생각하는 개념들에 대해서 나열하고 그것들에 대해서 설명해보려고 한다.

이번 포스팅에는 다음과 같은 개념들에 대해서 적어보려고 한다.

  • Ubiquitous language
  • Layers
  • Bounded contexts
  • Anti-Corruption Layer
  • Shared Kernel
  • Generic Subdomain

Ubiquitous language

소프트웨어 개발에서 계속 생기는 문제 중 하나는 코드를 보면서 이것이 무엇을 하는 것이고, 어떻게 동작하는지에 대해서 이해하는 것이 어렵다는 것이다. 만약 코드를 만든 사람이 어떤 코드에 대해서 ‘A’라고 말하는데 실제로 코드는 ‘B’와 관련된 것이라면, 그 코드를 보는 사람들은 더더욱 혼란스러워질 것이다. 하지만 이러한 문제들은 class와 method에 더욱 적절한 네이밍을 해주면 어느정도 사라지게 될 것이다. 여기서 ‘적절한 네이밍’이란 도메인 컨텍스트에 대해서 어떤 object가 어떤 일을 하고 어떤 method가 어떤 일을 하는지 더욱 명확하게 표현하는 것을 말한다.

Ubiquitous Language의 메인 아이디어는 앞으로 구현하게 될 application 및 technology와 요구사항에 해당하는 business을 매칭시키는 것이다. 매칭시킨다는 의미는 둘 사이간에 의미를 공유할 수 있는 공용어(common language)를 두는 것이다. 그리고 이렇게 만들어진 공용어를 이용해 코드를 작성하게 된다. 요구사항에 해당하는 business에서 용어의 컨셉을 가져온다 그 다음 그것을 구현할 technology 쪽에서 그것을 다듬고 확정하게 된다. (business에서 가져온 컨셉으로는 항상 좋은 네이밍을 가져가기 힘들다.) 이렇게 둘 사이의 입장을 반영한 용어를 만듦으로써 우리는 business와 technology에서 사용할 수 있고, 앞으로 우리가 작성할 코드에서도 모호함 없이 사용할 수 있다. 이것이 Ubiquitous Language라고 할 수 있다. 코드에서 사용될 class, methods, properties, modules의 네이밍은 Ubiquitous Language와 매칭돼야 한다.

Layers

다른 디자인에서도 layer라는 컨셉은 사용되지만, DDD에서 특징적인 layer는 다음과 같다.

  • User Interface

    User Interface에서는 사용자들이 상호작용할 수 있는 screen을 만들고 사용자들의 input을 application의 명령들(commands)로 변환한다. 여기서 중요한 점은 사용자(user)들은 사람이 될 수도 있지만 어떤 application이 다른 application의 api를 사용한다면 그 application도 사용자가 될 수 있다.

  • Application Layer

    사용자들이 요구하는 tasks들을 수행하기위해 domain object들을 사용한다. Application Layer는 busniess logic을 가지고 있지 않고 Application Service가 포함된다. Application Service는 domain object에 해당하는 repository, domain service, entity, value object을 가지고 그것들을 조합함으로써 필요한 목표를 달성한다.

  • Domain Layer

    Domain Layer에서 Domain Services, Entities, Events와 같은 domain object들은 필요한 모든 business logic들을 담고 있다. 그렇기 때문에 Domain layer가 전체 시스템에서 핵심이라고 할 수 있다. Domain Services는 Entity에 딱 맞지 않는 로직이 들어가거나 혹은 몇몇 Entity들을 이용해서 business logic을 처리한다.

  • Infrastructure

    persistence나 messaging과 같이 상위 계층에서 layers들을 서포트해주는 것들이 포함된다.

Domain-Driven Design layer diagram
Eric Evans, 2003

Bounded Contexts

기업용 application에서는 model이 커질 수 있고 그에 따라 같은 코드베이스에서 작업을 하는 개발자의 수가 늘어날 수 있다. 그런데 그에 따라서 두 가지 문제점이 발생할 수 있다.

  1. 한 코드 베이스에서 작업하는 개발자의 수가 늘어날수록 한 사람이 알아야하는 코드의 양이 많아지고, 여러명이 작업하기 때문에 코드를 이해하기도 어려워진다. 그렇기 때문에 버그나 에러가 생길 가능성이 커지게된다.
  2. 같은 코드 베이스에서 작업하는 개발자가 많아질수록 작업을 조율하기가 힘들어지고 같이 쓰는 domain 코드들이나 기술적인 부분이 많아지게 된다.

위와 같은 상황을 해결할 수 있는 방법 중 하나는 많은 개발자들이 공동으로 작업해야하는 코드베이스들을 쪼개는 것이다. 그리고 이렇게 쪼갤 수 있게 도와주는 것이 “bounded context”이다.

Bounded context는 model들이 분리되어 적용될 수 있는 context를 말한다. 이 ‘분리’는 기술적인 분리나 코드의 분리, 데이터베이스 schema의 분리 그리고 팀의 분리 등을 말한다. 이러한 분리의 여러 단계 중에서 어디까지 실제 프로젝트에서 적용할지는 그때 그때 상황과 필요에 맞춰서 이뤄진다.

그런데 이러한 bounded context를 이용한 분리라는 개념은 완전히 새로운 것은 아니다. Ivar Jacobson이라는 사람이 1992년에 그의 에 ‘subsystems’이라는 비슷한 개념에 대해서 서술한 적이 있다.

Subsystems description diagram
Ivar Jacobson, 1992

그 때 당시 그가 서술했을때 이미 다음과 같은 구체적인 아이디어를 가지고 있었다:

  • 전체 system은 여러 개의 subsystem들로 이루어져있다. 그리고 그 각각의 subsystem들은 또다시 subsystem을 가질 수 있다. (중략…) Subsystem들로 전체 system을 구성하는 것은 그렇기 때문에 전체 system을 개발하고 유지보수할 수 있는 방법이다.
  • Subsystem의 과제는 객체들을 패키징하는 것이다. 그렇게 함으로써 전체 시스템의 복잡도가 줄어들게 된다.
  • 특정 기능과 관련된 모든 객체들은 같은 subsystem에 두어야한다.
  • 이렇게 특정 기능과 관련된 객체들을 묶는 것은 같은 subsystem에서는 strong function coupling을 가지고 다른 subsystem과는 weak coupling을 가지게하기 위해서다. (오늘날에는 low coupling, high cohesion으로 알려지고 있는 개념이다.)
  • 한 subsystem에서는 한 명의 actor와만 coupled 되어야한다. 왜냐하면 system의 변화는 주로 actor에 의해서 일어나기 때문이다.
  • (중략…) control object들을 subsystem에 두기 시작하였고 그것들과 강한 결합을 가지는 entity object들과 interface들을 같은 subsystem 내에 두었다.
  • object 사이에서 기능적으로 강한 결합을 가진 것들은 같은 subsystem 내에 두어야한다. (중략…)
    • 어떤 한 object의 변화가 다른 object에도 영향을 미치는가?(지금 이 개념은 현재 Robert C. Martin이 1996년도에 “Granularity”라는 논문에 서술한 The Common Closure Principle ― 같이 변하는 class들은 같은 package로 묶여야한다.―로 알려져있다.)
    • 같은 actor에 대해서 커뮤니케이션하는가?
    • *어떤 두 object가 interface나 entity와 같은 다른 제 3의 object에 의존하는가? *
    • 어떤 object가 다른 object들에게 영향을 주고 있는가?(지금 이 개념은 현재 Robert C. Martin이 1996년도에 “Granularity”라는 논문에 서술한 The Common Reuse Principle ― 같이 사용되는 class들은 같은 package로 묶여야한다.―로 알려져있다.)
  • 또다른 subsystem 분리 기준은 그들 사이에서 최소한의 커뮤니케이션만 일어나야한다는 것이다.(low coupling)
  • 사이즈가 큰 프로젝트에서는 그렇기 때문에 subsystem을 나누는 다른 기준이 있을 수 있다. 예를 들어:
    • 다른 두 개발 팀은 다른 능력과 자원을 보유하고있고, 그에 따라 적절하게 작업량을 분배해야할 것이다.(두 팀이 지리적으로 떨어져있을 수도 있다.)
    • 분산 환경에서는, 각각의 subsystem은 각각의 logical node에 의해 사용될 수 있다.(SOA, web services and micro services)
    • 만약 어떤 product가 현 시스템에서 사용되려면, 그것은 또다른 subsystem으로 여겨져야할 것이다.(시스템이 ORM과 같은 library에 의존하는 경우)

Anti-Corruption Layer

Anti-corruption layer는 기본적으로 두 subsystem 사이에서 middleware역할을 한다. 그리고 나눠진 subsystem들은 서로 직접적으로 의존하는 대신 anti-corruption layer에 의존하게된다. 이렇게 하게되면 한 subsystem을 완전히 다른 것으로 바꾸더라도 우리가 수정해야할 부분은 anti-corruption layer밖에 없다. 다른 subsystem들은 그대로 두어도 된다.

Anti-corruption layer는 legacy 시스템과 새로운 시스템을 합쳐야할 때 유용하다. legacy 구조가 새롭게 디자인한 구조에 영향을 주지 않으려면 anti-corruption layer를 두어서 새로운 시스템에 꼭 필요한 legacy system의 API만 가져오면 된다.

이것은 세 가지 역할을 가지고 있다:

  1. Client subsystem이 필요한 API를 또다른 subsystem에서 가져오는 것
  2. Subsystem 사이에서 data와 command를 전달하는 것
  3. 단방향이든 양방향이든 필요함에 따라 서로 다른 subsystem들 사이에 communication을 형성해주는 것

Roles of anti-corruption layer in diagram
Eric Evans, 2003

이것은 여러 subsystem들을 직접 제어하고 싶지 않을 때 더욱 유용하다. 그런데 이것은 모든 subsystem들을 컨트롤하고 싶을 때에도 유용할 수 있다. 그리고 아주 성격이 다른 model들이 있을 때와 한 model을 바꿨을 때 나머지 시스템에 줄 수 있는 영향을 줄이고 싶을 때에도 좋다.

Shared Kernel

우리가 아무리 컴포넌트를 분리시키고 decouple 하려고해도 어떤 domain 코드들은 여러 컴포넌트에서 공유하는게 더 좋을 때도 있다.

이렇게 공유가 필요한 코드들을 여러 컴포넌트에 공유하는 부분이 Shared Kernel이고, Shared Kernel을 두게 되면 shared kernel과는 강하게 결합이 생기지만 나머지 컴포넌트끼리는 여전히 분리시킬 수 있다.

예를 들어, 어떤 컴포넌트가 event를 전파시키고 다른 컴포넌트들이 그 event를 listen하는 경우라면 event가 shared kernel 부분에 들어갈 수 있다. 물론 service interface나 entity들도 shared kernel에 포함될 수 있다.

그렇지만 shared kernel은 가능한 작게 두는 것이 좋다. 그리고 shared kernel 코드는 다른 컴포넌트들에서 사용될 가능성이 높기 때문에 이 부분을 수정할 때 어떤 다른 부분에 영향을 주는지 확인하는 것이 좋다. 그렇기 때문에 shared kernel 코드들을 수정할 땐 전체 팀원과 충분한 상의와 합의 후에 작업하는 것이 좋다.

Generic Subdomain

Subdomain은 완전히 분리된 domain을 말한다. 그리고 generic subdomain은 현재 우리가 작업 중인 application 뿐만 아니라 비슷한 성격의 다른 application에서도 충분히 쓰일 수 있는 subdomain을 말한다.

만약 우리가 금융과 관련된 부분이 우리 프로젝트에 있다면, 다른 곳에서 만들어진 금융 관련 library를 사용할 수도 있을 것이다. 그 library를 가져와서 쓰든 안쓰든, 만약 그 금융 부분이 generic subdomain이라고 한다면 우리 application의 꼭 필요한 부분이지만 핵심 business는 아니다. 그렇기 때문에 팀이 작업할 때 그 부분은 엄청 신경써야할 부분이 아니고 또 결정하기에 따라서 dependency management tool을 이용해서 아예 소스코드 밖에 두어도 된다.

Conclusion

여기서 소개된 DDD와 관련된 개념들은 대부분 single responsibility, low coupling, high cohesion, isolating logic과 관련된 것들이다. 그래서 이러한 것들은 application을 더욱 견고하고 요구사항에 따른 변화에 잘 적응할 수 있게 만들어준다.

Sources

1992 – Ivar Jacobson – Object-Oriented Software Engineering: A use case driven approach

1996 – Robert C. Martin – Granularity

2003 – Eric Evans – Domain-Driven Design: Tackling Complexity in the Heart of Software

2014 – Eric Evans – Domain-Driven Design Reference

본 글은 원작자의 허가를 받고 번역한 글입니다. 의역과 오역이 있을 수 있습니다.

원본 링크: https://herbertograca.com/2017/09/07/domain-driven-design/