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 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/

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