[아키텍처] 마이크로서비스 아키텍처 패턴

헥사고날 아키텍처

레이어드(계층화)(layered) 아키텍처에서는 레이어(레이어와 계층이라는 용어를 구분하기도 하지만 여기서 레이어라는 용어를 사용한다) 간 종속성이 상위 레이어에서 하위 레이어를 향하는 단방향이다. 표현(프레젠테이션) 레이어는 비즈니스(서비스) 레이어에 의존하며, 비즈니스 레이어는 영속성(데이터 액세스) 레이어에 의존한다. 데이터를 처리하는 영속성 레이어는 데이터베이스와 같은 인프라 기술에 의존한다. 상위 레이어는 하위 레이어와 상호작용할 수 있지만 그 반대는 아니다. 도메인 주도 설계인 경우 레이어의 종류와 개수는 달라질 수 있지만 상위 레이어가 하위 레이어에 의존하여 상호작용하는 것은 동일하다.

레이어드 아키텍처는 상위 레이어가 하위 레이어에 의존하므로 레이어 간 의존성 방향이 단방향이다. 도메인 주도 설계의 경우 비즈니스 레이어인 도메인 레이어가 인프라 레이어에 의존하므로 인프라 기술이 변경되면 도메인이 변경에 따른 영향을 받는다는 문제가 있다. 이러한 의존 문제를 해결하기 위해 인터페이스를 정의함으로써 구체적인 클래스에 의존하지 않도록 하여 레이어 간 결합을 약하게 만들고 의존성 방향을 저수준에서 고수준으로 역전시킬 수 있다.

모놀리식 아키텍처를 대신하여 마이크로서비스 아키텍처를 도입하면 데이터 저장 위치가 도메인 또는 서비스 별로 분리되고, 서비스를 독립적으로 개발할 수 있는 특징이 있다. 이 경우 한 서비스가 필요한 데이터를 조회하고 조합하기 위해 여러 다른 서비스와 상호작용해야 한다. 모놀리식 아키텍처를 적용한 시스템은 데이터 저장 및 조회를 위해 데이터베이스와 같이 데이터 처리에 특화된 전용 서비스를 이용한다면, 마이크로서비스 아키텍처에서는 데이터베이스 외에도 여러 다른 도메인 서비스와 상호작용하는 형태가 된다.

레이어드 아키텍처에서 애플리케이션이 데이터 영속화를 위해 단일 기술(데이터베이스)만 사용한다면 인터페이스를 통해 의존성 역전 윈칙(DIP)을 적용함으로써 시스템을 좀 더 유연하게 만들 수 있다. 하지만 애플리케이션의 구조가 복잡해지면서 외부 요청을 처리하고 데이터를 저장 및 조회하기 위한 여러 방법이 필요한 상황이 되면 레이어드 아키텍처의 한계가 존재한다. 애플리케이션에게 요청을 보내는 외부 클라이언트 시스템과, 애플리케이션이 데이터 저장 및 조회 뿐만 아니라 UI, 인증, 이벤트 기반 메시지 처리, 로깅과 같은 다양한 기능을 위해 요청을 보내는 외부 시스템 및 관련 컴포넌트의 유형이 다양해지면서 레이어드 아키텍처에서 정의한 레이어 보다 더 많은 형태의 레이어 및 인터페이스를 정의할 필요성이 생기게 된다.

헥사고날(hexagonal) 아키텍처에서는 비즈니스 로직 영역을 애플리케이션의 핵심적인 코어 영역(레이어)으로 바라보며, 애플리케이션의 동작을 위한 모든 요청 및 응답과 관련한 데이터 흐름은 이 코어 영역을 중심으로 발생한다. 헥사고날 아키텍처에서 애플리케이션은 비즈니스 로직을 표현하는 고수준의 내부 영역과 인터페이스 처리를 담당하는 저수준의 외부 영역으로 나뉜다. 이때, 비즈니스 로직이 포함된 비즈니스 영역(서비스 레이어)를 외부 영역과 분리하고, 비즈니스 영역은 외부 영역에 의존하지 않도록 한다. 대신 외부 영역이 비즈니스 영역에 의존하도록 한다. 외부 영역은 내부 영역에 정의된 인터페이스를 따르도록 하여 의존성 방향이 내부 영역을 향하도록 강제함으로써 요구사항에 의해 애플리케이션의 비즈니스가 변경되더라도 외부 영역은 영향을 받지 않는다. 반대로 외부 영역의 완전한 교체가 일어나더라도 내부 영역은 영향을 받지 않는다.

헥사고날 아키텍처는 포트 앤 어댑터(ports and adapters) 아키텍처라고도 한다. 포트(port)란 내부 영역으로의 진입점이며 내부 영역의 기능에 대한 인터페이스이다. 내부 영역은 순수한 비즈니스 로직을 표현하는 기술 독립적인 영역이며, 외부 영역과 연계되는 인터페이스인 포트를 가지고 있다. 어댑터(adapter)란 애플리케이션과 애플리케이션에 요청을 보내는(인바운드) 외부 서비스 간 연결점, 또는 애플리케이션과 애플리케이션이 요청을 보내는(아웃바운드) 외부 서비스 간 연결점이다. 어댑터는 서비스 간 통신을 위한 특정 기술(REST API, 메시지 큐, 데이터베이스 쿼리 등)을 사용하여 특정 포트를 통해 내부 영역과 상호 작용한다. 어댑터는 외부 영역에 존재한다. 외부 영역의 어댑터는 인터페이스인 포트를 통해 내부 영역의 기능을 사용한다. 이때 인터페이스인 포트를 사용하여 고수준의 내부 영역이 저수준의 외부 영역의 구체 어댑터에 의존하지 않도록 한다. 고수준의 내부 영역이 저수준의 외부 영역에 의존하지 않도록 하기 위해 내부 영역에 포트를 구성한다. 즉, 외부 영역의 어댑터가 내부 영역의 포트에 의존하도록 한다.

이와 같이 헥사고날 아키텍처에서는 포트(인터페이스)가 어댑터에 의존하는 것이 아닌, 어댑터가 포트(인터페이스)에 의존하도록 한다. 이는 의존성 역전 원칙과 관련이 있다. 인터페이스를 통한 의존성 역전을 달성하기 위해 어댑터는 인터페이스 구현체가 아닌 인터페이스에 의존하도록 한다. 이렇게 함으로써 소스 코드 의존성의 방향을 추상적인 방향으로 향하도록 할 수 있다.

포트는 내부 영역에 위치시키고 어댑터는 외부 영역에 위치시킨다. 비즈니스 로직이 담긴 내부 영역에 포트를 위치시키고, 외부 영역의 어댑터가 포트를 호출하도록(어댑터가 포트에 의존하도록) 함으로써 의존성 역전 원칙을 통해 의존성 방향을 저수준에서 고수준으로 향하도록 한다. 내부 영역의 비즈니스 로직은 포트를 통해 외부 영역으로부터 요청을 받거나 외부 영역으로 요청을 보낸다.

어댑터는 외부에서 들어오는 요청을 처리하는 인바운드(inbound) 어댑터와 내부 영역의 비즈니스 로직에 의해 호출되어 외부와 연계되는 아웃바운드(outbound) 어댑터로 구성된다. 인바운드 어댑터는 외부로부터 요청을 받아 인터페이스인 인바운드 포트에 연결해준다. 외부로부터 요청이 들어오면 인바운드 어댑터는 해당 요청을 받은 후 인바운드 포트를 호출하여 내부 영역에 전달한다. 인바운드 어댑터의 예는 다음과 같다.

  • 인바운드 어댑터의 예: REST API 컨트롤러, MVC 컨트롤러, 커맨드 핸들러, 이벤트 메시지 구독 핸들러 등

아웃바운드 어댑터는 내부로부터 받은 요청을 외부로 보낸다. 내부 비즈니스 로직이 외부에 요청을 보내면 아웃바운드 어댑터는 해당 요청을 받은 후 아웃바운드 포트를 호출하여 외부 서비스에 전달한다. 아웃바운드 어댑터의 예는 다음과 같다.

  • 아웃바운드 어댑터의 예: 데이터 액세스 객체(DAO), 이벤트 메시지 발행 객체, 외부 서비스 호출 프록시 객체

포트는 인바운드 포트와 아웃바운드 포트로 구분된다. 인바운드 포트는 외부 영역이 내부 영역의 비즈니스 로직을 사용하기 위해 외부 영역으로 표출되는 인터페이스이며 외부 영역의 인바운드 어댑터가 호출한다. 아웃바운드 포트는 내부 영역이 외부 영역을 호출하기 위해 외부 영역으로 표출되는 인터페이스이며 외부 영역의 아웃바운드 어댑터가 호출한다.

내부 영역의 개발은 도메인 모델 객체 정의, 서비스 구현 객체 정의 순으로 수행한다. 도메인 모델 객체는 해당 도메인에 대한 데이터(속성)와 기능(메서드)이 정의된 객체이다. 도메인 별 책임 및 역할을 도메인 모델 객체에 부여하여 도메인의 핵심적인 비즈니스 로직은 도메인 모델 객체에 포함시키고 외부 서비스로부터 요청을 받거나 외부 서비스로 요청을 보내는 역할은 도메인 모델 객체가 아닌 서비스 구현 객체에서 수행한다. 서비스 구현 객체는 외부 서비스와 상호작용하며 출입되는 데이터를 도메인 모델 객체에 전달하고 도메인 모델 객체에 정의된 기능을 호출함으로써 핵심적인 비즈니스 로직을 수행한다.

비즈니스 로직이 담긴 내부 영역은 다음과 같이 외부 영역과 연계된다. 데이터 저장 및 조회 처리를 위한 외부 서비스 호출 시 내부 영역의 서비스 구현 객체에 인터페이스 타입으로 선언된 아웃바운드 어댑터의 구현체에 대한 객체 주입 후 아웃바운드 어댑터를 호출한다. 아웃바운드 어댑터는 내부 영역의 인터페이스에 의존하므로 결국 아웃바운드 포트에 의존한다. 외부로부터 들어오는 요청 처리 시 내부 영역의 서비스 구현 객체에 인터페이스 타입으로 선언된 인바운드 어댑터의 구현체에 대한 객체 주입 후 인바운드 어댑터를 호출한다. 인바운드 어댑터는 내부 영역에 주입된 인터페이스에 의존하므로 결국 인바운드 포트에 의존한다. 위와 같이 포트와 어댑터를 연결함으로써 내부 영역이 외부 서비스로부터의 인바운드 요청을 받고 외부 서비스로 아웃바운드 요청을 보낼 때 객체 간 의존성의 방향은 항상 내부를 향하도록 유지할 수 있다.


도메인 영역과 인프라 영역 간 인터페이스

도메인 서비스 구현체가 하나의 리포지토리 구현체만 사용하는 경우 리포지토리 인터페이스를 사용하여 특정 데이터 영속 기술에 의존하지 않는 코드 구현이 가능하다. 그러나 여러 데이터 영속 기술로부터 여러 도메인의 데이터를 조회한 후 이러한 데이터를 조합하여 도메인 모델을 구성하는 코드 구현이 필요한 경우 이러한 역할을 하는 별도의 클래스를 내부 영역에 위치시킬 필요성이 생기게 된다. 이때 각 도메인 구현체 별로 리포지토리 구현체를 생성한다.

내부 영역에 위치한 도메인 서비스 구현체는 도메인 모델을 구성하기 위한 데이터를 조회하는 리포지토리 구현체(아웃바운드 어댑터)의 종류와 호출 순서(예: 데이터베이스 조회 후 API 조회)를 알지 못하도록 해야 한다. 내부 영역은 외부 영역의 아웃바운드 어댑터의 종류와 호출 순서에 의존해서는 안 되며 독립적으로 존재해야 한다. 이를 위해 하나의 도메인 모델을 구성하기 위한 여러 리포지토리 구현체의 의존성을 주입하고 구현체들의 여러 메서드 호출을 담당하는 별도의 클래스를 생성할 수 있다.



마이크로서비스와 API 게이트웨이

모놀리식 아키텍처를 마이크로서비스 아키텍처로 변경함으로써 서비스의 모듈 및 기능 추가 용이성, 스케일 아웃을 통한 확장성, 배포 독립성과 유연성이라는 장점을 시스템에 부여할 수 있다.

마이크로서비스의 개수가 증가함에 따라 시스템 복잡도와 서비스 간 의존도는 증가하게 된다. 클라이언트가 이렇게 분리된 마이크로서비스 각각에 대해 요청을 보낸다면 시스템의 복잡도가 클라이언트에게 직접적인 영향을 미치는 상황이 된다. 시스템을 이용하는 클라이언트로부터 시스템의 복잡도를 숨기고 추상화하기 위해 클라이언트와 마이크로서비스 사이에 게이트웨이(gateway)를 위치시킬 수 있다. 게이트웨이는 시스템의 단일 엔드포인트(진입점)가 된다. 이러한 게이트웨이를 API 게이트웨이라고 하며 이를 사용한 아키텍처 패턴을 API 게이트웨이 패턴이라고 한다.

API 게이트웨이의 기본적인 역할은 복잡한 마이크로서비스 아키텍처로 구성된 시스템을 추상화하고 특정 서비스에 대한 요청이 적절한 서비스로 라우팅되는 과정을 숨기는 것이다. 또한 API 게이트웨이는 시스템의 부하를 적절히 분산시키는 로드 밸런서의 역할도 수행할 수 있다.

API 게이트웨이로 인해 클라이언트는 시스템의 각 마이크로서비스에 직접적인 요청을 보낼 수 없다. API 게이트웨이는 인증 및 권한 부여와 같은 일부 보안 작업을 수행할 수도 있다.

API 게이트웨이는 시스템의 엔드포인트이므로 서비스의 데이터 트래킹, 요청 메트릭 수집, 통계 정보 계산을 수행하는 장소로서 그 역할을 수행하기 용이하다. 또한 HTTP 요청과 응답 헤더에 정보를 추가하여 시스템 내 서비스에서 이를 사용할 수 있도록 해준다.


API 게이트웨이와 서비스 라우팅

API 게이트웨이는 클라이언트의 모든 요청을 다른 서비스 엔드포인트로 동적으로 라우팅할 책임이 있다. 라우팅은 클라이언트의 요청을 받은 후 요청이 어떤 종류인지 파악하여 이 요청을 어떤 서비스에 전달할지 결정하는 과정을 말한다. 여기서 라우팅은 애플리케이션 레벨(소스 코드 레벨)의 요청 처리가 아닌, 시스템 레벨의 요청 처리 결정 과정을 말한다. HTTP 메서드 및 리소스 종류에 따라 요청을 다르게 처리하는 REST API 컨트롤러 클래스는 애플리케이션 레벨에서 클라이언트의 요청 처리를 결정하는 라우팅을 수행한다고 볼 수 있다.

라우팅은 요청을 보낼 서비스의 상태 확인(헬스 체크)이 수반되어야 한다. 해당 서비스가 요청을 정상적으로 처리할 수 있는 서비스인지 먼저 확인하는 과정이 필요하다. 게이트웨이의 라우팅은 항상 정상적인 서비스를 대상으로 수행되도록 유지되어야 한다.

마이크로서비스 아키텍처로 시스템을 구성하게 되면 서비스의 수는 증가하게 되겠지만 API 게이트웨이를 시스템에 도입한다면 시스템 추상화로 인해 클라이언트는 시스템의 복잡도에 대해 알지 못한다. 시스템이 복잡도가 높아질수록 API 게이트웨이는 시스템 추상화를 위해 클라이언트의 요청을 적절한 서비스에 라우팅할 능력과 책임의 수준이 높아진다.


서비스 레지스트리와 서비스 디스커버리

시스템을 구성하는 서비스들은 언제든 변화 가능하다. 따라서 API 게이트웨이의 라우팅은 동적으로 이루어져야 한다. 시스템의 변화란 새로운 서비스의 추가, 기존 서비스를 담당하는 클러스터의 네트워크 정보 변경, 서비스의 장애 등을 말한다.

요청을 받아 처리할 수 있는 서비스에 요청을 전달하기 위해 API 게이트웨이는 서비스들의 변화 가능한 대상들을 추적할 수 있어야 한다. 서비스 레지스트리 및 서비스 디스커버리는 이러한 역할을 수행한다.

서비스 레지스트리란 동적으로 변경되는 서비스와 관련된 정보를 등록 및 관리하는 과정 또는 그러한 역할을 수행하는 서비스를 말한다. 동적으로 변경되는 정보에는 서비스명, 서버의 네트워크 상의 위치 정보(IP) 및 포트 정보, 서버 구성 설정 정보 등이 있다. 서비스 레지스트리 서비스를 사용하여 시스템을 구성하는 다양한 클러스터에 대한 서버 환경 설정 관리를 중앙화할 수도 있다.

서비스 디스커버리란 서비스 레지스트리에 등록된 서비스(또는 서비스와 관련된 정보)를 검색 또는 탐색하는 과정을 말한다. API 게이트웨이는 클라이언트의 요청이 들어오면 적절한 서비스로의 라우팅을 위해 서비스 레지스트리에 등록된 해당 서비스의 네트워크 위치 정보를 불러오는 서비스 디스커버리를 수행한다.

서비스 레지스트리는 시스템 외부 클라이언트와 마이크로서비스 간 통신 뿐만 아니라, 시스템 내부 마이크로서비스 간 통신을 위해서도 사용된다.


서비스 애그리게이션 게이트웨이

API 게이트웨이는 시스템에 대한 클라이언트의 요청 라우팅, 로드 밸런싱, 보안 등의 다양한 역할을 수행한다. 클라이언트의 여러 요청을 단일 요청으로 모으는 역할을 수행하는 목적으로 게이트웨이를 사용할 수도 있다. 이러한 게이트웨이를 애그리게이션 게이트웨이라고 하며 이를 사용한 아키텍처 패턴을 게이트웨이 애그리게이션 패턴(gateway aggregation pattern)이라고 한다.

클라이언트가 어떤 작업을 수행하기 위해 서로 다른 여러 서비스에 요청을 보내는 시스템 구조에서 서비스에 새로운 기능이 추가되거나 시스템에 새로운 서비스가 추가될 경우 클라이언트는 추가적인 요청을 해야한다. 이는 클라이언트 입장에서 리소스 비용과 네트워크 호출을 증가하게 만든다. 기능 변경에 따른 이러한 영향은 클라이언트의 성능과 확장성에 나쁜 영향을 미친다.

게이트웨이가 클라이언트를 대신하여 작업 수행에 필요한 일련의 요청들을 각 서비스를 대상으로 수행한 후 그 결과를 클라이언트에게 응답함으로써 시스템에 대한 요청 수를 줄일 수 있다.


쿠버네티스 플랫폼과 마이크로서비스 아키텍처

모노리식 아키텍처 시스템에 마이크로서비스 아키텍처를 도입하면서 발생하는 문제들을 해결하기 위해 여러 아키텍처 패턴이 도입되었고 이를 뒷받침하기 위한 넷플릭스 OSS 기반의 여러 서비스 및 스프링 클라우드 서비스가 등장하였다.

컨테이너 오케스트레이션 오픈소스 시스템인 쿠버네티스(Kubernetes)나 상용 솔루션인 오픈시프트(Openshift)는 이러한 서비스들을 자체적인 플랫폼 기능으로 제공한다. 서비스 레지스트리와 서비스 디스커버리 기능을 서비스(Service), 인그레스(Ingress) 리소스로 제공하며 외부 컨피그 설정 기능을 컨피그맵(ConfigMap)으로 제공한다.

쿠버네티스는 또한 서비스 메시(service mesh)라는 개념과 사이드카(sidecar) 패턴을 도입하여 기존과는 다른 방식으로 마이크로서비스의 여러 아키텍처 패턴을 도입해준다.


참고

Comments