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.

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.

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/