개발 회고: 테스트와 코드 리뷰에 대해서

프로덕트 사이클이 한 번 끝나면서 이번 사이클에서 부족했던 부분을 회고해보려고 한다. 그리고 어떻게 하면 개선할 수 있을지 대해서 정리해보려고 한다.

테스트

다양한 상황 테스트

여기서 말하는 테스트란 코드로 작성된 유닛테스트, 통합테스트를 말하는 것이 아니다. 유닛테스트와 통합테스트는 당연하게 작성해야한다고 마인드를 가져야한다.

그런데 시간 문제 때문에 모든 상황에 대해서 테스트 코드를 짜는 것은 상당히 힘들다. 그래서 핵심 로직에 대해서 테스트 코드를 짜는 경우가 많은데, 이 마저도 모든 예외 상황에 대해서 테스트 코드를 짜는 것은 상당한 노력이 필요하다.

여기서 상당한 노력이란, 시간 문제뿐만아니라 ‘이 정도면 괜찮겠지’라는 순진한 생각을 떨쳐내는 것을 포함한다.

그래서 기본적으로 테스트 코드를 작성하고나면 실제로 돌려보면서 잘 돌아가는지 확인해보는데, 이러한 과정도 테스트 케이스를 작성하는 것만큼이나 중요하다.

실제로 돌려보는 과정에서 중요한 것은 다양한 상황에 대해서 테스트를 하는 것이다. 이번에 프로덕트를 만드는 과정 중 테스트와 관련해서 반성할만한 상황이 몇 가지 있었다.

가장 기본적인, 그런데도 그것을 지키지못해 너무 부끄러워졌던 상황을 꼽자면 다양한 인자에 대해서 테스트를 못해본 것이다.

API를 변화하는 요구사항에 따라 조금씩 여러번 고쳐나갈 때가 있었다. 그 중 어떤 엔티티를 생성하는 API가 있었는데 그것을 생성하기 위한 JSON body 데이터는 100줄이 조금 넘었다. 그래서 너무 부끄럽지만 고백하자면 시간이 지날수록 잘되는 상황에 대해서만 테스트를 했었다. 이전에 모든 경우에 대해서 테스트해서 통과했던 기억으로 ‘지금도 잘될거야’라는 너무 순진한 생각을 했다.

융통성을 가져야하겠지만 git diff에 꽤 변화가 보인다면 꼭 모든 상황에 대해서 테스트해보자. 그래야 다른 사람한테도 ‘요구사항에 맞춰 다 수정했다’고 자신있게 말할 수 있다. 빨리 만드는 것도 좋지만 안전하게 만드는게 더 중요하다. 너무 기본적인 것이지만 자꾸 빨리하고 다음으로 넘어가려는 본성때문에 이 원칙이 잘 지켜지지 않을 때가 있다. ‘잘 될거야’라는 말은 진짜 하지 말자.

스트레스 테스트

예외 상황이 발생했을 때 잘 죽고, 잘 복구되고 다양한 상황에서 기본적인 기능들이 제대로 동작한다면, 스트레스 테스트는 꼭 해보자.

예를 들어 Kubernetes pod에 어떤 서비스를 얹어서 클라이언트에게 제공한다면 어느 정도 커넥션을 감당할 수 있는지 확인해보는 경우가 있을 것이다. 혹은 클라이언트마다 pod이 뜬다면 현재 production cluster에 몇 개의 pod이 뜰 수 있는지 확인해보는 것이 있을 수 있다.

어느정도 부하를 감당할 수 있는지 파악하고 있어야 나중에 사용자 수에 따라서 필요한 대처를 할 수 있다.

위에서 얘기한 것이 부하의 강도에 대한 것들이라면 이에 못지않게 중요한 것은 서비스를 한 번 띄웠을 때 오래동안 별 문제 없이 살아있느냐도 확인해볼 필요가 있다.

대부분의 경우 개발과정에서는 ‘오랫동안 무언가를 한다’는 것에 대해 테스트를 하지 않는다. 그래서 잠깐 동안 모든 기능들이 제대로 동작하는 것이 길게도 가능한지 한 번 확인해볼 필요가 있다.

이번 사이클에서는 어떤 경우에 클라이언트와 30분마다 소켓 연결이 끊어져서 애먹은 경우가 있었는데 이런 테스트가 없었다면 모든 게 정상적으로 동작한다고 생각했을 수도 있다. 예전에는 정상적으로 동작하는 것처럼 보이는 시스템이 며칠 뒤에 뻗었는데 살펴보니 로그 파일을 관리하는 프로그램이 제대로 동작하지 않아 로그 파일이 디스크를 모두 차지했기 때문이다.

중요한건 마음가짐

여기서 말하려고하는 ‘테스트를 꼼꼼히 하자’는 것은 분명 머리로는 알고 있는 것이고 말하는 것에서 그치면 아무런 소용이 없다. 몇 개의 원칙을 가지고 그것을 마음 속에 단단히 집어넣는 것이 더 중요할 거 같다.

코드 리뷰

이번 프로덕트 사이클에서 테스트만큼 많이 했고 아직 많이 부족하다고 느낀 것들 중 하나는 다른 사람의 ‘코드 리뷰’라고 생각한다. 카카오 리뷰 에 나온 글에서도 공감가는 부분이 많았고 리뷰하는 방법에 대해서도 배울 점이 많았다.

이번 회고글에서는 아직도 많이 부족한 점이 많지만 ‘다음 번에 리뷰를 이렇게 하면 좋겠다’라는 것들에 대해서 적어보려고 한다.

코드 리뷰에 대해 느꼈던 것은 코드 리뷰에 여러 계층이 존재한다는 것이다. 정말 간단한 변수명, 함수명에서부터 전체적인 시스템 관점에서의 리뷰가 있을 수 있다.

초반에는 어떤 것을 리뷰해야할까에서부터 명확한 기준이 서있지 않아 리뷰가 부족했거나 빠뜨린 부분이 있었다. 그래서 이번 회고에서는 코드 리뷰를 할 수 있는 여러 가지 면들에 대해서 정리해보려고 한다.

  • 코드 관점: 변수명, 함수명, 코드 컨벤션, 테스트 코드 유무와 같은 코드의 기본적인 것들
  • 기능 관점: 작성한 코드가 현재 도메인 지식에 맞춰서 제대로 동작하는가?
  • 아키텍처 관점: 현재 이 모듈이 여기 있어도 괜찮은가? 모듈 사이의 의존성, 관계

코드 관점

코드 관점 리뷰 같은 컴포넌트를 작업하는 팀의 개발자가 아니더라도 할 수 있는 리뷰이다. 변수명, 함수명의 경우도 봐야하는 코드와 알아야하는 지식이 클래스를 벗어나지 않기 때문이다. 파일 맨 위에 라이센스를 추가해달라, 테스트 코드를 추가해달라 모두 이에 포함되는 관점이다.

기능 관점

기능 관점은 코드 관점과는 다르게 해당 컴포넌트의 도메인 지식이 필요하기 때문에 다른 팀의 개발자가 기능 관점에서 리뷰를 하는 것은 어렵다. 기능 관점에서는 코드가 예쁘게 짜여졌더라도 컴포넌트들이 디테일한 요구사항에 맞춰서 동작하는지를 확인해야한다.

아키텍처 관점

아키텍처 관점은 기능 관점과는 또 느낌이 다르다. 기능 관점에서는 말 그대로 기능이 어떻게 동작해야한다는 디테일한 부분을 알고 있어야 리뷰를 할 수 있지만 아키텍처 관점에서는 좀 더 거시적으로 이 컴포넌트가 전반적으로 어떤 동작을 해야한다는 것을 알고 있으면 리뷰를 할 수 있다.

나의 코드 리뷰는 어땠나

코드 리뷰 관점을 위와 같이 세 가지로 구분 했을 때 이번 사이클에서 나의 코드 리뷰는 ‘기능 관점’에서 많이 부족했던 것 같다. 코드 관점과 아키텍처 관점에서만 보고 괜찮다고 생각하여 pull request approve를 했는데 다른 사람이 기능 관점에서 코멘트를 했을 때 아차했고 모든 부분을 보지 못했다고 생각하여 부끄러운 마음이 들었다.

기능 관점에서 좀 더 리뷰를 잘하기 위해서는 시시각각 변하는 요구사항을 잘 캐치하고 기억해야한다. 어쩌면 이 부분은 프로덕트에 대해 문서화를 잘하는 것과 관련이 있을 수도 있다. 혹시 잘 기억이 안나거나 내가 도메인에 대해서 잘 모르는 경우에는 pull request를 올린 사람에게 ‘이 기능을 구현한 것이 맞냐’라고 물어볼 수도 있다.

마치며

‘다양한 상황 테스트하기’의 경우 머리로는 분명 알고 있었다. 그런데 ‘터지면 어쩌지’라는 불안감, 근거 없는 자신감 등 심리적인 요소에 의해서 많이 빠뜨렸던 것 같다.

‘코드 리뷰’의 경우는 ‘어떤 것을 리뷰해야 될까’라는 것에 대해서 고민을 많이 했던 것 같다.

이번 프로덕트 사이클 회고를 정리하면 다음과 같다.

  • 조급해하지말고 다양한 상황들에 대해서 꼭 통합테스트를 해라
  • 안될 거 같은 상황을 꼭 테스트 해라. 안되면 고치면 된다. 잘못된 게 아니다.
  • 기능 관점에서 코드 리뷰를 더 잘하기 위해서 해당 컴포넌트 도메인을 정확하게 알고 있자.
Photo by NOAA on Unsplash

DelegateCall: Calling Another Contract Function in Solidity

In this post, we’re going to see how we can call another contract function. And we talk about more deeply about delegatecall

When writing Ethereum Smart Contract code, there are some cases when we need to interact with other contract. In Solidity, for this purpose, there’s several ways to achieve this goal.

If we know target contract ABI, we can directly use function signature

Let’s say we have deployed simple contract called “Storage” that allows user to save a value.

And we want to deploy another contract called “Machine” which is caller of “Storage” contract. “Machine” reference “Storage” contract and change its value.

In this case, we know the ABI of “Storage” and its address, so that we can initialise existing “Storage” contract with the address and ABI tells us how we can call “Storage” contract’s function. We can see “Machine” contract call “Storage” setValue() function.

And write a test code to check whether “Machine” saveValue() function actually calls “Storage” setValue() function and change its state.

And test passed!

If we don’t know target contract ABI, use call or delegatecall

But what if caller (in this case “Machine” contract) doesn’t know the ABI of target contract?

Still we can call target contract’s function with call() and delegatecall().

Before explain about Ethereum Solidity call() and delegatecall(), it would be helpful to see how EVM saves contract’s variables to understand call() and delegatecall().

How EVM saves field variables to Storage

In Ethereum, there’s two kinds of space for saving contract’s field variables. One is “memory” and the other is “storage”. And what ‘foo is saved to storage’ means that the value of ‘foo’ is permanently recorded to states.

Then how could so many variables in single contract not overlap each other’s address space? EVM assign slot number to the field variables.

EVM saves field variables using slot
EVM saves field variables using slot

Because first is declared first in “Sample1” it is assigned 0 slot. Each different variables distinguished by its slot number.

In EVM it has 2256 slot in Smart Contract storage and each slot can save 32byte size data.

How Smart Contract Function is Called

Like general programming code such as Java, Python, Solidity function can be seen as group of commands. When we say ‘function is called’, it means that we inject specific context (like arguments) to that group of commands (function) and commands are executed one by one with this context.

And function, group of commands, address space can be found by its name.

In Ethereum function call can be expressed by bytecode as long as 4 + 32 * N bytes. And this bytecode consist of two parts.

  • Function Selector: This is first 4 bytes of function call’s bytecode. This is generated by hashing target function’s name plus with its arguments type excluding empty space. For example savaValue(uint). Currently Ethereum use keccak-256 hashing function to create function selector. Based on this function selector, EVM can decide which function should be called in the contract.
  • Function Argument: Convert each value of arguments into hex string with the fixed length of 32bytes. If there is more than one argument, concatenate

If user pass this 4 + 32 * N bytes bytecode to the data field of transaction. EVM can find which function should be executed then inject arguments to that function.

Explain DelegateCall with test case

Context

There’s a word “context” when we talked about how smart contract function is called. Actually the word “context” is much general concept in software and the meaning is changed little bit depending on the context.

When we talked about execution of program, we can say “context” as all the environment like variable or states at the point of execution. For example, on the point of exeuction of program ‘A’, the username who execute that program is ‘zeroFruit’, then username ‘zeroFruit’ can be context of program ‘A’.

In the Ethereum Smart Contract, there is lots of context and one representative thing is ‘who execute this contract’. You may be seen msg.sender a lot in Solidity code and the value of msg.sender address vary depending on who execute this contract function.

DelegateCall

DelegateCall, as name implies, is calling mechanism of how caller contract calls target contract function but when target contract exeucted its logic, the context is not on the user who execute caller contract but on caller contract.

Context when contract call another contract
Context when contract calls another contract
Context when contract delegatecall another contract
Context when contract delegatecalls another contract

Then when contract delegatecall to target, how state of storage would be changed?

Because when delegatecall to target, the context is on Caller contract, all state change logics reflect on Caller’s storage.

For example, let’s there’s Proxy contract and Business contract. Proxy contract delegatecall to Business contract function. If user call Proxy contract, Proxy contract will delegatecall to Business contract and function would be executed. But all state changes will be reflect to Proxy contract storage, not Business contract.

Test case

This is extension version of contract explained before. It still has “Storage” as field and addValuesWithDelegateCall , addValuesWithCall in addition to test how storage would be changed. And “Machine” has calculateResult , user for saving add result and who called this function each.

And this is our target contract “Calculator”. It also has calculateResult and user.

addValuesWithCall

And this is our addValuesWithCall test code. What we need to test is

  • Because context is on “Calculator” not “Machine”, add result should be save into “Calculator” storage
    • So “Calculator” calculateResult should be 3, and user address should set to “Machine” address.
    • And “Machine” calculateResult should be 0, and user to ZERO address.

And test pass as expected!

addValuesWithDelegateCall

And this is our addValuesWithCall test code. What we need to test is

  • Because context is on “Machine” not “Calculator”, add result should be save into “Calculator” storage
    • So “Calculator” calculateResult should be 0, and user address should set to ZERO address.
    • And “Machine” calculateResult should be 3, and user to EOA.

But FAILED! What??? Where ‘562046206989085878832492993516240920558397288279’ come from?

As we mentioned before each field variable has its own slot. And when we delegatecall “Calculator”, the context is on “Machine”, but the slot number is based on “Calculator”. So because “Calculator” logic override Storage address with calculateResult, so as calculateResult to user, test failed.

Based on this knowledge, we can find where ‘562046206989085878832492993516240920558397288279’ come from. It is decimal version of EOA.

"Machine" contract field variable overrided when delegatecall
“Machine” contract field variable overrided

So to fix this problem, we need to change the order of “Machine” field variable.

And finally test passed!

Wrap up

In this post, we’ve seen how we can call another contract’s function from contract.

  • If we know the ABI of target function, we can directly use target function signature
  • If we don’t know the ABI of target function, we can use call(), or delegatecall(). But in the case of delegatecall(), we need to care about the order of field variable.

Source code

If you want to test on your own, your can find the code on this repository.

https://github.com/zeroFruit/upgradable-contract/tree/feat/delegatecall

Software Design from point of dependency Part1

We’ve all learned object oriented programming to keep our system being from monster. In object oriented programming, we always talk about Encapsulation, Polymorphism, Inheritance, IoC, DI and so on. But as a developer, we need to think about the point of object oriented programming.

Think about why we learn object oriented stuff? I think the answer is to keep our system maintainable. Then what is meaning of maintainable? Maintainable means we don’t need to code lots of crazy things to satisfy our ever changing business requirements.

What makes us to code all night to change such a little feature? I think one of biggest cause is lack of dependency management. How we create dependency between objects makes great impact on whole system architecture.

In other words we can see how we design our software as making decisions of how object will make relationship with other object. For example, ‘What code would be placed in this class’ or ‘What classes are going to be placed this package’, all these kinds of decisions are kind of software design.

Then is there any principle to put your code on which class or which packages? One simple but fundamental rule is focusing on changeability. If A and B should change together? Then place those in same packages. If not, do not.

Dependency

what is dependency
what is dependency

If A’s change makes B’s change, then we can say that B is depends on A. In other words, depdency is possibility to be change by other object’s change.

There are two basic kinds of dependency.

Association

association diagram
association diagram

If A has association with B then it means A has permanent path for accessing B. This is conceptual explanation and how to implement association is by A having fields for B.

Dependency

dependency diagram
dependency diagram

If A has dependency with B then it means A has temporary relationship with B. In the case of A having B as parameter or return type in method, we can say A has dependency with B.

Real World Example

We’ve talked that designing software architecture is about consideration where to place classes, packages, codes. Codes which have high possibility to change together should be placed nearby, i.e. in the same class or package, so that the code cohesion can be increased. System which has high cohesion can be changed easily by developers. Because each part of the system takes in charge of single responsibility which remove possibility to break system as a result of changing other component.

For further explain how could we improve our system in terms of dependency, let’s take a real world example. I brought scenario in which customer order product on e-commerce service. And we are operating e-commerce system.

The code of this application can be found here:

https://github.com/zeroFruit/oop-dependency/tree/master/ecommerce-00

Service Flow

Basic flow of our service is as such:

e-commerce basic user flow
  1. Customer orders item with options, for example customer ordered two “Fusion5 ProGlide Power razors” with “4 razor blade” and with color of “white”.
  2. If customer ordered successfully and payed, service takes commission fee and start shipping

Business Rules

And this e-commerce service has serveral business rules or restriction, we should follow these rules when designing our system:

  1. Our product list can be changed in real time, this can be happened because product suddenly out of stock or we may decided not to sell this product anymore.
  2. As a result if customer order on our service we should check whether ordered product name is matched with our service’s product name.
  3. In addition to product name, we should check ordered product’s option
  4. In our service, each product has minimum order amount, price.
  5. This is not ordinary but our service has closing hour (for system check hours 🙂 ), so we should check whether our service is opened.

Organize into class

We can see these kinds of rules as order validation logic in software point of view. We should check these kinds of things in our service:

  1. should compare order item name with product name.
  2. should compare order item option group name with product option group name.
  3. should compare order item option name with product option name
  4. should compare order item price with product price
  5. should check whether our service is available
  6. should check whether order item price is over product’s minimum order price

Based on business scenario diagram, we can map these rules into classes with its own methods.

e-commerce service class diagram
e-commerce service class diagram

With this diagram, we can find out what objects will be collaborated to work with validation and order and see which object will make relationship with others.

But before we dive into code to implement this design, there’s one more thing we need to decide: direction of relationship.

Direction of relationship

Direction of relationship in other words tells us what class will depend on what class. In the code this will be implemented by one class having other class as field variable or by having as method parameters.

For example, having other class as field variable, is called association. As we talked before association is permanent relationship.

Order has association with OrderItem
Order has association with OrderItem

With association, we can create path to navigate the other objects. For example, if Order has association with OrderItem and if we know Order, we can find any OrderItem through Order. If these two objects need to collaborate to satisfy business rules and we need a way to find the other object. We can make association.

Association is conceptual thing and there’s lots of way to implement this thing and this is one example. Order has OrderItem as a list. This two has strong relationship, whenever order is placed we need to get its information from order items.

As such we can decide direction of relationship.

direction of dependency between objects
direction of relationship between objects

The direction of arrow tells us which object is associate or dependent on other object. In the case of ‘Order’ and ‘Store’, arrow points to ‘Store’ with solid line. This means that ‘Order’ is associate with ‘Store’ and in the code Order class will have Store as field variable.

In the case of ‘OptionGroupSpecification’ and ‘OrderOptionGroup’ arrow points to ‘OrderOptionGroup’ with dotted line. This means that ‘OptionGroupSpecification’ is dependent on ‘OrderOptionGroup’ and in the code OptionGroupSpecification will reference OrderOptionGroup in the method as parameters or as local variable.

From the layered architecture view

Let’s think about these relationship from layered architecture.

layered architecture diagram
layered architecture diagram

All we’ve talked about objects and relationship goes into ‘domain’ layer. And such a word like ‘service’, ‘domain’ and ‘infrastructure’ is conceptual thing.

How we implement this is another topic. In java, these layer can be implemented through concept of packages.

So we can place our Order, Store classes into ‘domain’ package and OrderService, StoreService which use domain objects and make application API will be placed in ‘service’ package. Below diagram shows packages with its dependencies.

e-commerce service package diagram with dependency
e-commerce service package diagram

But there’s one big problem. You see? There’s cyclic dependency between ‘store’ and ‘order’ in domain layer.

Having cyclic dependency is a signal that architecture designed not that well and means that those packages are strongly coupled which cause seperation of responsibility hard and one component’s changes break other components, plus it is hard to test.

Problem

So where is the problem?

e-commerce service domain class diagram
e-commerce service domain class diagram

To figure out the cause, drew class diagram with its package. You may noticed OptionGroupSpecification directly reference OrderOptionGroup and OptionSpecification reference OrderOption from ‘order’ package. This two dotted lines create dependency from ‘store’ package to ‘order’ package.

Use abstraction to break dependency

The code of how we can refactoring with abstract can be found here

https://github.com/zeroFruit/oop-dependency/tree/master/ecommerce-01

To fix this problem, we are going to use abstraction. Instead of directly referencing ‘order’ package’s class, reference abstract class which is in the ‘store’ package.

Be cautious of the word of ‘abstract’. It is not abstract keyword in java programming language nor interface. I used this term to mean ‘not changed much’, ‘summery of point’, ‘concentrates on essential’.

This is our abstract class. There’s no abstract, interface keyword. Because Option and OptionGroup have essential fields for representing option and option group, we can say that Option, OptionGroup class is much abstracted than OptionSpecification, OptionGroupSpecification class in ‘store’ package or OrderOption, OrderOptionGroup class in ‘order’ package.

With these abstracted class, both ‘order’, ‘store’ domain implement validation logic, also increased reusability.

Result

e-commerce service package diagram with abstract class
e-commerce service package diagram with abstract class

This diagram shows result of refactoring. Now Order, OrderOption depends on OptionGroup, Option class in ‘store’. As a result, by creating abstract class and depends on it, we can remove cyclic dependency.

The way of how we refactor this module is somewhat about ‘Dependency Inversion Principle’.

The idea of this principle is that when designing the interaction between a high-level module and a low-level, the interaction should be done by abstraction between them.

In other words, high-level modules should be easily reusable and unaffected by changes in low-level modules. Also when designing low-level modules, developer should keep in mind with the interaction.

Still there’s some problem in this design, we are going to handle that in the next post.

Conclusion

One important thing when we design software architecture is dependency. Because how we make dependency or relationship between components makes grate impact on the system. And as a developer this can cause fragile codes: one component’s simple change can propagate to other components and break their codes.

There are two kinds of relationship between component: ‘association’, ‘dependency’. Association creates permanant path between components. If A and B has association and we want to know B’s information, we can get it from A.

One signal whether this architecture is well designed or not can be found by checking whether components are have cyclic dependency between them. Cyclic dependency should be avoided as possible because this creates strong coupling.

One way to break cyclic dependency is creating abstraction and making components which cause cyclic dependents to interact with it. This is kinds of ‘Dependency Inversion Principle’ because modules are interact with abstraction instead of concrete one.

Photo by Walter Lee Olivares de la Cruz on Unsplash

왜 JSON-RPC를 사용할까?

Intro

최근에 web3.js 코드를 살펴볼 일이 있었는데 이 때 JSON-RPC를 처음 접했다. 그리고 JSON-RPC 공식 홈페이지에서 JSON-RPC 2.0 스펙을 보았는데 가장 먼저 든 생각은 ‘왜 이걸 쓸까?’ 였다. 그래서 이번 포스트에서는 JSON-RPC가 어떤 것이고 어떤 장점이 있는지 정리하려고 한다.

우선 JSON-RPC가 등장한 시기를 살펴보면 2000년을 시작으로 REST 방식이 등장하고 2000년대 중순에 JSON-RPC 등장했고 마지막으로 2015년에 gRPC가 나왔다.

RPC와 gRPC에 대해서 궁금하다면 여기서 살펴볼 수 있다.

여러 글들에서 과거엔 어떤 서비스를 만들 때 가장 표준이 되었던 REST 방식을 사용했지만 최근에는 JSON-RPC도 많이 사용하고 있다고 한다. 그렇다면 JSON-RPC와 REST을 비교했을 때 JSON-RPC는 어떤 특징을 가지고 있을까?

REST vs JSON-RPC

TCP 위에서 동작한다

REST는 HTTP(S) 프로토콜 위에서 동작하는 표준인 반면에 JSON-RPC는 TCP 위에서 동작하는 표준이기 때문에 좀 더 다양한 프로토콜에서 사용할 수 있다. web3에서도 클라이언트와 서버가 웹소켓을 통해서 통신을 하는데 여기서 JSON-RPC를 사용할 수 있는 이유는 JSON-RPC가 HTTP 프로토콜뿐만아니라 TCP 위에서 동작할 수 있기 때문이다.

또한 REST는 다른 리소스를 얻기 위해서 다른 URL에 접근해야하는 반면에 JSON-RPC는 하나의 엔드포인트 URL에서 모든 요청과 응답을 받는다.

그리고 REST는 요청을 보낼 때 여러 HTTP Method를 통해서 보내는 반면에 JSON-RPC는 (HTTP의 경우) 하나의 Method를 통해서 통신하게 된다.

다양한 action을 나타낼 수 있다.

REST는 JSON-RPC에 비해 표현할 수 있는 operation의 범위가 비교적 한정적이다. 왜냐하면 REST 방식은 어떤 객체에 대해서 CRUD operation을 하는 것에 적합하게 설계되었고 반면에 JSON-RPC는 CRUD를 포함한 다양한 action을 나타내는 operation을 표현할 수 있다.

조금 추상적이니 예를 들어보자. 우리가 차를 얼마의 기간동안 렌트하려고 할 때 그 비용을 API를 통해 구하고 싶다. REST API를 설계하는 입장에서 우리는 어떤 URL을 뽑아내야할까?

몇 가지를 뽑아보면 일단 GET /car/rent 이 떠올랐을 수도 있다. 그런데 GET /car/rent 는 느낌이 이 API를 호출하면 rent라는 객체가 반환될 거 같은 느낌이다. 그리고 이후에 POST를 통해 rent를 추가할 수 있을 거 같기도 하다. POST /car/calculate_rent 가 떠올랐을 수도 있다. 하지만 POST /car/calculate_rent 는 URL이 명사로 이루어져있지 않기 때문에 RESTful 방식이 아니다.

또 다른 상황으로 주문을 검증하는 API를 만들어야한다고 생각해보자. GET /order/validate 가 그럴듯해보이지만 이는 마찬가지로 명사가 아니기 때문에 REST 방식이 아니다.

이처럼 REST 방식은 CRUD 기능을 나타내는데 적합하기 때문에 구체적인 동작을 나타내기에는 쉽지 않다.

반면에 위와 같은 API들을 JSON-RPC 방식으로 구현한다고 생각했을 때 각각의 메소드들은 다음과 같이 표현할 수 있다.

  • car.rent.calculate_fee: 차를 렌트하는데 요금을 계산하는 메소드
  • car.rent.create: 차를 렌트하는 메소드
  • order.validate: 주문에 대해서 검증하는 메소드

통일된 parameter 전달방식

REST가 JSON-RPC에 비해 번거로운 점 중 하나는 parameter를 전달하는 방법이 다양해서 이것을 핸들링할 수 있는 방법도 다양하다는 것이다.

일반적으로 GET method에 쿼리 조건을 붙이기 위해서 querystring을 붙이는 방법이 있고 POST로 데이터를 추가할 때는 보통 body에 데이터를 실어서 보낸다. PUT, DELETE에는 특정 객체의 상태를 바꾸기 위해서 URL에 id나 다른 인자를 붙여서 보낸다.

반면에 JSON-RPC는 parameter를 전달하는 방법이 한가지 밖에 없다. HTTP의 경우 body에 데이터를 싣는 방법이다. 핸들링하는 코드도 훨씬 단순해질 것이고 구조도 단순해질 수 있다.

심플하다

마지막으로 JSON-RPC에 대해서 찾아보던 중 인상깊은 글이 있어서 가져왔다.

I’ve been a big fan of REST in the past and it has many advantages over RPC on paper. You can present the client with different Content-Types, Caching, reuse of HTTP status codes, you can guide the client through the API and you can embed documentation in the API if it isn’t mostly self-explaining anyway.

But my experience has been that in practice this doesn’t hold up and instead you do a lot of unnecessary work to get everything right. Also the HTTP status codes often don’t map to your domain logic exactly and using them in your context often feels a bit forced.

But the worst thing about REST in my opinion is that you spend a lot of time to design your resources and the interactions they allow.

And whenever you do some major additions to your API you hope you find a good solution to add the new functionality and you didn’t design yourself into a corner already.

This often feels like a waste of time to me because most of the time I already have a perfectly fine and obvious idea about how to model an API as a set of remote procedure calls. … Our programs are based on calling procedures so building a good RPC client library is easy, …

https://stackoverflow.com/a/37823128/6433772

이처럼 JSON-RPC와 달리 REST에서는 한정된 operation으로 원하는 서비스를 제공해야하므로 불필요한 모델들을 추가하고 ‘어떻게 CRUD 내에서 기능을 제공할 수 있을까’에 대해서 고민해야한다. 그리고 새로운 기능을 추가할 때 그 기능에 대해서 기존의 REST API와 어떻게 하면 seamless하게 가져갈 수 있는지 고민도 필요할 것이다.

JSON-RPC는 action-based operation에 적합하기 때문에 내가 도메인 모델을 정해놓고 나면 그에 대한 행동을 묘사하는 메소드를 뽑아내는 것은 훨씬 간단한 일이다. 더불어 HTTP의 경우 HTTP에서 기본적으로 표준을 정해놓은 Status code를 RPC의 응답값과 힘들게 매핑시킬필요도 없다.

Reference

Photo by Spencer Davis on Unsplash

Kubernetes CRD와 Operator에 대해서

최근에 Kubernetes를 통해서 프로젝트를 진행했었다. Kubernetes의 기본적인 사용법과 어떻게 쓰는지는 시간이 지나면서 자연스레 익혀지지만 Kubernetes의 전체적인 철학이나 어떻게 Kubernetes가 동작하는지 원리에 대해서 개념이 머리에 명확하지 않은 것 같아서 시간을 내어 따로 정리해보려고 한다.

그 중에서도 CRD와 Operator 위주로 정리해보려고 하는데 이 두 개의 개념이 Kubernetes의 전반적인 개념과 동작 원리를 가장 잘 보여준다고 생각하기 때문이다.

Custom Resources

Kubernetes에서는 resource라는 개념이 있는데 공식문서에 따르면 resource는

Kubernetes Object를 모아놓은 Kubernetes API의 엔드포인트

라고 한다.

사실 정의에서부터 확실히 모르는 개념이 있었기 때문에 이해하기가 쉽지 않았다. 그래서 먼저 Kubernetes Object와 Kubernetes API에 대해서 먼저 정리해보려고 한다.

Kubernetes Objects

Kubernetes Objects는 공식문서에

persistent entities in the Kubernetes system

으로 표현되는데 Kubernetes는 이 object를 이용해서 다음과 같은 정보를 나타낼 수 있다.

  • application에 현재 붙어 있는 node들
  • application이 현재 이용할 수 있는 resource들
  • application이 어떻게 동작할지에 대한 policy: 간단한 예로 application이 어떻게 재시작할지 어떤 방식으로 업데이트할 것인지 fault-tolerance에 대한 정책들이 있을 수 있다.

Kubernetes의 독특한 점 중 하나인데 Kubernetes의 object는 java에서 말하는 object와는 느낌이 다르다. java에서는 object를 생성하게되면 그 순간 존재하는 것이지만 Kubernetes에서 object를 생성한다고 했을 때는 그것을 바로 생성하는 것은 아니다.

만약 사용자가 kubectl 을 통해서 pod을 1개 생성하는 요청을 보내면 Kubernetes system에서는 pod 하나가 존재하는 “상태”를 Kubernetes에 기록해놓는다. 그리고 Kubernetes 시스템은 그 상태를 맞추기 위해서 현재 상태와 사용자가 ‘원하는’ 상태를 비교하게 된다. 예를 들어 pod 한 개를 만들라는 상태를 kubernetes system에 기록해놓았는데 실제 클러스터에 pod 하나가 떠있지 않다면 Kubernetes system은 사용자가 ‘원하는’ 상태를 맞추기 위해서 실제로 pod을 클러스터에 생성할 것이다.

Object Spec and Status

모든 Kubernetes Object는 공통적으로 두 가지 필드를 가지고 있는데 specstatus가 그것이다.

  • Spec은 해당 object가 어떤 상태를 가질지에 대한 명세를 가지고 있다.
  • Status는 실제로 클러스터에 떠 있는 object가 어떤 상태를 가지고 있는지에 대한 정보를 담고 있는데 이것은 Kubernetes System에 의해서 바뀐다.

Kubernetes API

이와 같이 Kubernetes Object와 관련해서 CRUD 작업을 하기 위해서는 Kubernetes API를 통해서 해야한다. kubectl 은 사용자가 Kubernetes Object를 생성하고 싶다는 요청을 받으면 사용자 대신에 자신이 직접 Kubernetes API에 필요한 요청을 날리고 그 결과 클러스터에 해당 object가 생성되는 것이다.

kubectl 대신에 Kubernetes API 클라이언트 라이브러리를 통해서 프로그래밍적으로도 사용자가 직접 요청을 보낼 수 있다.

Custom Resources

이제 다시 정의로 넘어와보면

resource는 API object를 모아놓은 Kubernetes API의 엔드포인트

kubernetes에서 기본적으로 제공되는 pod resource를 생각해보았을 때 pod resource를 통해서 사용자는 pod과 관련해서 CRUD 를 수행할 수 있을 것이고 pod이라는 것이 어떤 내용을 담고 있는지 알 수 있을 것이다. 좀 더 추상적으로 정리하면 pod resource는 pod과 관련된 Kubernetes의 operation + pod에 대한 정의라고 볼 수 있을 거 같다.

custom resource는 Kubernetes에서 기본적으로 제공하는 resource들 이외에 사용자가 자신의 필요에 따라서 새로 정의한 resource를 말하는데 다르게 말하면 Kubernetes API를 확장하는 작업이라고 볼 수 있다. 이 부분과 관련해서는 Kubernetes에서 aggregation layer라는 개념이 있는데 이 부분은 직접 읽어보는 것이 나을 것 같아서 링크만 첨부한다.

한 번 custom resource가 특정 클러스터에 설치되면 다른 권한이 있는 유저들은 custom resource의 object를 kubectl 을 통해서 생성할 수 있다. 이것은 해당 resource에 대해서 REST API가 aggregation layer를 통해서 Kubernetes API가 확장되었기 때문에 가능한 것이라고 볼 수 있다.

Custom Controllers

Kubernetes controller는 기본적으로 유저가 ‘원하는’ 상태를 읽어서 현재 상태와 비교해 ‘원하는’ 상태로 클러스터의 싱크를 맞춰주는 역할을 한다.

Custom Controller는 어느 resource와도 같이 사용될 수 있지만 custom resource와 같이 사용될 때 가장 강력하다. Custom resource와 custom controller를 같이 사용하는 패턴을 Operator Pattern이라고 하는데 Operator pattern은 custom resource에 대해서 사용자가 원하는 상태를 유지하도록 만들어 준다.

Operator Pattern

Operator Pattern은 현실 세계에서 관리자(operator)의 컨셉을 따온 시스템이다. 현실 세계에서 관리자는 자신이 담당하고 있는 공정이나 서비스의 자세한 부분들을 속속히 알고 있고 자신이 알고 있는 지식에 따라서 각 공정이나 서비스가 어떻게 운영되어야하고 관리되야하는지를 명확하게 알고 있는 사람이다.

Kubernetes operator도 이와 비슷하다. Operator는 자신의 custom resource들을 관리할 때 application domain을 이용해서 resource들 각각이 어떻게 동작해야하고, 어떻게 배포되야하고, 문제에 대해서 어떻게 반응해야하는지 등에 대해서 정의하고 핸들링할 수 있다. 다시 말하면 operator는 application-specific한 controller라고 볼 수 있다.

operator 또다른 강점은 application-specific하기 때문에 resource에 대해서 application domain을 이용해서 부가적인 동작을 하도록 만들 수도 있다. 예시는 아래에서 소개하겠다.

operator에 대한 구체적인 예로는 다음과 같은 상황이 있을 수 있다.

  • SampleDB라는 custom resource를 사용자가 만들어서 클러스터에 생성하였다.
  • 사용자가 SampleDB object를 Kubernetes API를 통해서 클러스터에 생성했다. 그 결과 사용자가 원하는 상태가 업데이트 되었다. (예: 클러스터 내에 SampleDB application 1개가 떠있어라.)
  • SampleDB operator는 Kubernetes control plane에 쿼리하여 SampleDB가 어떤 상태로 클러스터에 존재할지를 확인한다.
  • SampleDB operator는 현재 상태와 원하는 상태를 비교해서 만약 다르다면 원하는 상태로 가기 위한 동작을 취할 것이다. 원하는 상태로 가기 위한 동작으로는 다음과 같은 것들이 있을 수 있다.
  • 현재 상태에 SampleDB가 존재하지 않아서 새로운 SampleDB application을 클러스터 내에 배포할 수 있다. 이 때 SampleDB resource를 구성하는 PersistentVolumeClaim, 실제 application을 띄우기 위한 StatefulSet, 그리고 배포 초기에 설정한 initial configuration 작업을 돌릴 Job object들을 각각 만들 것이다.
  • 현재 상태에 원하는 상태보다 SampleDB가 더 많이 존재해서 SampleDB 하나를 지울 수 있다. 이 때 operator는 삭제할 SampleDB resource의 스내배샷을 찍고 StatefulSet, Volume 등 SampleDB와 관련된 resource들을 삭제할 것이다.
  • operator가 application domain을 이용해서 부가적인 동작을 하도록 코드를 추가 할 수 있다. 예를 들면 주기적으로 SampleDB를 백업하도록 하거나 특정 SampleDB의 버전이 예전 것이라면 최신 버전이 되도록 할 수도 있다.

Kubernetes Control Plane

Kubernetes system과 사용자의 cluster 사이에서 어떤 방식으로 통신하는지에 대해서는 Kubernetes Control Plane이 정의하고 있다. Kubernetes Control Plane에는 다양한 컴포넌트들이 존재하는데 그 중엔 대표적으로 Kubernetes Master와 kubelet 프로세스가 있다.

Control Plane은 클러스터 내부에 존재하는 모든 Kubernetes Object들의 기록들을 관리하고 있으며 사용자가 ‘원하는’ 상태를 etcd에 저장하고 있다. 그 기록들을 통해 object 상태를 관리하기 위해서 control loop를 돌리고 있다. Control loop는 클러스터 내부 상태에 변화가 생길 때마다 그것을 감지하고 만약에 사용자가 원하는 상태와 현재 상태가 일치하지 않으면 그것을 맞추기 위해서 동작한다.

예를 들어 사용자가 Kubernetes API를 통해서 Deployment를 생성할 때 사용자가 원하는 Deployment state를 같이 제출한다. Kubernetes Control Plane은 이것을 기록해서 보관하고 있고 이 상태를 맞추기 위해서 Deployment spec에 제시한 application을 클러스터 내부에 배포하게 된다. 이처럼 Control Plane은 클러스터 내부에 변화가 생길 때마다 다시 사용자가 원하는 형태로 만드려고 한다.

Reference

  • https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#understanding-kubernetes-objects
  • https://kubernetes.io/docs/reference/using-api/api-overview/
  • https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/
  • https://coreos.com/blog/introducing-operators.html
  • https://kubernetes.io/docs/concepts/#kubernetes-control-plane
  • https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
  • https://admiralty.io/blog/kubernetes-custom-resource-controller-and-operator-development-tools/
  • https://medium.com/faun/writing-your-first-kubernetes-operator-8f3df4453234
Photo by Hayden Scott on Unsplash

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 algorithm overview

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

fabric-logo

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/

Tips for OpenSource Project – Git

Recently I’m working on a opensource project: it-chain/engine. I’ve participate in this project as collaborator early in the year. For about 6 month, there’re lots of hard situation to work with other developers. And also learned about how can I work with others not making awkward situation. In addition I want to share the tips to work on opensource project and how to use git, not basic tutorial but more about practices which fit into opensource project.

Work on Smaller Feature

I think this is coincide with the principle of Seperation of Concerns(SoC) Principle in software design. And this is what I utmost want to emphasize in this post. Let’s assume that you are working on food delivery service project with others. And one developer on your team issued on github that “Now I’m going to fix whole delivery system” and let’s assume that these delivery system code affects on all other sub-servcies code, like UserManagement, StockManagement. Then whenever other developers are trying to fix those sub-services, the codes will broken because the developer who works on whole delivery system will mass up whole of your codes. And I think this is one of most irritating situation. Works on smaller feature first which do not affact on others parts, then integrate with others.

Issue on github which feature I’m going to working on

Two different developers may work on same part. And at the point of pull request, the other developer who is not pull requested may panic. So this is basic but important manner to working with other in opensource project. Issue what’s I’m going to working on and tag the other developers to notify.

But code should be large enough to express functional unit

For example, someone pull requested with this single struct

We don’t know what is this command struct is. And where this struct are going to used. It might be better to pull request with ConfirmBlockHandler function which actually use this struct. And other can see that where this struct are going to used in the view of whole system.

Tips on Using Git

git pull -r

Usually in personal project we just pull the code. But just pull the code actually dirties your git tree if that project grow.

And there’s another advantage by using -r option. For easily rebase, pull request should have least number of commit. This convention helps each pull request have 1 commit and this also helps each pull request have smaller feature which helps other feedback your codes and also each team member can work well by not interfered with the codes that pull requested.

git commit --amend

Our team makes conventions one of which is not to make unnecessary commit and this can be the cause of reject of pull request.
To remove unnecessary commits, git commit --amend is the way. Let’s assume that you made a commit with message “add command handler”, then you find out that you forgot to add some pieces of codes, of course by mistake. But this fixes are too small to make another commit. Then you can use --amend options. You just need to type git commit --amend then the codes that you commited and the codes that you fixes are now in one commit with message “add command handler”.

git reset --soft HEAD~<NUM>

You got a feedback from pull request that you may need to combine two commits into one that you pull requested. Because other can think the second commit is unnecessary. Then you can use git reset in the local.

Let’s this is the commits you made. In this situation, you need to combine Unnecessary commit into Some commit. Then you can use git reset --soft HEAD~1, with this you reset the recent one commit and those codes you made in Unnecessary commit are unstaged. Your team feedbacks you combine two commits so now you git add . and git commit --amend. Then your commits looks like this.

How to Write Go Testing

Intro

In making your application with code, it is strongly recommended to write test cases. Because it can make sure that your whole bunch of codes work correctly and for your mind health. Recently I have a chance to work in great project named it-chain-Engine with golang. In shortly this project is about lightweight and completely customizable blockchain engine.

Completely customizable, because this project is implemented with DDD & event sourcing way which can separate every component with bounded context and each component is composed with lots of layers. And to make sure those layers as well as micro components which consists of layer are interact well, writing test case is best. So how can we write go testing? There are some ways and I’ll post some tips with my experience.

Use the “underscore test” package

Those codes are almost same. Then what is difference? What is good thing when writing “underscore test” package name?

One thing is we can write test cases like when we really use that codes. When we use BlockPoolModel in other package, we write codes like blockchain.NewBlockPool(), not NewBlockPool(). This provides us the real experience as we actually write some codes.

Also there is another advantage that provide exception which golang files in the same directory belong to the same package. In other words, you can put block_pool.go and block_pool_test.go in the same directory named blockchain with different package name blockchain and blockchain_test.

Test Helpers

When testing, we may need to setup test fixture like connecting to db, mq, providing mock datas. So we are writing that codes over and over although actual testing codes really short. You may can think ‘How about refactor those codes’ like JUnit @Before or mocha before method. We can implement it using defer.

Here before writing actual test codes you can setup fixture, in this case executing cmd commands works as JUnit’s @Before. And in SetupTest you can pass testing.T, so you can also test whether our test fixtures are setup well. Also as you can see SetupTest returns function. In returned function you can clean up fixture like close db connection, remove files and so on. By refactor setup function you can increase readability of your test codes and can see what is going on in those test cases at once. And the best thing is you can save your effort to write boring setup codes.

Table Driven Tests

How about testing about several cases with only one setting? You may can take into account Table Driven Tests. Let’s start with simple example and see what is it.

We can see fibTests slice (table) which provides test input and output. And every iteration you can test with given input, output. Advantage of this approach is you can test lot of cases with one actual test case codes and there’s no need much effort to add test cases, just add one row of table. Let’s see another one.

This is rather complicate example. First, used map instead of slice. As you can see key is used as test case description and by using value as struct you can explicitly indicate what is input, output and error. Below the table is injecting mock object to command handler which is what you are going to test. Mocking will be explained in next section. Finally in the for loop, there’s actual test codes commandHandler.HandleConfirmeBlockCommand(test.input.command).

Downsides: readability

This is about Table Driven Tests. And yes, there’s advantages with this technique but also exists downside. The point is readability, as you may noticed in second example, table’s input, output, error row can be large in real-world testing and more importantly you can’t know what is going to do with these huge inputs. You can find out where all of these inputs are going to use at the point of actual test codes.

Using Mock

Let’s assume that we want to test HandleTxCreatedEvent function. This function take txCreatedEvent as parameters and from this event we get target transaction to save. And with this transaction we save it to TransactionRepository.

But we do not know how TransactionRepository is implemented. So TransactionRepository may can affect to our test results. We should take control of txRepository.Save so that we can make sure the cause of failure of tests are not from TransactionRepository. And this is where Mock is come into play. We can see constructor receives TransactionRepository and create EventHandler object. And instead of injecting real TransactionRepository we could inject our own mocking TransactionRepository. As a result t.txRepository.Save(tx) could trigger our own defining function which helps tests.

With mocking, test goes like this. We create MockTransactionRepository struct which implements TransactionRepository so that we could inject this mock into our testing eventHandler. Next is declare functions we need to control, in this case SaveFunc which is triggered when MockTransactionRepository.Save is called. And those functions are defined inside test cases for our own tastes, we could assert about parameters and could return specific errors for some case. Finally inject those mocking object into our testing object: eventHandler.

Downsides: too many mocks

This methodolgy surly has downsides. We could think of testing object but that object receive too many parameters in constructor and for take control of those parameter object we should make all of them as mock, also implement mock object’s functions. So for one easy test case, we may need to write hundreds of codes for mocking object and this is super waste, and we are lazy.

But we need to think about this first! Those objects which need many mocking object may have too many responsibilities and this is not good design. We should think about separating those responsibility by separating object and this is about ‘Single Responsibility Principle’.

And the other way to solve this problem is redeclaring interface. We may not need all of the functions declared in TransactionRepository. We may only need write functions for this interface. So we could fix like this.

By creating WriteOnlyTransactionRepository, not only there’s just one function to implement in mock object but this could see that this repository only works for writing things at once and also separate responsibility. I think this is better design.

fabric-logo

Hyperledger Fabric Configure Network – byfn 뜯어보기

Intro

Hyperledger Fabric docs를 읽다보면 가장 처음 접하게 되는 것이 byfn 튜토리얼이다. 처음에 개념적인 부분과 실제 네트워크의 흐름을 맞춰보는데 정말 도움이 되는 것 같다. 그런데 정말 찬찬히 뜯어봐야한다. 하나씩 ‘이건 왜 이렇게 동작하는거지?’, ‘이건 왜 안되는거지?’ 하면서 Fabric의 개념적인 부분하고 연결시키면서 생각하면 어느순간 전체적인 그림이 그려지게 된다.

처음에 sample을 받고, develop 혹은 연습하는데 도움이 되는 바이너리 파일을 받게 된다. 바이너리 파일에는 cryptogen, configtxlator, peer 등 우리가 네트워크를 쉽게 구성할 수 있게 도움을 줄 수 있는 프로그램들이 있다.

전체 디렉토리 구조이다. 빠진 파일들도 있지만 일단 우리에게 필요한 것은 이 정도이다. 간단하게 각각을 살펴보면 다음과 같다.

  • cryptogen: 간단하게 네트워크 구성원들에게 certificates들을 발급해준다. production에서는 사용하지 않는 것이 좋다. 대신 CA에서 받아야한다.
  • configtxlator: protobuf와 json 변환 및 파싱을 도와준다.
  • byfn.sh: Hyperledger Fabric에서 만들어준 sample 스크립트이다. 이번 포스팅에서는 이 파일을 하나하나 다 뜯어 볼 것이다.
  • configtx.yaml: 네트워크의 channel과 genesis block을 만들고 anchor peer를 설정한다. 파일 이름에서 유추할 수 있듯이 네트워크 전체의 설정 내용을 담고 있다.
  • crypto-config.yaml: cryptogen이 이 파일을 사용한다. 이 파일을 이용해서 organization와 그 구성원들에게 각각의 certificate을 발급한다. 그래서 각각의 organization들이 독자적인 CA를 가지고 있는 것처럼 보이게 할 수 있다.
  • docker-compose-cli.yaml, docker-compose-base.yaml: 전체 네트워크 노드들의 docker-compose 설정들이다.

튜토리얼에서는 ./byfn.sh -m generate ./byfn.sh -m up으로 전체적인 네트워크 구성이 시작되고 완성된다. 우리는 byfn.sh에서 어떤 일이 일어나는지 알아보고 싶다.

Generate Crypto Artifacts, Channel Configuration

시작 부분이다. generate 에서는 generateCerts, replacePrivateKey, generateChannelArtifacts 가 일어난다.

Create Certificates Using Cryptogen

generateCerts 부분이다. 여기서는 cryptogen 을 이용해서 네트워크의 ceritificates를 만들게 된다. cryptogen 의 사용법은 다음 링크에 자세히 나와있다.(사용법) cryptogen으로 만든 crypto artifacts들은 crypto-config에서 확인할 수 있다. 펼쳐보면 확인할 수 있겠지만 ordererOrganizations, peerOragnizations 디렉토리로 나눠져있고, 각각은 orderer의 certificate, peer들의 certificate들이 담겨져있다.

여기서 잠깐 지금 포스팅을 하는 이유가 나온다. replacePrivateKey 를 보면 알겠지만 organization domain이 hardcoding 되어있다. 그렇기 때문에 우리는 이 스크립트를 이용하는 것이 아니라, 이해한 뒤에 우리가 필요한 부분에 대해서 스크립트를 다시 짜야한다.

이 부분은 Fabric과 관련은 없는 부분이다. 단순히 docker-compose template파일을 복사한 뒤에 이전에 우리가 cryptogen으로부터 받은 certifcate들을 리눅스의 sed 명령어를 이용해 compose 파일 적절한 부분에 바꿔넣기 하고 있다.

Generate Channel Config Transaction

Channel configuration들을 만드는 부분이다. 이 부분은 echo 부분이 이해하는데 도움이 되어서 남겨놓았다.

configtxgen -profile TwoOrgsOrdererGenesis -outputBlock ./channel-artifacts/genesis.block

먼저 configtxgen 을 이용해서 Genesis block을 만든다. genesis block의 설정과 관련된 부분은 configtx.yaml 파일의 Profiles 에 명시되어있다.

TwoOrgsOrdererGenesis 를 보면 genesis block의 설정을 알 수 있다. 네트워크의 Orderer를 설정하고 Consortiums 섹션에서 consortium 이름과 거기에 속한 organization들을 설정할 수 있다. (configtx.yaml 더 자세한 설명 보기)

마찬가지로

으로 channel configuration 파일을 만든다.

여기서는 각 organization의 anchor peer들의 정보를 transaction으로 만든다.

여기서 만들어진 네 파일들은 모두 channel을 만드는데 설정파일같은 역할을 하게 된다. 네이밍에서 알 수 있지만 모두 .block, .tx로 끝나는 것을 볼 수 있다. 각각은 모두 block이거나 transaction들로 channel이 만들어지고 그 channel의 peer들의 ledger에 모두 기록되게 된다.

지금까지 우리가 만든 것을 정리하면 다음과 같다.

  • Orderer, Peer certificates
  • Genesis Block
  • Channel config transaction
  • Anchor peer config transaction for each organization

이제 channel을 만들기 위한 준비는 끝났고, 노드들을 띄워서 channel을 만들고 네트워크를 형성할 순서이다.

Build Network

network를 띄우는데 두 가지 옵션이 있다. state database로 default인 leveldb를 쓸 것이냐 couchdb를 쓸 것이냐가 그것인데 일단은 leveldb를 쓰기로 한다.(바꾸는 것은 정말 어렵지 않다.) 그리고 지금 단계부터는 스크립트를 돌리지 말고 직접 docker에 들어가서 놀아보는 것을 권장한다.

docker-compose -f docker-compose-cli.yaml up 으로 노드들을 띄운다. 여기 -d 옵션을 빼고 실행해서 직접 로그들을 살펴보는 것이 좋다. 또 cli docker container에서 script.sh 을 실행시키는데 이 부분도 직접 container에서 돌려보는 것이 좋다.

더 나아가기 전에 우리가 주로 놀 곳이 cli 이기때문에 docker-compose-cli.yaml 에서 cli 가 어떻게 생겼는지부터 살펴보자. 그리고 혹시 docker나 docker-compose에 익숙하지 않은 사람들은 간단하게나마 어떤 것인지, 무엇을 하는 것인지 이해하고 보면 더 좋을 것 같다.

여기서 우리가 살펴볼 부분은 environment 부분들이다. 앞으로 cli에서 우리는 environment variable들을 바꾸면서 다른 organization, 다른 peer들로 옮겨다닐 것이다. 현재 CORE_PEER_ADDRESS=peer0.org1.example.com:7051 로 되어있고 volumes 에서 crypto-config 가 mount 되어있으므로 environment variable을 바꾸지 않는 이상 cli는 peer0.org1처럼 행동하게 된다. 또한 volumes 에서 chancode, crypto-config, scripts, channel-artifacts 가 mount 되어 있는데 이 덕분에 나중에 cli에서 chaincode install과 channel을 만들거나 join할 수 있다.

그럼 이제 다른 터미널을 띄워서 cli로 들어가보자.

docker exec -it cli bash

Create Channel

먼저 channel을 만들기 전에 transaction을 제출할 Orderer의 certificate을 환경변수로 설정해주어야한다. 그리고 설명의 편의를 위해 channel이름도 ‘mychannel’이란 이름으로 환경변수에 넣어주자.

설정을 마쳤으면 아까 만들어놓은 channel config transaction을 Orderer에게 제출하자.

그러면 Received block: 0을 통해 mychannel channel가 성공적으로 생성되었고, mychannel.block 이라는 genesis block을 받았음을 알 수 있다.

docker-compose 의 로그를 통해서도 성공적으로 orderer에 transaction이 제출되었음을 확인할 수 있다.

Join Organizations to Channel

이제 우리는 성공적으로 channel을 생성했다. 이제 남은 일은 org1.peer1과 org2.peer0, org2.peer1들을 channel에 참여시키는 것이다. 아까도 말했지만 cli docker container는 crypto-config를 mount해놓았기 때문에 환경변수를 다른 peer로 바꾸는 것만으로도 그 peer처럼 행동할 수 있다. 그럼 먼저 org1.peer1으로 바꿔보자.

그리고 mychannel 에 join 요청을 보내자. Hyperledger Fabric에서는 channel에 join 요청을 할 때는 channel의 Genesis block을 가지고 요청을 보낸다. 그런데 우리는 아까 Genesis block을 만들어 놓았다. 그러면 이것을 가지고 join 요청을 보내보자.

성공적으로 join 했음을 확인할 수 있다. 마찬가지 방법으로 peer0.org2, peer1.org2도 channel에 join하면 된다. 이제 남은 일은 각 organization에서 anchor peer를 업데이트시키면 네트워크 구성이 마무리된다. Anchor peer의 설정파일은 아까 만들어둔 Org1MSPanchors.tx, Org2MSPanchors.tx 를 이용해서 업데이트하면 된다.

성공적으로 업데이트 시켰음을 확인할 수 있다.

Conclusion

Hyperledger Fabric이 어떤 식으로 돌아가는지 확인하고 싶다면, sample로 주어진 shell script를 찬찬히 뜯어보라고 권하고 싶다. 이번 포스팅에서 cryptogen으로 organization과 orderer의 certificate을 만들고, configtxgen으로 channel config transaction을 만든 다음, channel 하나에 두 개의 organization이 있는 네트워크를 만들었다. Channel join은 해당 channel의 genesis block을 가지고 join하게 된다.

다음 시리즈에서는 이제 이 네트워크를 확장하고 싶다면? 어떻게 하면 좋을 지에 대해서 얘기해보겠다.

Hyperledger Fabric Configure Network – Network Overview

Hyperledger Fabric 네트워크 구축을 자유롭게 할 필요가 있어서 한 주간 찾으면서 공부했다. 공부해본 결과, 자료가 정말 없다. 이론적인 문서는 많은데 그것을 바탕으로 실제 네트워크를 구성하는데 레퍼런스가 될만한 것들이 정말 없었다. 그래도 고생하고 고생하며 노력한 끝에 이제 왠만큼 네트워크를 꾸릴 수 있게 되었다. 배운 것들을 정리한다는 생각으로 글을 써보기로 했다. 누군가에게 도움이 된다면 정말 더할나위 없을 것 같다.

다음과 같은 포스트를 작성할 예정이다.

  1. Network Overview
  2. byfn(Build Your First Network) 뜯어보기
  3. eyfn(Extend Your First Network) – Add New Org to Network
  4. Upgrade orderer from SOLO to Kafka
  5. No cryptogen!

Components of a Network

Hyperledger Fabric 네트워크는 다음과 같은 컴포넌트들로 구성된다.

  • Ledger (channel당 하나씩 있다. 그리고 blockchain과 state database로 구성된다.)
  • Smart Contract (chaincode)
  • Organizations, MSP
  • Peer nodes
  • Ordering services
  • Channel
  • Fabric CA

Transaction Flow

Overview

Transaction Proposal

각각의 컴포넌트들의 역할을 전체적인 흐름에서 파악하는 것이 더 이해하기 쉽다. 먼저 Client는 Application SDK에서 transaction proposal을 Endorsing Peer에게 날리게 된다. 어떤 Peer들이 endorser들이 되는지와 transaction이 valid한지 기준은 Endorsement Policy에 의해 결정된다. SmartContract의 역할을 하는 chaincode는 endorsement policy를 정의하고있다. Policy에 정의된 endorse peer들에게 Client는 무조건 transaction proposal을 보내야하며 그들의 endorse 여부를 모아야한다.

Proposal을 받은 Endorsing Peer들은 transaction을 ledger을 업데이트 시키지않고 chaincode 실제로 돌리면서 시뮬레이션해본다. 그리고 각 Endorsing Peer들은 시뮬레이션 결과를 RW Set 형태로 나오는데 이것을 endorse 할 지 reject 할 지의 응답과 함께 Application에 돌려준다. RW Set은 transaction을 시뮬레이션 하기 이전의 world state의 상태와 시뮬레이션 했을 때 그 결과 상태 두 가지 set을 말한다.

Submit to Ordering Service

만약 transaction이 endorse되었다면 ordering service에 보내게 된다. Ordering Service는 쉽게 생각하면 endorsed transaction queue라고 보면된다. 실제로는 여러 client들이 transaction 날리는데 이것을 하나로 queueing 해주는 것이다. 현재 Hyperledger Fabric에선 SOLO, Kafka 두 가지 방식을 지원한다. SOLO 방식은 production에서는 사용하지 말라고 doc에 명시하고 있다. 왜냐하면 ordering service node가 한 명이고 fault-tolerant하지 않다.

Broadcast Block

그리고 여기서 모아진 transaction들을 block으로 만들어서 anchor peer들에게 보내준다. Anchor peer들만 organization 내에서 block을 받을 수 있다. Anchor peer가 organization 내에 peer들에게 block들을 broadcast해준다. 마지막으로 block을 받은 peer들은 block validation을 거치고 나면 ledger에 저장하고 world state를 업데이트 시킨다. 그리고 transaction 성공여부를 client에게 알려준다.

Conclusion

그래서 위와 같은 전체적인 과정을 그려보면 다음과 같은 순서로 진행이 된다. Company에서 client를 이용해서 transaction을 날리면 바로 ledger에 저장하는 것이 아니라, 우선 endorse peer에게 endorse response를 수합해서 그것이 valid 하면 비로소 block으로 만들어질 수 있다. 또한 실제에서는 Company가 여러개이고, endorsed transaction들도 여러개가 날아올테니 그것을 ordering service를 이용해서 queueing하고 block으로 만들어서 전파하게된다. 다음 포스트에는 byfn 튜토리얼을 뜯어보면서 과정 하나하나가 어떤 의미인지 포스팅해보려고 한다.

Reference

  • https://medium.com/coinmonks/how-does-hyperledger-fabric-works-cdb68e6066f5
  • http://hyperledger-fabric.readthedocs.io/en/release-1.1/arch-deep-dive.html
  • https://hyperledger-fabric.readthedocs.io/en/master/network/network.html#

How Java Pass Arguments

프로그래밍을 하면서 항상 method를 사용하고 거기에 관련된 arguments를 넘긴다. 그런데 어떻게 다른 method에 값들이 넘어가는지 메커니즘을 정확히 알지 못하면 가끔씩 미묘한 버그가 발생하기도 한다. 오늘 할 이야기는 Java에서 어떻게 arguments를 method로 넘기는지에 대해서다. Java를 공부해봤다면 한번 쯤 들어봤을 것 같다. 그리고 그것을 공부하면서 ‘음.. 그렇구나. 이해했으니 넘어가자’ 그러고 넘어갔다. 그리고 실제로 이해했다고 생각한 개념들에 대해서 제대로 된 깨달음을 얻을 때는 그와 관련된 버그때문에 고생하다 간신히 고쳤을 때일 것 같다!

문제가 발생했던 때는 Dijkstra’s Shortest Path Algorithm을 이용해서 Flight Scheduler를 만드는 중이었다. 나에게는 Flights들이 주어지고 그것을 이용해서 승객들의 출발지 공항부터 도착지 공항까지 최단 시간의 루트를 제공해주는 프로그램이다.

다음은 코드 중 일부이다. Shortest path를 가져온 뒤 Fringe Edge를 업데이트를 해주는 부분이다. 마지막으로 Edge weight를 업데이트한 뒤 Min-Heap에 집어 넣어준다. (이때 Min-Heap은 minutes을 기준으로 heapify된다.)

이렇게 Dijkstra’s Algorithm을 제대로 구현하고 동작시키면, 잘못된 경로가 나온다.

한참을 고생하다 Heap을 뜯어보았다. 그리고 문제의 실마리를 찾았다.

다음 결과에서 i=1의 element를 보고 머리가 번뜩였다. 두 element가 같은 주소를 가리키고 있구나!

Distance d = distanceMap.get(destination);에서 d는 새로운 주소에 변수를 할당받는 것이 아니라 Map에 있던 Distance를 가리키고 있는 것이다.

In Java, arguments are passed by value

먼저 Pass by value가 무엇일까. Pass by value는 어떤 arguments들이 method로 전달될 때 arguments들의 복사본이 전달되는 것이다. 그렇기 때문에 method 안에서 전달된 arguments들의 값을 아무리 바꾸어도 method 바깥의 variables들의 값은 그대로다.(복사한 것들을 method 안으로 넘겼기 때문에!)

이야기를 더 진행하기 전에 Java에서 arguments들이 어떻게 memory에 저장되는지에 대해서 알아보자. 먼저 Java에는 두 가지 종류의 변수 종류가 있다: primitivesobjects이다.

Primitive variables는 항상 stack에 저장된다. 하지만 Object의 경우 두 단계를 거쳐서 저장된다. 먼저 object의 실제 데이터가 Heap 영역에 저장되고 그 reference(pointer, Heap 영역의 주소)가 stack에 저장된다.

그렇다면 Java에서는 어떤 방식으로 arguments들이 전달될까?

Java에서는 항상 arguments들은 pass by value 형식으로 전달된다. 그래서 method를 호출할 때마다 다음과 같은 과정이 반복된다.

  • argument의 복사본들이 stack에 생성되고 그것의 복사본들을 method로 넘겨준다.
    • 만약에 argument가 primitive type이었다면, 단순하게 복사본을 stack에 생성하고 그것을 method안으로 넘겨준다.
    • 만약에 argument가 object type이라면, 그 reference(pointer, Heap 영역의 주소)를 stack에 저장하고 그것을 method에게 넘겨준다. 그렇기 때문에 method를 호출하기 전 argument로 넘어가는 variable과 method를 호출하면서 method로 넘어가는 variable은 같은 object data를 가리키고 있다.

two objects pointing to same heap address

그렇기 때문에 아까 발생한 문제는 위와 같은 그림으로 나타낼 수 있다. Map에 있는 Distance object와 get으로 얻은 Distance reference는 같은 Heap 영역을 가리키고 있다. 그래서 하나의 값을 바꾸면 다른 값도 변하게 된다.

그렇다면 어떻게 문제를 해결할 수 있을까? 바로 new를 이용해서 Heap영역에 새 메모리를 할당받아서 그곳을 가리키는 reference를 이용하면 될 것이다.

Conclusion

Java는 pass-by-value로 argument가 method에 전달된다. 또한 그 argument가 Object인 경우, 실제 data는 Heap영역에 저장한 후 address를 stack에 저장한다. 따라서 Object를 argument로 전달할 때는 data의 address를 전달하는 것이다.

Reference

  • https://www.programmergate.com/java-pass-reference-pass-value/