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/