마이크로서비스 내부아키텍처 - 3회 : 어플리케이션 구성 패턴
Updated:
마이크로서비스 내부아키텍처 - 3회 : 어플리케이션 구성 패턴
마이크서비스 내부 아키텍처에 대해 살펴보자. 3회에 걸쳐서 기존 아키텍처의 문제점 및 이를 개선하고자 했던 아키텍처 변화양상과 이를 반영한 어플리케이션 구조에 대해 알아 보겠다.
어플리케이션 구성 패턴
지금까지 레이어드 아키텍처 및 헥사고널, 클린 아키텍처에 대해 살펴봤다. 이 같은 아키텍처는 기존 모노리스 어플리케이션 유형에도 통용되는 아키텍처로 마이크로서비스 만을 위한 아키텍처는 아니다. 그렇지만 최근 들어 더욱 강조되고 있는데 그 까닭은 이런 아키텍처 구조들이 마이크로서비스가 지향하는 유연성, 확장성을 지원하는 구조들 이기 때문이다. 그럼 이번 포스트에서는 이런 아이디어를 기반으로 마이크로서비스 내부구조를 생각해보자.
바람직한 마이크로서비스 내부 아키텍처 : 클린 마이크로서비스
지금까지 언급한 아키텍처 구조들은 점점 복잡해지는 모노리스 소프트웨어를 통제하기 위한 오랫동안 진행해 왔던 고민의 결과물이다. 그에 비에 마이크로서비스는 복잡해진 모노리스의 각 기능들을 쪼개기 때문에 어느정도 복잡성을 덜어낼 수 있다. 그렇지만 분리해도 복잡성은 이전되고 그 안의 복잡성을 통제할 필요가 있음은 마찬가지이다.
마이크로서비스의 내부구조를 정의 시 반드시 고려해야 할 점 하나는 마이크로서비스 시스템에서 정의해야 할 마이크로서비스 내부구조가 다양 할 수 있다는 것이다. 왜냐하면 마이크로서비스는 앞서 살펴본 것처럼 자율적인 마이크로서비스팀에 의한 폴리그랏한 내부구조를 가질 수 있기 때문이다.
마이크로서비스사는 유용한 마이크로서비스 설계에 대한 유용한 가이드를 개발자 사이트을 통해 공유하고 있는데, 아래그림은 마이크로소프트사가 정의한 마이크로서비스의 폴리그랏한 멀티 아키텍처들이다.
각 서비스의 개발 언어와 저장소가 다양하고 그 뿐만 아니라 아키텍처 구조까지도 다양함을 볼 수 있다. 마이크로서비스 아키텍처에서 각 서비스들은 각각 그 목표와 활용에 따라 명확하게 분리되어야 하고 그 서비스의 목적에 따라 적절한 개발언어 및 저장소 , 내부 아키텍처를 정의함이 바람직하다. 조회나 아주 간단한 기능들은 헥사고널 이나 클린 아키텍처 방식의 구조를 고수할 필요는 없을 것이다. 그렇지만 비지니스 규칙이 복잡한 서비스 들은 헥사고날 이나 클린 아키텍처의 구조를 기반으로 정의함이 바람직하다.
그럼 3개 아키텍처가 지향하는 바를 모아 바람직한 마이크로서비스 내부 구조를 정의해 보자 그러기 위해서는 3개 아키텍처가 지향하는 원칙들이 정리할 필요가 있는데 정리하면 다음과 같다.
- 지향하는 관심사에 따라 응집성을 높이고 관심사가 다른 영역과는 의존도를 낮추게 해야 한다.
- 업무 규칙을 정의하는 비지니스 로직 영역을 다른 기술 기반 영역으로부터 분리하기 위해 노력한다.
- 세부 기술 중심, 저수준의 외부 영역과 핵심 업무 규칙이 정의된 고수준의 내부 영역으로 구분된다.
- 고수준 영역은 저수준영역에 의존하지 않도록 해야하며 저수준영역이 고수준영역을 의존하게 해야 한다.
- 저수준 영역은 언제든지 교체, 확장 가능 하게 해야 하며, 이 변화가 고수준영역에 영향을 주면 안 된다.
- 자바 언어의 경우 저 수준 영역의 고수준 영역 의존을 위해 인터페이스를 통한 의존성 역전의 법칙을 활용한다.
- 인터페이스는 고수준의 안정된 영역 존재해야 하여 저 수준의 어댑터가 이를 구현한다.
이러한 원칙들을 준수하면서 아래 그림과 같은 마이크로서비스 아키텍처 구조를 정의해 보았다. 우선 기술 처리 중심의 세부사항을 의미하는 외부 영역과 비지니스 로직을 표현하는 내부영역으로 구분한다.
내부영역 에는 제일 안쪽에 도메인이 존재하고 도메인을 서비스가 감싸고 있다. 도메인은 핵심 비지니스 규칙을 구현하며, 서비스는 도메인을 호출하여 업무를 처리하는 절차를 기술한다. 내부 영역은 외부 영역과 연계하기 위해 인터페이스를 보유한다. 서비스인터페이스는 외부에서 내부 영역을 사용할 수 있도록 API를 제공하고 서비스가 이를 구현한다. 내부 영역의 또 다른 인터페이스는 데이터 처리를 위한 인터페이스이다. 레파지토리(Repository) 인터페이스 인데 레파지토리 인터페이스는 외부 영역에서 정의되지 않고 내부 영역에서 비지니스 처리 시 필요한 데이터 처리 사항을 추상화 하여 정의한다. 그럼 외부 영역의 저장소 어댑터는 이 레파지토리 인터페이스를 구현하여 세부 기술로 데이터 처리를 수행한다. 외부 영역 에는 저장소 처리 어댑터 뿐만 아니라 다양한 인 바운드, 아웃 바운드 을 처리하는 어댑터가 존재한다. REST API처리를 담당하는 어댑터, 이벤트 메시지 처리 담당 어댑터 , 메시지를 구독하는 메시지 컨슈머 어댑터, 다른 마이크로서비스의 API를 호출하는 프록시 어댑터 등이 위치한다. 저장소 처리 어댑터, 이벤트 발행 어댑터, API호출 프록시 어댑터 등 모든 아웃바운드 어댑터는 의존관계역전의 원칙을 적용하여 외부 영역에서 내부 영역을 의존하도록 설계한다.
내부 영역 – 업무 규칙
지금부터는 각 영역에 세부 구현에 필요한 패턴에 대해 생각해 보자. 업무 규칙을 정의하는 내부영역에는 서비스 인터페이스, 서비스, 도메인 , 레파지토리 인터페이스 , 도메인 이벤트 인터페이스,API 프록시 인터페이스 가 존재한다. 서비스 인터페이스는 외부 영역의 내부 영역에 대해 너무 많이 알지 못하도록 막기 위해 존재한다. 만약 없다면 추이 종속성이 발생 할 수 있다.(정보 은닉) 레파지토리 인터페이스 , 도메인 이벤트 인터페이스, API 프록시 인터페이스는 의존관계 역전의 원칙을 지원한다. 보다 안정된 곳인 고수준영역에 인터페이스가 존재하고 저수준의 외부 어댑터가 이들 인터페이스를 구현하게 한다. 다음에 고민해야 될 부분은 비지니스 로직의 핵심인 서비스와 도메인 이다. 서비스와 도메인은 클린 아키텍처의 유스케이스와 엔티티의 역할과 같다. 도메인은 비지니스 개념을 표현하고 서비스는 도메인을 활용하여 시스템 흐름 처리를 수행한다. 이러한 방식을 처리하기 위해 유용한 패턴이 존재하는데 마틴 파울러의 ‘엔터프라이즈 어플리케이션 아키텍처 패턴’에서 언급된 트랜잭션 스크립트 패턴과 도메인 모델 패턴이다.
트랜잭션 스크립트 패턴
트랜잭션 스크립트(Transaction Script) 패턴에서는 앞서 잠시 언급했듯이 도메인 객체가 비지니스 개념을 정의하되 아래 그림과 같이 행위를 가지고 있지는 않다. 모든 비지니스 행위, 즉 책임은 서비스에 존재한다. 서비스가 비지니스 절차에 따라 절차적으로 도메인 객체를 이용하여 모든 처리를 수행한다. 트랜잭션 스크립트 패턴의 서비스는 비대해지고 점점 도메인 객체는 정보 홀더(Information Holder)의 역할 정도로 변해 간다.
객체지향 지식이 없어도 절차 식 프로그래밍 방식과 같기 때문에 일반적으로 쉽게 이해할 수 있는 구조이고 기존 데이터베이스 중심 아키텍처에 익숙하다면 더 쉽게 적응할 수 있다. 이 패턴은 간단한 비지니스인 경우 쉽게 적용할 수 있다. 그렇지만 복잡한 비지니스에 적용할 경우 점점 코드의 양이 증가하는 등 데이터베이스 중심 아키텍처에서 겪었던 문제점이 발생 될 여지가 크다. 간단한 비지니스를 처리를 위해 선택하는 것이 좋다.
도메인 모델 패턴
도메인 모델(Domain Model) 패턴은 도메인 객체가 데이터 뿐만 아니라 비지니스 행위를 가지고 있으며 도메인 객체가 소유한 데이터는 도메인 객체가 제공하는 행위에 의해 은닉된다. 도메인 객체는 각각의 비지니스 개념 및 행위에 대한 책임을 수행하고, 서비스의 행위들은 대부분의 비지니스 처리를 도메인 객체에게 위임한다. 서비스의 책임들이 도메인으로 적절히 분산되기 때문에 서비스가 비대해 지지 않고 서비스 메서드는 단순해 진다. 도메인 모델 패턴의 도메인 모델은 객체지향 설계의 객체 모델이다. 거대한 서비스 클래스 대신에 각기 적절한 책임을 가진 여러 클래스로 구성되므로 이해하기 쉽고 관리,테스트 하기 쉽다. 더 진화하여 도메인 주도 설계의 애그리게잇(Aggregate) 패턴을 적용할 수 있는 구조이다. 핵심은 도메인 모델이기 때문에 객체지향 지식에 대한 경험과 역량이 필요하다. 잘 만들어진 도메인 모델은 복잡한 비지니스 로직 처리에 유용하며, 잘 정의된 도메인 모델은 코드의 양을 줄여주고 재 사용성도 높아진다. 복잡한 비지니스 로직이 많은 마이크로서비스의 구조로 선택하는 것이 좋다.
DDD 애그리게잇 패턴
애그리게잇(Aggregate) 패턴은 에릭에반스의 도메인 주도 설계(Domain Driven Design : DDD) 에 등장하는 패턴으로 점점 복잡해 질 수 있는 객체 모델링 단점을 보완한 패턴이라 볼 수 있다. 앞서 도메인 모델링을 하다 보면 객체간의 관계를 참조로 정의한다. 참조로 정의하면 일 대 다의 관계의 객체를 쉽게 사용할 수 있는 장점이 있다. 그렇지만 업무가 복잡해지면 참조로 인해 계층구조가 여러 단계를 가지게 되고 점점 참조 관계가 복잡해지고 무거워질 수 있다.
또한 이런 복잡한 도메인 모델은 모델 내부의 경계가 불 명확하다. 예를 들면 어떤 도메인 모델이 One To Many 관계를 가지고 있고 Many클래스의 개수의 총계를 One 클래스에 집계해야 하는 규칙이 있다고 보자. 서비스에서 로직 처리시 Many클래스가 추가되면 One 클래스의 집계 값을 수정해야 한다. 그런데 Many클래스만 추가하고 집계 값을 수정하지 않는다면 비지니스 일관성이 깨질 것이다. 점점 도메인 모델이 커지게 됨에 따라 이런 문제가 복잡해 지고 꼬일 수 있다. 이에 대한 개선으로 제일 상위에 존재하는 루트 엔티티(Root Entity) 중심으로 개념의 집합을 분리한 것이 에그리게잇 패턴이다.
아래 그림과 같이 복잡한 모델을 세 덩어리의 개념으로 분리할 수 있다. 1개 이상의 엔티티와 Value Object 로 구성된다. 제일 상위에 있는 루트 엔티티를 에그리게잇이라 한다. 애그리게잇 패턴은 이런 에그리게잇을 한 단위로 일관되게 처리하기 위해 다음과 같은 규칙을 부여한다.
- 애그리게잇 루트만 참조한다. 애그리게잇 내 상세클래스를 바로 참조하지 않고 루트를 통해 참조해야 한다. 수정도 마찬가지이다.
- 애그리게잇간 참조는 객체를 직접 참조하는 대신 기본 키를 사용한다. 기본 키를 사용하면 느슨하게 연관되고 수정이 필요하지 않은 애그리게잇을 함께 수정하는 실수를 방지한다.
- 하나의 트랜잭션으로 하나의 애그리게잇만 생성,수정한다.
외부 영역 - 세부사항
외부 영역은 내부 영역의 서비스 인터페이스를 사용하는 인 바운드 어댑터와 내부영역에서 선언한 아웃바운드 인터페이스들을 구현하는 어댑터들로 구성한다. 어댑터는 플러그인처럼 언제든지 교체되거나 확장될 수 있어야 한다. 따라서 내부 영역이 먼저 정의된 후에 외부 영역의 세부사항은 늦게 정의되어도 상관없도록 해야 한다. 소프트웨어는 부드러워야 한다. 동기,비동기 통신 및 저장소 처리를 위해 필요한 각 어댑터의 구현 메커니즘 및 고려사항에 대해 살펴보자.
API퍼블리싱 어댑터
REST API를 발행하는 인 바운드 어댑터이다. 내부 영역의 서비스 인터페이스를 호출하여 REST 형식의 API로 제공한다. 명시적인REST 리소스 명칭을 정의하고 각 REST 메서드가 의도에 맞게 서비스 API를 호출해 준다. 엔티티를 직접 제공하지 않고 API 필요해 맞는DTO(Data Transfer Object)를 생성, 엔티티를 이용 매핑하여 전달하는 것이 바람직하다.
API 프록시 어댑터
다른 서비스의 API를 호출하는 아웃바운드 어댑터이다. 내부 영역의 정의된 추상화된 프록시 인터페이스를 구체화 한다.
저장소 처리 어댑터
저장소 처리 어댑터 구현 시 데이터 처리 메커니즘에 대한 선택이 필요하다. OR 매핑 방식과 SQL 매핑 방식 을 사용할 수 있는데 내부영역에서 어떤 구조를 선택하였던지 둘 다 사용할 수 있다. 그렇지만 일반적으로 트랜잭션 스크립트 패턴을 사용했을 경우 SQL매핑 방식을 사용하고, 도메인 모델 패턴을 사용했을 경우 OR매핑 방식을 많이 선택한다.
SQL 매핑 방식의 프레임웍으로는 MyBatis가 가장 많이 사용되고 OR 매핑 방식으로 JPA나 Spring.Data가 많이 사용된다. SQL 매핑 방식은 SQL 질의문을 수동으로 작성해야 하므로 세밀한 SQL 컨트롤이 필요한 경우 유용하다. OR매핑 방식은 OR 매퍼가 저장소에 따라 런타임 시 자동으로 질의문을 생성한다. 따라서 SQL 작성에 대한 워크로드를 줄일 수 있다. 또한 설정파일 설정에 따라 쉽게 저장소를 변경할 수 있다. 때문에SQL 매퍼 방식보다는 유연한 메커니즘이다. 질의문의 수동 작성을 줄여주므로 익숙해 진다면 균일한 질의문 품질과 생산성 향상을 이룰 수 있다.
최근의 추세 를 보면 외국같은 경우는 OR매퍼가 SQL매퍼보다 휠씬 많이 사용되고 있지만 아직까지 국내 추세는 SQL 매퍼의 사용률이 높다. 아마도 처음에 논했던 데이터베이스 중심의 아키텍처 및 객체지향 경험의 내재화가 높지 않았던 개발문화에서 기인한다고 본다.
OR매퍼를 선택했던 프로젝트 예를 들면 생산성이 높은 OR매퍼를 선택 했음에도 프로젝트 팀원들의 대부분 SQL 매퍼에 익숙하고 객체모델링에 익숙하지 않아서 런닝커브가 높아 어려움이 많았다.
아키텍트는 이런 상황을 고려하여 제공할 비지니스 성격 및 팀원의 역량 , 개발 효율성들을 두루 고려하여 이러한 저장 메커니즘을 선택해야 할 것이다.
도메인 이벤트 발행 어댑터
앞서 외부 아키텍처에서 서비스 간 비동기 메시지 통신에 대해 살펴봤다. 여기서 메시지 대상이 되는 정보가 도메인 이벤트 이다. 도메인 이벤트는 어떤 사건에 따른 상태의 변경 사항을 말하여 ‘ 주문됨, 주문 취소됨 등’의 명칭를 갖는 클래스로 정의되며 컨슈머에게 전달되기 위해 도메인 이벤트 발행 어댑터를 통해 발행된다. 애그리게잇 패턴을 적용했을 경우, 이런 도메인 이벤트는 애그리게잇에서 발생한 사건이 된다.
실제로 도메인 이벤트가 생성되는 위치는 내부 영역이며 도메인 이벤트 발행 어댑터는 내부 영역의 이벤트 인터페이스를 구현하여 아웃바운드로 특정 메시지 큐나 스트림 저장소에 발행하는 역할을 수행한다.
도메인 이벤트 핸들러
도메인 이벤트 발행 어댑터가 있다면 당연히 인 바운드 어댑터도 필요하다. 도메인 이벤트 핸들러는 외부에서 발행된 도메인 이벤트를 구독하여 내부영역으로 전달하는 일을 수행한다. 이벤트 상태에 따라 적절한 서비스 인터페이스를 호출한다.
결론
지금까지 3회에 걸쳐 마이크로 서비스 내부 아키텍처 구조에 대해 살펴봤다. 현실에서 진행되고 있는 기존 어플리케이션 구조의 문제점에 대해 살펴보았고 아키텍처 구조의 흐름에 대해 살펴봤다. 헥사고날이나 클린 아키텍처를 통해 보았듯이 바람직한 소프트웨어 구조는 유연함이다. 로보트 c 마틴의 말처럼 소프트웨어는 소프트해야한다. 물론 플랫폼이나 쿠버네티스 같은 외부 아키텍처 적용을 통해서만으로도 처음에는 꽤 유연하고 기민 해질 수 있다. 그렇지만 시스템의 핵심은 소프트웨어이고 실제로 비지니스를 제공하는 마이크로서비스이다. 핵심이 유연하지 않다면 오래가지 못할 것이다.
따라서 당연히 기민한 비즈 제공을 위해 마이크로서비스 내부 또한 유연해야 한다. 마이크로서비스 내부가 유연 해야지 마이크로서비스간의 관계도 이벤트를 기반으로 느슨하게 구현할 수 있고 이러한 구조가 비로소 서비스를 독립적으로 확장, 대체하고 배포하게 해준다.
여기서 살펴본 것처럼 이러한 구조의 핵심은 어떻게 철저하게 어플리케이션의 관심사를 분리할 수 있는 것인가 하는 점이다. 그리고 가장 중요한 것은 비지니스 표현과 변화가 잦은 기술 표현을 나눌 것이다.
초반에 언급 한 것 처럼 가장 빨리 가는 방법은 제대로 가는 것이다. 처음에는 오래 걸리고 힘들고 불편하겠지만 꼼수를 쓰지않고 원칙을 준수하는 것이 길게 보면 가장 빠른 법인 것이다.