[번역] Azure 앱서비스 구조를 깊게 살펴봅시다

원문: https://msdn.microsoft.com/en-us/magazine/mt793270

Azure 앱서비스는 웹, 모바일, API 어플리케이션을 개발할 때 최고의 PaaS가 되도록 만들었습니다. 간단한 마케팅 페이지부터 확장성이 중요한 상거래 솔루션까지 모두 소화할 수 있습니다.

앱서비스는 완전한 관리형(managed) 플랫폼으로, 쉽게 말해서 인프라를 직접 운영할 필요가 없다는 뜻입니다. OS 패치와 프레임워크는 모두 플랫폼에서 알아서 해주기 때문에 서버를 직접 관리하지 않아도 됩니다. 여러분이 만든 어플리케이션은 가상화된 서버에서 동작하며 확장할 최대한의 서버 인스턴스 수만 정해주면 됩니다. 어플리케이션이 더 많은 리소스를 필요로 할 때 플랫폼이 자동으로 인스턴스를 확장하고, 동시에 확장한 인스턴스의 부하 분산 처리까지 합니다.

저희 앱서비스 팀은 여러분이 굳이 자세한 내용을 몰라도 편하게 쓸 수 있도록 만들었지만 동작 원리를 깊게 알면 더 좋을 것입니다. 이 글에서는 앱서비스의 내부 구조를 다루고 몇 가지 시나리오에 알맞은 구성에 대해서도 살펴보겠습니다.

전세계적으로 지역 분산된 구조

클라우드 컴퓨팅은 빠르게 확장할 수 있어야 하고 한 없이 큰 용량을 가져야 합니다. 클라우드의 확장 개념을 우리가 모니터를 보는 모습에 빗대보겠습니다. 모니터에 그림 하나가 있다고 할 때, 이를 멀리서 보면 하나의 선명한 그림이지만 가까이서 보면 수많은 픽셀로 이루어져있습니다. 클라우드도 이처럼 많은 서버로 구성되면서 하나의 큰 그림을 만듭니다. 앱서비스 클러스터는 서버의 묶음을 ‘확장 단위(scale unit)’ 또는 ‘스탬프’라고 부르고, 이 확장 단위가 전세계 Azure 데이터센터에 빼곡히 있습니다.

앱서비스는 전세계에 Azure를 지원하는 지역이면 어디든 있습니다. Azure가 있는 모든 지역에는 사용자의 어플리케이션을 실행하는 앱서비스 확장 단위와 지역 제어 단위(regional control unit)가 있습니다. 평상시에는 제어 단위가 사용자에게 보이지 않으며 플랫폼의 일부로 취급됩니다. 제어단위에 문제가 있다면 그 때는 보이겠지만 평소엔 제어 단위가 있는지도 모를 것입니다. 제어 단위 중에는 모든 API 호출을 관리하는 게이트웨이 역할을 하는 제어 단위도 있습니다. 사용자가 새 어플리케이션 생성 명령을 포털에서 하든 명령줄인터페이스로 하든 Azure REST API로 하든 상관없이 Azure의 중앙제어부(management.azure.com)로 생성 요청을 보내는 역할입니다. Azure 리소스 관리자 (Azure Resource Manager; 이하 ARM. bit.ly/2i6UD07)는 사용자의 여러가지 리소스를 하나의 그룹으로 관리할 수 있도록 합니다. 여러분이 ARM에서 사용하는 API에 대해서 말하자면, ARM으로 리소스를 제어할 때 사용하는 API는 ARM이 직접 관리하는게 아닙니다. Azure는 서비스 별로 관리용 API가 있고, 여러분이 ARM에 요청하면 ARM이 각 서비스의 API를 호출하는 식입니다. 앱 서비스의 경우 ARM이 받는 모든 API 요청은 Geo-Master로 포워딩됩니다. Geo-Master는 전세계의 모든 확장 단위를 파악하고 있으므로 여러분이 새 웹사이트나 웹 작업 같은 앱 서비스 어플리케이션 생성을 요청하면 Geo-Master가 최적의 확장 단위를 찾은 후, 그 확장 단위에 생성 요청을 포워딩합니다. 확장 단위가 생성 요청을 받으면 생성할 앱을 실행할 공간과 인프라를 확보한 후(프로비저닝이라고 합니다) 앱 실행에 필요한 리소스를 할당합니다. 아래 그림 1은 Azure에서 새 앱을 만드는 과정입니다.

Global Distribution of App Service Scale Units
그림 1 전세계에 분산된 앱서비스 확장 단위

새 앱을 만드는 과정은 다음과 같습니다.

  1. 사용자가 새 웹사이트 생성을 요청합니다.
  2. ARM은 사용자가 리소스에 대한 접근과 작업 권한이 있는지 확인합니다. 이번 작업은 ‘생성하기’에 대한 권한입니다. 권한 확인이 끝나면 해당 요청을 앱서비스의 Geo-Master로 포워딩합니다.
  3. Geo-Master는 사용자 요청에 가장 알맞은 확장 단위를 찾아서 해당 요청을 포워딩합니다.
  4. 하나의 확장 단위에 새 어플리케이션을 만듭니다.
  5. Geo-Master는 요청에 대한 성공 여부를 리포트합니다.

앱서비스가 얼마나 많은 확장 단위로 이루어져있든 여러분의 어플리케이션은 대부분의 경우 단 하나의 앱서비스 확장 단위에서 동작합니다. Azure 트래픽관리자를 이용해서 여러 지역에서 앱이 실행되고 있다면 여러 개의 독립된 확장 단위에서 동작합니다. 확장 단위는 지역에 종속되어있기 때문입니다. 다시 말해, 여러분의 앱은 하나의 확장 단위에 종속되어있다고 볼 수 있습니다.

앱서비스 확장 단위란?

앱서비스 확장 단위(App Service scale unit)는 어플리케이션을 호스트하고 실행하는 여러 개의 서버를 하나로 묶은 단위입니다. 보통 한 단위는 1,000개 이상의 서버로 이루어져 있습니다. 서버를 클러스터로 구성하면 규모의 경제를 실현하고 인프라의 재사용성을 높일 수 있습니다. 앱서비스 확장 단위를 구성하는 시스템은 Azure 클라우드서비스(역자주: Cloud Service는 Azure 제품 이름 중 하나입니다) 배포로 이루어집니다. 지금은 Azure 클라우드서비스가 클래식 취급을 받지만 앱서비스를 처음 런칭했을 때는 2012년 6월이었습니다. 확장 단위를 만들고 제어하는 과정은 모두 자동화되어있습니다.

확장 단위의 부분 별 설명

확장 단위의 역할은 기본적으로 사용자의 어플리케이션을 호스팅하고 실행하는 것입니다. 어플리케이션은 윈도우 서버에서 구동되고 웹 작업자(Web Workers; 줄여서 ‘작업자’)는 구동 중인 어플리케이션을 참조합니다. 확장 단위 안에 있는 서버의 대부분은 작업자입니다. 확장 단위에는 작업자 외에 앱서비스의 여러 기능을 처리하는 지원 서버(support server)도 있습니다. 지원 서버마다 역할이 있고 각각의 지원서버는 여러 인스턴스에 배포되어 확장을 보다 수월하게 합니다.

프론트엔드

프론트엔드는 Layer-7 부하분산 장치입니다. 프록시와 같은 개념이며, 들어오는 HTTP 요청을 여러 어플리케이션과 어플리케이션에 속한 작업자에 분배합니다. 현재 앱서비스는 단순한 라운드로빈(Round-robin) 알고리즘으로 구현되어 있습니다. (역자주: 라운드로빈은 새가 한 번 날개짓 할 때마다 오르락내리락하는 것처럼 1-2-3-1-2-3…순서로 하나씩 돌아가며 수행하는 방식입니다)

웹 작업자 (Web Workers)

작업자는 앱서비스 확장 단위의 핵심입니다. 앱 그 자체를 구동합니다.

앱 서비스에서는 어플리케이션을 어떻게 실행할지 선택할 수 있습니다. 예를 들어, 공유(shared) 서버 또는 단독(dedicated) 서버를 ‘앱 서비스 계획(App Service Plan)’에서 선택할 수 있습니다. 앱 서비스 계획은 서버 할당, 기능, 용량 등을 정의한 묶음입니다. 공유 작업자는 여러 고객들의 어플리케이션을 호스팅하고 단독 작업자는 단 하나의 고객을 위한 어플리케이션을 호스팅합니다. 단독 작업자가 올라가 있는 단독 서버는 유형과 크기를 선택할 수 있습니다. 서버 사이즈가 클 수록 더 많은 CPU와 메모리 지원을 사용할 수 있습니다. 앱 서비스 계획은 어플리케이션을 실행하기 위해 서버를 미리 할당해놓습니다.

앱 서비스 확장 단위는 미리 프로비전한 작업자 풀을 가지고 있어서 그림 2의 첫번째 그림처럼 여러분의 어플리케이션을 받아들일 준비를 하고 있습니다. 여러분이 앱 서비스 계획에서 서버 두 대를 사용하겠다고 지정하면 그림 2의 두번째 그림처럼 앱 서비스는 두 대의 서버를 할당합니다. 그 다음, 그림 2의 세번째 그림처럼 앱서비스 계획에서 서버 두 대를 수평확장(scale-out)하겠다고 하면 작업자 풀에서 즉시 작업자를 할당합니다. 작업자를 미리 프로비전했고 워밍업까지 되어있으므로 여러분의 어플리케이션을 작업자가 배포받기만 하면 됩니다. 어플리케이션이 배포되면 작업자가 본격적으로 돌기 시작하고 프론트엔드는 트래픽을 받습니다. 이 모든 과정이 단 몇 초 내로 끝납니다.

Server Application Process in App Service Scale Unit
그림 2 앱 서비스 확장 단위에서 서버 어플리케이션을 실행하는 단계

그림 2의 네번째 그림은 여러 개의 앱서비스 계획을 각기 다른 색으로 표시하고 있습니다. 다른 색은 서로 다른 고객에 속한 앱 서비스 계획을 의미합니다.

파일 서버

앱은 콘텐츠를 보존할 저장소가 있어야 합니다. 콘텐츠는 HTML, .js, 이미지, 코드 등 어플리케이션을 구동하는데 필요한 파일을 말합니다. 파일 서버는 Azure 저장소 blob을 마운트하고 작업자에게 네트워크 드라이브로 보이도록 합니다. 작업자는 네트워크 드라이브를 로컬 드라이브로 맵핑합니다. 이렇게 하면 작업자가 취급하는 어플리케이션에서는 로컬드라이브를 다루는 것과 똑같아집니다. 어플리케이션에서 이루어지는 모든 파일 읽기/쓰기 작업은 파일서버를 거칩니다.

API 콘트롤러

API 콘트롤러는 Geo-Master가 앱 서비스를 조작할 때 사용됩니다. Geo-Master가 모든 확장 단위를 알고 있지만 여러분의 어플리케이션을 직접 관리하는 작업은 API 콘트롤러로 이루어집니다. 다시 말해, Geo-Master는 해당 확장 단위의 API 콘트롤러를 대행(delegate)할 뿐입니다. 예를 들어, Geo-Master에서 앱을 생성하라는 API 요청을 받으면 이 요청을 한 확장 단위의 API 콘트롤러로 넘기고 API 콘트롤러는 이 작업에 필요한 모든 단계를 처리합니다. Azure 포털에서 여러분이 만든 앱 서비스에 ‘재설정(Reset)’ 버튼을 누르면 API 콘트롤러가 그 앱에 할당된 모든 웹 작업자에 알림을 보내서 앱을 재시작합니다.

게시자 (Publishers)

Azure 앱 서비스는 FTP 접속을 지원하며 FTP로 앱 콘텐츠에 접근할 수 있습니다. 앞에서 언급했듯 앱 콘텐츠는 Azure 저장소 Blob에 저장되어있고 파일 서버는 이를 로컬 드라이브로 매핑하고 있는데, 게시자는 FTP 기능을 외부에 노출해서 앱 콘텐츠에 접근할 수 있게 합니다. 여러분은 게시자가 열어주는 FTP로 앱 콘텐츠 뿐만 아니라 로그도 접근할 수 있습니다.

앱 서비스에 앱을 배포하는데는 FTP 외에 여러가지 방법이 있습니다. 대표적으로 Visual Studio에서 Web Deploy를 이용하는 방법이 있고, 그 외에 Visual Studio 릴리즈 매니저의 지속 배포 기능이나 GitHub을 이용할 수도 있습니다.

SQL Azure

앱 서비스 확장 단위는 Azure SQL 데이터베이스에 메타데이터를 저장합니다. 어플리케이션 별로 저장 영역이 있고 그 안에는 어플리케이션에 대한 실행 정보도 저장되어 있습니다.

데이터 역할 (Data Role)

모든 역할에는 데이터베이스에 저장해야하는 데이터가 있기 마련입니다. 예를 들어, 웹 작업자는 앱을 런칭할 때 사이트 설정 정보가 있어야 하고 프론트엔드는 어떤 서버가 앱을 실행하고 있는지 알아야 HTTP 요청을 앱에 정확하게 전달할 수 있습니다. API 콘트롤러 또한 사용자가 명령한 정보를 읽고 데이터베이스에 저장하고 있습니다. 여기서 데이터 역할은, 확장 단위마다 있는 SQL 데이터베이스와 여러 역할 사이의 캐시(cache)입니다. 다른 여러 역할에서의 데이터 레이어(SQL 데이터베이스)를 추상화해서 확장성을 높이고 성능을 끌어올립니다. 뿐만 아니라, 소프트웨어 개발과 유지보수도 더 단순해집니다.

더 확실하게 알기 위한 사례

여기까지 여러분은 Azure 앱 서비스가 어떻게 구성되어있는지 알 수 있었습니다. 이제 이를 바탕으로 앱 서비스 팀이 정리한 팁을 살펴보겠습니다. 지금부터 나올 내용은 앱 서비스 엔지니어링 팀이 사용자들과 함께 일을 진행해보면서 얻은 지식입니다.

밀집도 조절하기

대부분의 사용자는 하나의 앱서비스 계획에 10개 이하로 적은 수의 어플리케이션을 실행합니다. 아주 많은 어플리케이션을 실행하는 사용자도 있는데, 이 때 서버의 연산 능력을 넘지 않도록 조심해야 합니다.

예를 들어가며 어플리케이션의 구성과 계산 리소스(CPU/메모리/트래픽의 조합)와의 관계에 대해 살펴봅시다. 웹앱 2개와 모바일 백엔드 앱 1개가 앱 서비스 계획에 있다고 가정합시다. 이 때 앱서비스 계획은 서버 2대로 설정했습니다.

기본적으로 앱 서비스 계획에 있는 어플리케이션은 그 앱 서비스 계획이 제공하는 서버 리소스 전체를 사용합니다. 앱 서비스 계획이 단 하나의 서버만 실행하고 있다면 그저 단일 서버에서 모든 어플리케이션이 구동하고 있다고 이해하면 됩니다.

앱 서비스 계획에 계산 리소스가 여러 개일 때는 더 깊은 이해가 필요합니다. 하나의 앱 서비스 계획에 10개의 계산 리소스가 있다면 어플리케이션은 10개의 계산 리소스마다 실행됩니다. 다시 말해, 하나의 앱 서비스 계획에서 50개의 앱을 서버 10개로 설정했다면 첫 서버에 50개, 두번째 서버도 50개 등등 10개의 서버 모두 50개씩 실행하고 있는 것입니다.

앱 서비스 계획에서 여러 어플리케이션을 실행하다가 그 중 한 어플리케이션에 HTTP 요청이 많아져서 더 많은 계산 리소스가 필요할 때가 있습니다. 이 때 어플리케이션을 실행할 서버 수만 늘리면 해결될거라 생각해서 서버를 한 대에서 여러 대로 확장하면 별 효과를 보지 못할 수 있습니다. 왜냐하면 서버 수는 늘어나지만 그 안에 다른 어플리케이션이 차지하는 CPU/메모리 또한 서버 수만큼 확장되기 때문입니다.

이보다는 각 어플리케이션의 사용량과 트래픽을 고려해서 적은 자원이 필요한 앱끼리 묶고 높은 사용량을 보이는 앱끼리 묶어서 앱 서비스 계획을 분리하는 편이 낫습니다. 50개의 어플리케이션이 실행 중이라면 아래와 같이 계산 리소스를 구분할 수 있습니다:

  • 낮은 사용량의 40개 어플리케이션은 단일 앱 서비스 계획에 둡니다.
  • 중간 정도 사용량을 보이는 5개는 두번째 앱 서비스 계획을 만들고 단일 서버로 설정합니다.
  • 사용량이 높은 5개 어플리케이션은 각각 별도의 앱 서비스 계획에 둡니다. 그리고 자동 크기 조정 기능을 설정하고 최저는 1대, 최고는 각 사용량에 맞춰서 설정합니다.

위와 같이 하면 50개의 어플리케이션을 7개의 계산 리소스에 담은 기본적인 설정이 됩니다. 이제 사용량이 높은 5개의 어플리케이션은 필요에 따라 독립적으로 확장할 수 있습니다.

앱 단위로 확장하기

많은 어플리케이션을 앱 서비스 계획에서 효율적으로 관리하는 방법으로는 밀집도 조절 외에 앱 단위로 확장하는 방법도 있습니다. 자세한 내용은 이 링크(bit.ly/2iQUm1S)에서 볼 수 있습니다. 앱 단위로 확장하면 어플리케이션이 실행되는 서버 수의 최대치를 조절할 수 있을 뿐만 아니라 어플리케이션 단위로도 조절할 수 있습니다. 어플리케이션 단위로 조절하면 사용 가능한 모든 서버에서 실행되는 것이 아닌, 직접 설정한 최대한의 서버 수 내에서 실행할 수 있게 됩니다.

앞서 예를 든 50개의 앱을 앱 단위 확장 방법으로 설정하면 모두 하나의 앱 서비스 계획에 담고 각각의 앱을 다음과 같이 설정할 수 있습니다:

  • 낮은 사용량의 40개 어플리케이션은 최대 1개의 서버에서 실행하도록 각각 설정합니다.
  • 중간 정도 사용량의 5개 어플리케이션은 각각 최대 2개의 서버에서 실행하도록 설정합니다.
  • 나머지 높은 사용량을 가진 5개의 어플리케이션은 최대 10개의 서버에서 실행하도록 설정합니다.

위와 같이 하면 앱 서비스 계획은 최소 5개의 서버(높은 사용량에 최소치를 맞추어)로 시작할 수 있습니다. 그 후 CPU/메모리 사용량에 따라 자동 크기 조정 규칙을 만들면 됩니다.

이렇게 설정하면 Azure 앱 서비스는 어플리케이션이 필요한 컴퓨터 자원을 자동으로 할당합니다. 그리고 앱 서비스는 각 어플리케이션에 설정된 최대한의 작업자 숫자에 맞춰 어플리케이션 인스턴스 수를 제한할 것입니다. 결론적으로, 앱 서비스 계획에 작업자 숫자를 늘린다고 50개 앱이 모든 곳에서 실행되지 않게 됩니다.

요약하면, 앱 단위 확장은 앱 서비스 계획 위에서 도는 어플리케이션을 모든 서버로 흘러넘치지 않게 해줍니다. 그래서 모든 어플리케이션이 모든 계산 리소스에서 돌지 않도록 합니다.

어플리케이션 슬롯

앱 서비스에는 배포 슬롯이라는 것이 있습니다. 자세한 내용은 다음 링크(bit.ly/2iJzv3f)에서 볼 수 있습니다. 배포 슬롯은 프로덕션으로 동작 중인 어플리케이션과는 구분된 별개의 어플리케이션을 ‘슬롯’이라는 개념으로 만든 것입니다. 새로 만든 어플리케이션은 프로덕션으로 교체하기 전에 테스트용으로 활용할 수 있습니다.

어플리케이션 슬롯은 앱 서비스에서 가장 많이 쓰이는 기능입니다. 각각의 어플리케이션 슬롯은 사실상 완전히 독립된 어플리케이션입니다. 즉, 커스텀 도메인, 별개의 SSL 인증서, 별개의 어플리케이션 설정값 등 모든 요소가 독립적입니다. 나아가 앱 서비스 계획조차 프로덕션 슬롯과는 별개로 다룰 수 있다는 뜻이기도 합니다.

기본적으로 각 어플리케이션 슬롯은 동일한 앱 서비스 계획 안에 만들어집니다. 낮은 사용량의 어플리케이션은 리소스 사용량이 낮으므로 같은 앱 서비스 계획 안에 만들어도 괜찮습니다.

하지만, 하나의 앱 서비스 계획에 담긴 모든 어플리케이션은 동일한 서버에서 실행되므로 프로덕션 어플리케이션과 같은 서버에 있게 됩니다. 프로덕션이 아닌 어플리케이션 슬롯에 부하테스트를 해도 프로덕션 어플리케이션까지 영향을 받게 되는 문제가 있습니다.

부하테스트를 할 때 프로덕션 슬롯에 주는 영향 없이 리소스 사용량을 점검하고 싶다면, 새 앱 서비스 계획을 만든 후 해당 슬롯을 그 계획으로 옮기면 됩니다. 다음과 같이 해보세요:

  • 테스트용 슬롯을 옮길 새 앱 서비스 계획을 만듭니다. 주의사항: 앱 서비스 계획은 프로덕션 슬롯이 있는 앱 서비스 계획과 같은 리소스 그룹, 같은 지역에 있어야 합니다.
  • 테스트용 슬롯을 위 단계에서 만든 앱 서비스 계획으로 옮깁니다. 이제 계산 리소스는 프로덕션 슬롯과 완전히 다릅니다.
  • 이제 테스트 슬롯에 마음껏 테스트를 하세요. 앱 서비스 계획이 다르므로 프로덕션 슬롯의 리소스에는 영향을 주지 않습니다.
  • 테스트를 마친 후 프로덕션 슬롯으로 교체하고 싶다면 테스트 했던 슬롯을 다시 프로덕션 슬롯이 있는 앱 서비스 계획으로 옮긴 후에 전환(swap)을 하세요.

무중단 프로덕션 배포

어떤 개발팀은 어플리케이션을 운영하면서 매일 업데이트를 배포하기도 합니다. 이 때, 여러분은 프로덕션에 바로 업데이트를 하고 싶진 않을 것입니다. 배포할 때 서비스 중단을 최소화할 수만 있다면 더욱 좋겠지요. 어플리케이션 슬롯을 잘 활용하면 충분히 가능합니다. ‘pre-production’이라는 슬롯을 하나 만든 후 가장 최근의 소스코드를 배포하고 프로덕션 설정과 동일하게 맞춥니다. 테스트를 충분히 했다면 전환(swap) 버튼을 눌러서 프로덕션 슬롯과 바꿉니다. 교체 작업은 어플리케이션을 재시작하지 않고 콘트롤러가 프론트엔드 부하분산장치(load balancer)에 알림을 줘서 트래픽을 최신 슬롯으로 리다이렉트하도록 합니다.

프로덕션의 트래픽을 받기 전에 워밍업을 해야 하는 어플리케이션도 있습니다. 예를 들면 캐시를 생성해야 하거나 .NET 어플리케이션의 경우 .NET 런타임이 JIT 처리를 해야하는 경우입니다. 프로덕션으로 전환하기 전에 미리 슬롯을 워밍업할 수 있습니다.

pre-production 슬롯으로 테스트와 워밍업을 동시에 하는 사용자도 있습니다. Visual Studio Release Manager 같은 지속 배포 도구를 사용하면 슬롯 전환 전에 pre-production 슬롯에 코드를 배포하자마자 테스트를 실행해서 문제가 없는지 확인한 후 워밍업 수행까지 한 흐름에 할 수 있습니다.

확장 단위의 네트워크 설정

앱 서비스의 확장 단위는 클라우드 서비스를 통해 배포됩니다. 이와 관련한 네트워크 설정과 특징을 이해하면 여러분의 앱이 네트워크에 어떤 영향을 주고받는지 더 깊게 이해할 수 있습니다.

확장 단위는 하나의 가상 IP (VIP)만 노출하고 있습니다. 한 확장 단위 안에 있는 모든 어플리케이션은 이 VIP를 통해 트래픽을 받습니다. 또한, 이 VIP는 앱 서비스 확장 단위가 배포된 클라우드 서비스를 나타낸 것이기도 합니다.

앱 서비스 어플리케이션은 HTTP(80 포트)와 HTTPS(443 포트)로 오는 트래픽만 받습니다. 기본적으로 모든 어플리케이션은 HTTPS를 지원하는 azurewebsites.net 도메인이 있습니다. 뿐만 아니라 앱 서비스는 Server Name Indication(SNI)와 IP기반 Secure Socket Layer(SSL)를 모두 지원합니다. IP기반 SSL의 경우, 인바운드 트래픽에 대한 IP만 할당받으며, 이 IP가 클라우드 서비스 배포본과 엮여있습니다. 참고로 HTTPS로 요청받는 모든 SSL 연결은 프론트엔드에서 끝나고, 프론트엔드는 그 트래픽을 특정 어플리케이션이 있는 작업자로 포워딩합니다. (역자주: 프론트엔드에서 인증서기반 암호화/복호화를 처리하며, 프론트엔드와 작업자 사이는 일반 HTTP 통신입니다. 더 자세한 내용은 다음 글을 참고하세요. http://stackoverflow.com/a/43132372/361100)

공개 VIP

기본적으로 한 확장 단위의 모든 인바운드 HTTP 트래픽에는 1개의 공개 VIP를 사용합니다. 즉, 어떤 앱이든 하나의 VIP로 지정할 수 있습니다. 앱 서비스에 1개의 앱만 있다면, nslookup 명령을 실행해보세요. 아래는 그 결과 예입니다.

#1 PS C:\> nslookup awesomewebapp.azurewebsites.net
#2 Server:  UnKnown
#3 Address:  10.221.0.3
#4 Non-authoritative answer:
#5 Name: waws-prod-bay-001.cloudapp.net
#6 Address:  168.62.20.37
#7 Aliases: awesomewebapp.azurewebsites.net

한 줄씩 awesomewebapp.azurewebsites.net에 대한 결과를 살펴보겠습니다.

  • 줄 #1 nslookup에서 awseomwebapp.azurewebsites.net 을 쿼리한 결과입니다.
  • 줄 #5 어플리케이션 awseomwebapp을 호스팅하는 확장 단위의 도메인 이름을 볼 수 있습니다. cloudapp이라는 이름에서 알 수 있듯이 앱 서비스 확장 단위는 Azure 클라우드서비스에 배포되어있다는 것을 알 수 있습니다. WAWS는 Windows Azure Web Sites의 약자입니다. 과거에 Azure가 Windows Azure라고 불렸으며 앱 서비스는 Web Sites로 불렸던 적이 있습니다.
  • 줄 #6 확장 단위의 VIP를 볼 수 있습니다. 줄 #5에서 언급한 waws-prod-bay-001는 호스팅하는 모든 어플리케이션을 하나의 공개 VIP로 지정할 수 있습니다.
  • 줄 #7 같은 IP 주소에 매핑된 모든 도메인 별칭입니다.

아웃바운드 VIP

보통의 어플리케이션은 다른 Azure 서비스와 연결하거나 Azure가 아닌 외부 서비스와 통신할 때가 많습니다. 이 때 어플리케이션이 속해 있는 확장 단위를 넘어서 외부 네트워크로 연결을 해야 하는데 이를 아웃바운드 네트워크라고 합니다. Azure 서비스인 SQL 데이터베이스나 저장소에 연결하는 것도 아웃바운드 네트워크입니다. 아웃바운드 통신에는 최대 5개의 가상IP (VIP; Virtual IP)를 사용할 수 있습니다. 하나는 공용 VIP이며, 나머지 4개는 아웃바운드 전용입니다. 하나의 확장 단위에 있는 모든 어플리케이션은 이 5개의 IP를 사용하는데, 여러분이 어떤 VIP를 사용할지 선택할 수는 없습니다. 그러므로, 여러분이 접속할 서비스에 화이트리스트 IP를 등록하고 싶다면 5개를 모두 등록해야합니다. 여러분의 어플리케이션이 놓여있는 확장 단위의 IP 주소를 확인하려면 아래 그림 3과 같이 포털의 앱 서비스 속성에서 볼 수 있습니다.

App Service Application Outbound IP Address View in Azure Portal
그림 3 Azure 포털에서 앱 서비스 어플리케이션의 아웃바운드 IP 주소를 볼 수 있습니다

인바운드와 아웃바운드의 모든 IP를 단독으로 할당받으려면 이 링크(bit.ly/2hVRSlR)의 앱 서비스 환경(App Service Environment) 문서를 참고해주세요.

IP와 SNI SSL

앱 서비스는 IP기반 SSL 인증서를 지원합니다. IP-SSL을 사용하려면 HTTP 트래픽을 받기 위한 인바운드용 IP 주소를 지정받아야(dedicated IP) 합니다.

Azure에서 제공하는 지정 IP 주소와는 달리 앱 서비스의 IP-SSL은 여러분이 앱을 사용하고 있는 동안만 유효합니다. 다시 말해, IP 주소를 지정받았으되 고정적으로 가지고 있을 수는 없습니다. 여러분이 IP-SSL을 삭제하면 지정받았던 IP 주소를 잃고, 그 IP 주소는 다른 어플리케이션에 할당됩니다.

앱 서비스는 SNI SSL도 지원합니다. SNI SSL은 IP를 지정받을 필요가 없고 대부분의 브라우저에서 지원하므로 SNI SSL 사용을 권장합니다.

아웃바운드 네트워크에서의 포트 허용량과 그 한계

보통의 어플리케이션은 외부 네트워크에 아웃바운드로 연결할 일이 많습니다. 앞서 언급했듯 Azure 내부 서비스인 SQL 데이터베이스나 저장소에 접속하는 것 뿐만 아니라 HTTP/HTTPS API에 접속하기도 합니다. Bing 검색 API를 이용하거나 비즈니스 로직을 구현한 백엔드 API 어플리케이션에 접속하는 경우가 그 예입니다.

위의 경우는 모두 앱 서비스가 외부로 나가는 네트워크 소켓을 열고 아웃바운드 요청을 만드는 작업이며, Azure 네트워크 관점에서는 원격(remote) 접속입니다. 그러므로, 앱 서비스에서 원격 엔드포인트로 나가는 요청은 Azure 네트워킹 설정과 관련이 있으며,  네트워크주소변환(Network Address Translation; NAT) 테이블의 매핑작업으로 관리되고 있습니다.

하나의 앱 서비스 확장 단위에서 NAT 매핑을 새로 만드는 작업은 시간도 걸리지만 만들 수 있는 최대 NAT 매핑 갯수도 명확한 한계값이 있습니다. 그러므로, 앱 서비스의 아웃바운드 연결은 제약을 받을 수도 있고 더 이상 만들 수 없는 경우도 있습니다.

연결 수의 한계는 다음과 같습니다:

  • B1/S1/P1 인스턴스 당 1,920 연결
  • B2/S2/P2 인스턴스 당 3,968 연결
  • B3/S3/P3 인스턴스 당 8,064 연결
  • 앱 서비스 환경 당 최대 64K 연결

연결 관리를 잘 못하는 어플리케이션은 접속 제한 문제를 늘 겪습니다. 보통은 많은 부하를 받을 때 외부 접속도 많아지므로 이런 문제가 있는 어플리케이션은 부하가 많아질 시점에 원격 접속 실패 메시지도 그만큼 자주 나오곤 합니다. 접속 실패시 다음과 같은 메시지를 보게 됩니다: “액세스 권한에 의해 숨겨진 소켓에 액세스를 시도했습니다. aaa.bbb.ccc.ddd (An attempt was made to access a socket in a way forbidden by its access permissions aaa.bbb.ccc.ddd)”

이런 문제를 줄이기 위한 몇가지 방법이 있습니다:

  • ADO.NET/EF를 사용하는 .NET 어플리케이션은 데이터베이스 연결 풀링을 사용하세요.
  • php/MySql의 경우 지속적인 데이터베이스 접속(persistent database connections)을 사용하세요.
  • Node.js에서 아웃바운드 HTTP/HTTPS 요청을 할 때는 keep-alives를 설정해서 연결을 재사용하세요. 설정에 대한 자세한 내용은 다음 링크(bit.ly/2iGrcoo)를 참고하세요.
  • .NET 어플리케이션에서 아웃바운드 HTTP/HTTPS 요청을 할 때는 System.Net.Http.HttpClient 인스턴스를 이용해서 연결 풀을 재사용하거나 System.Net.HttpWebRequest 사용할 때 Keep-alive 연결로 설정하세요. 참고: System.Net.ServicePointManager.DefaultConnectionLimit 숫자를 늘리세요. 기본값은 하나의 엔드포인트마다 2개의 동시접속으로 제한되어 있습니다.

App Service 샌드박스에는 몇 가지 제약이 더 있습니다. 지금까지 언급했던 제약보다 저수준의 제약 사항이며 자세한 내용은 다음 링크(bit.ly/2hXJ6lL)를 참고하시기 바랍니다.

정리하기

Azure 앱 서비스는 웹, 모바일, API 어플리케이션에 알맞은 PaaS 입니다. 앱 서비스 내부는 유동적인 요소가 많지만 개발자가 어플리케이션 개발에만 집중할 수 있도록 추상화했습니다. 이제 여러분들이 전세계로 어플리케이션을 확장하는데 필요한 복잡한 고민은 앱 서비스가 처리해줄 것입니다.

우리가 꼽는 앱 서비스의 모범적인 사례는 대부분 어플리케이션 확장에 대한 내용입니다. 앱 서비스 계획 안에서 어플리케이션이 웹 작업자와 어떻게 매핑되는지 잘 이해할 수록 확장 규모를 최적화하는데 도움이 됩니다.

Azure와 Azure 앱 서비스는 우리가 클라우드-퍼스트를 제창한 이래 빠르게 발전하고 있습니다. 2017년에도 새로운 혁신은 계속 될 것입니다.

덧붙임: 확장 단위 내의 구성요소 관계에 대해

이 글을 읽으면 확장 단위의 각 요소가 매우 강한 의존관계에 있는 것처럼 보일 수 있습니다. 하지만 설계상 각 요소는 느슨하게 연결되어 있습니다. 웹 작업자 외에 다른 역할이 비정상적인 상태여도 HTTP 트래픽을 처리하고 있는 어플리케이션은 계속 HTTP 트래픽을 처리할 수 있습니다.

예를 들어, 게시자가 정상동작 하지 않으면 FTP 접속을 할 수는 없지만 어플리케이션의 HTTP 트래픽에는 영향이 없고 다른 배포 기능에도 영향이 없습니다. API 콘트롤러에 있는 버그로 새 어플리케이션을 만들 수는 없더라도 이미 확장 단위에 있는 어플리케이션은 문제없이 동작합니다.


Yochay Kiriaty Microsoft Azure 팀의 principal program manager 입니다. 앱 서비스 플랫폼에서 웹, 모바일, API, functions를 이끌고 있습니다. Kiriaty는 90년대 후반부터 웹 기술 분야에서 일을 했고 성능과 확장을 중요하게 생각합니다. 이메일: yochay@microsoft.com 트위터: @yochayk

Stefan Schackow Azure 앱 서비스 팀의 program manager 입니다. Azure 웹앱을 처음 클라우드 서비스로 선보일 때부터 일했습니다. 현재는 Azure 앱 서비스의 배포와 개발을 책임지는 program manager들을 이끌고 있으며, Microsoft의 온프레미스/하이브리드 제품군(Azure Pack 및 Azure Stack) 개발도 책임지고 있습니다. 이메일: stefsch@microsoft.com

이 문서의 리뷰를 맡아준 Eduardo Laureano와 Nir Mashkowski에게 감사드립니다.

이 문서를 번역한 김영재 교육서비스 바로풀기의 개발사 Bapul의 CTO로서 기술로 교육에 새로운 시각을 주기 위해 열심히 개발하고 있습니다.

Power BI로 유저 리텐션 간단히 알아내기

Power BI는 Microsoft에서 만든 데이터 분석/시각화 도구입니다. 데이터 덩어리를 간단한 조작으로 조합하여 데이터 간 연관성을 파악하고 그 결과를 시각적으로 표현할 수 있게 해줍니다.

리텐션이란, 서비스를 이용하는 사용자가 얼마나 오랫동안 우리 서비스를 사용하는지를 의미합니다. 처음 사용한 후 다시는 안오면 리텐션은 0%이며, 처음 사용한 사람 100명 중, 다음 날 20명이 재방문했으면 ‘일간 리텐션 20%’라고 말합니다. 웹서비스 통상적으로는 특정 서비스를 사용해 본 이후 30일 내에 적어도 한번은 더 사용해 본 사람의 비율을 말한다고 하는군요. 한편, 첫날 사용 후 바로 다음날 사용 비율을 가장 중요하게 따지기도 합니다.

이 글에서는 Power BI를 사용하여 어느 서비스의 로그에 사용자ID와 타임스탬프만 있다고 할 때 리텐션을 간단히 시각화하는 법을 알아보겠습니다. 최종적으로 아래와 같은 그래프를 보게 됩니다.

powerbi11

참고로 이 글은 저의 Power BI 포럼 질답을 정리한 글입니다. 원글 링크는 아래 링크에 있습니다.

http://community.powerbi.com/t5/Desktop/I-have-timestamp-and-userid-how-do-I-get-number-of-users-for/td-p/101086

이 글은 다음의 순서로 되어있습니다.

  1. Power BI Desktop 설치
  2. 데이터 준비하기
  3. 데이터 가져오기
  4. 데이터 연결하기
  5. 시각화 하기

1. Power BI Desktop 설치

컴퓨터에서 사용할 수 있는 Power BI는 크게 세가지입니다.

  • 데스크톱: 대량의 데이터를 가져와서 조합하고 시각화한 후 내 계정에 업로드할 수 있습니다.
  • 윈도우앱: 업로드한 정보의 보여주기용으로 사용됩니다. 회사에 큰 터치스크린 키오스크가 있을 때 이 앱을 이용해서 보기 좋은 통계 페이지를 내걸 수 있습니다.
  • 웹: 간단한 수준의 데이터 조작과 시각화를 만들 수 있습니다. 데스크톱보다 강력하진 않습니다.

여기서는 데스크톱을 이용해서 다루겠습니다. 다운로드는 아래 링크를 클릭해주세요.

https://powerbi.microsoft.com/ko-kr/desktop/

2. 데이터 준비하기

이 글에서는 아래와 같이 방문 로그가 쌓여있다고 가정합니다.

유저ID………타임스탬프
1……………..2017-01-01
23……………2017-01-03
23……………2017-01-03
76……………2017-01-03
23……………2017-01-05
27……………2017-01-05

이것은 가장 기본적인 로그 형태로, 유저 23번처럼 동일한 유저에 대한 이벤트가 여러번 기록되어 있습니다. 유저23번은 1월 3일 두 번, 5일 한 번 방문했다는걸 알 수 있네요! 이 결과를 아래와 같이 출력하고 싶습니다.

사용시간…………….이용자 수
2일 이용자…………….12 명
3일 이용자………………5 명
5일 이용자………………3 명
9일 이용자………………1 명

나중에 위의 표를 그래프로 그리면 흔히 보는 리텐션 그래프가 나오겠지요. 즉, “1일차 대비 2일차 이용자는 70% 감소한다” 같은 통계를 볼 수 있을 것입니다.

저는 CSV 파일로 데이터를 준비했고, 이를 엑셀로 열면 아래와 같습니다. 참고로 Power BI는 SQL 데이터베이스를 비롯해서 여러 소스를 지원하므로 틀만 만들어놓으면 거의 실시간으로 데이터를 표시할 수 있습니다.

%ec%ba%a1%ec%b2%98

데이터가 준비되었다면 이제 본격적으로 해볼까요?

3. 데이터 가져오기

우리가 알고 싶은건 ‘하루 단위의 유저 리텐션’이지만 우리에게 있는 데이터는 ‘하루에 여러번 방문’한 기록이므로 ‘하루에 여러번 온 유저는 한 번으로 취급’하기 위해 일별 그룹핑을 할 것입니다. 이를 통해 우리는 ‘일별 중복없는 유저ID‘를 얻게 됩니다.

일단 아무 생각 없이 데이터를 불러와봅시다.

powerbi0

첫 화면에서 위 그림처럼 냅다 데이터 가져오기를 누릅니다. CSV 파일을 선택하면 간략한 데이터를 보여주는데 여기서 아래 그림처럼 ‘편집’ 버튼을 눌러서 일별 그룹핑을 해야 합니다.

powerbi12

그룹핑은 다음과 같이 설정합니다.

powerbi8

위의 말은 무슨 뜻이냐면, ‘해당 날짜에 속한 기록끼리 모아줘. 그 다음 각 날짜에 동일한 이름표 붙은 기록은 하나로 퉁쳐줘’라는 뜻입니다. 확인을 누르면 아래 그림처럼 ‘개수’라는 새로운 열이 만들어집니다. 몇 개 기록을 하나로 퉁쳤는지 그룹핑 결과를 보여주는 값인데 우리는 사용하지 않을겁니다. 이제 좌상단에 ‘닫기 및 적용’을 누르세요.

powerbi9

4. 데이터 연결하기

각 날짜에 어떤 유저ID가 있는지 알게 되었다면, 이제 각 유저ID가 몇 일 몇 일에 출현했는지를 셀 수 있어야 합니다. 결론적으로, 2일 출현한 유저들, 4일 출현한 유저들끼리 묶으면 ‘몇 일 사용한 유저가 얼만큼이다‘라는 값을 얻을 수 있겠지요.

다시 한 번 동일한 데이터를 불러봅시다. 이미 한 번 가져왔던 데이터는 ‘데이터 가져오기’ 버튼 오른쪽에 있는 ‘최근 소스’ 버튼을 누르면 원클릭으로 불러올 수 있습니다.

powerbi2

간략한 데이터를 보여주는 화면이 뜨면, 이전과 같이 ‘편집’을 눌르세요. 이제 UserId 열에 우클릭을 해서 ‘중복 제거’를 하고, Date 열은 우클릭해서 삭제합니다. 이 데이터 이름은 UserIds 라고 합시다. 좌상단에 ‘닫기 및 적용’ 버튼을 눌러서 닫습니다.

powerbi5

세번째 메뉴를 누르면 아래 그림과 같이 두 테이블이 연결되어있다고 보여줄거에요. 뭐 별 의미는 없습니다.

powerbi6

다시 두번째 메뉴로 와서, 아래 그림을 참고하여 새 열을 추가해보세요. 함수로 아래와 같이 입력합니다.

DaysUsed = CALCULATE(COUNTROWS(Logs))

참고로, 위의 함수에서 Logs는 처음 부른 데이터 이름입니다. 데이터 이름은 언제든 수정할 수 있어요.

powerbi7

새로 추가한 열은 각 유저가 몇 일 사용했는지를 나타냅니다. 이 데이터를 해석하면, 우리가 7일치 데이터를 가졌을 때 결과로 7이 나온다면 그 유저는 7일간 매일 사용한 셈입니다. 1은 가입 직후에만 써보고 6일간(어쩌면 영원히) 사용 안한 것이지요. 데이터 한 번이 1이므로 0은 있을 수 없습니다.

이제 데이터 정리는 끝났습니다. 정리하면, 첫 데이터에서 일별 그룹핑을 하고, 두번째 데이터에서 UserID를 고유하게 만든 후에 각각이 일별로 출현한 횟수를 셈한 것입니다.

5. 시각화하기

이제 마지막입니다. 우리는 ‘몇 일 사용한 유저가 몇 명인지‘를 그래프로 그릴 것입니다. 마우스만 움직이면 됩니다.

첫번째 메뉴에서 아래 그림처럼 설정해보세요. 시각화에서 막대그래프를 선택하고 두 개의 값만 드래그앤드롭하면 끝입니다!

powerbi10

각 그래프에 마우스를 올려보면 정확한 숫자도 나옵니다. 첫째날과 둘째날은 각각 6339, 2574네요. 계산해보면 첫 날 대비 둘째 날 유저는 약 40.6% 잔존하는 것을 확인할 수 있습니다.

번외: 통계의 함정

이 글에는 함정이 있습니다. 분명 여기까지 안읽고 뭐라고 하는 사람이 있겠지

정확한 의미의 유저 리텐션은 아닌데요. 원래 유저 리텐션은 ‘얼마나 오랜기간 사용하는가’를 중요시 하지만 이번에는 특정 기간 내 몇 번 사용했는가’를 따진 것이므로 적확한 통계가 아닙니다. 즉, 다음날 한 번 사용하고 그 후로 떠나도”2″, 5일간 안쓰다가 6일째 한 번 사용한 날도 “2”로 찍히죠.

그러므로, ‘User Activity Frequency’ 정도가 맞는 표현일 것입니다. 만일 우리가 데이터를 첫날 가입 기준으로 Date 값을 +1일, +3일 정도로 만든 후(offset 처리죠)에 이 글의 기법을 적용하면 가입 이후 일정 기간동안 몇 일 사용했는지를 알 수 있겠습니다.

결론적으로, 이 글에 소개된 기법으로 알 수 있는 정확한 의미의 유저 리텐션은 2일간의 데이터여야 할 것입니다. 왜냐하면 3일이 되는 순간 몇 일에 안썼는지 알 수 없기 때문이죠. 2일짜리로 데이터로 구하면 값은 둘 중 하나죠 – 1 또는 2입니다. 이를 통해 2일차 잔존율을 구할 수 있을 것입니다. 하지만 2일간의 데이터만으로도 충분히 값집니다! 왜냐하면 서두에 인용했듯, 웹서비스는 2일차 잔존이 가장 중요하고 나머지는 이에 따라 정해진 반감기를 가지는 패턴을 보이기 때문이지요.

이상입니다.

[번역] 최신 기술 – Event Sourcing 처음 적용하기

원문: https://msdn.microsoft.com/magazine/mt422577

뭐든 큰 변화없이 언제나 뻔하다고 생각하면 어느새 신경도 쓰지 않게 됩니다. 데이터 저장소를 생각할 때 우리는 당연히 데이터의 현재 상태가 저장되어 있겠거니 합니다. 보험, 금융과 같이 큰 규모의 프로젝트는 모든 이력을 정확히 추적하고 기록해야하지만, 그렇지 않은 대부분의 어플리케이션과 웹사이트는 현재 상태만 저장해도 충분합니다.

이처럼 현재 상태를 저장하는 방식은, 시스템의 현재 상태를 스냅샷으로 찍어서 보존한다고 표현할 수 있습니다. 데이터는 보통 관계형데이터베이스에 저장합니다. 이렇게만 해도 새 트랜젝션을 만들고 과거의 트랜젝션 결과를 가져올 수 있습니다. 여기까지가 지난 수 십년간의 ‘뻔한 것’이었습니다.

오늘날 비즈니스는 따라오기 벅찰 정도로 빠르게 변하고 있습니다. 그래서 비즈니스와 도메인에서 일어나는 이벤트를 정확하게 추적해야 하는 경우도 많아졌습니다. 이벤트소싱(ES; Event Sourcing)은 스토리지 설계와 데이터를 저장하고 가져오는 방식에 영향을 주는 패턴입니다. 또한, 도메인에서 비즈니스 이벤트를 저장하고 보는 수준에 그치지 않고 데이터 프로젝션까지 즉시 만들 수 있는 패턴입니다.

이벤트소싱은 비즈니스를 기록하고 살펴보기에 똑똑하고 멋진 방법입니다. 데이터 저장 모델로는 꽤 새로운 이론이며 관계형 모델이 처음 등장했을 때만큼 참신합니다. 최근 등장한 NoSQL 보다 큰 변화를 줄 수도 있습니다. 물론 이벤트소싱은 현재 활발히 사용 중인 관계형이나 NoSQL을 대체하는게 목적은 아니며, 여러분은 이벤트소싱을 이들 둘의 상위 개념으로 구현할 수 있습니다. 이벤트소싱은 특정 시점의 상태로만 취급했던 데이터를 이벤트 단위로 다룹니다. 이벤트소싱을 사용할 수록 우리는 데이터에 대한 시각도 새로워질 것입니다.

이벤트소싱을 적용하면 무엇이 좋은가?

현재 상태만 저장하는 저장 모델에서 한 단계 발전한 형태로 갱신 이력을 추적하는 모델이 있습니다. 서점관리 프로그램을 생각해봅시다. 책마다 설명 속성이 있고 여러분은 속성 수정 권한이 있습니다. 이 때 속성에 대한 수정 이력을 보존해야할까요?

요구사항은 상황마다 다르겠지만, 이번 예시에서는 이러한 변경내역 추적이 중요한 기능이라고 합시다. 어떻게 구현할 수 있을까요? 한가지 방법은, 현재 상태를 저장하는 테이블 하나와 변경 내역을 저장하는 별도의 테이블로 구성하는 것입니다. 업데이트할 때마다 하나의 레코드가 추가되며 업데이트 기록에는 변경한 컬럼과 변경한 내용을 저장합니다.

다른 방법으로 시도해볼까요? 하나의 테이블에 하나의 책에 대해서도 여러 개의 레코드를 기록합니다. 각 레코드는 그림 1과 같이 타임스탬프와 현재 상태를 순서대로 저장합니다.

Multiple Records Hold Entity History
그림 1 엔티티 변경 내역을 여러 개의 레코드로 저장하는 형태

위와 같이 구성하면 현재 상태를 가져오기 위한 별도의 API를 만들어야 합니다. 단순히 레코드ID로 쿼리해서는 최신 상태를 가져올 수 없고 타임스탬프 상의 최신이거나 업데이트카운트가 가장 큰 값을 가져오도록 만들어야 합니다. 입력한 데이터 엔티티에 대한 모든 이벤트는 하나의 흐름으로 표현할 수 있는데, 이처럼 이벤트를 흐름으로 표현하는 것이 이벤트소싱의 핵심입니다. 그러므로 이벤트를 원활하게 추적하는 시스템을 구현하고 싶다면 이벤트소싱이 정답입니다.

기존의 개념 중 이벤트소싱과 관련있어 보이지만 다른 개념도 있습니다. 이벤트소싱이 로깅이나 감시 기능과 유사하다고 생각할 수 있지만, 로깅은 예외 상황이나 프로파일링까지 고려한다는 점에서 다릅니다. 이벤트소싱은 비즈니스 이벤트에 대해서만 다룹니다. 그러므로, 로깅 기능을 구성할 때처럼 여러 도메인과 구조를 관통하는 공통된 역할을 콤포넌트화 하는 작업과도 다릅니다. 이처럼 공통 부분을 정의하는 과정을 Aspect-orient 소프트웨어에서는 공통의 관심사(cross-cutting concern; 횡단관심사라고도 함)라고 하는데 이와는 다른 의미입니다. 이벤트소싱은 데이터를 어떤 구조로 설계하고 저장하는지에 대한 내용으로 봐야 합니다.

이벤트소싱이란

이벤트소싱은 이벤트를 데이터 소스로 간주합니다. 수 십년간 개발자들은 이벤트에 대해서 그다지 중요하게 생각하지 않았습니다. 어쩌면 그런 이유로 이벤트소싱이 주목받지 못했는지도 모릅니다. 이벤트소싱이 딱히 쓸모있지 않다고 생각해도 괜찮습니다. 아직 필요하지 않을 뿐입니다.

이벤트소싱은 도메인 전문가가 이벤트를 순서대로 추적하고 싶을 때 특히 유용합니다. 소극적으로 사용한다면, 워크플로우를 표현하거나 비즈니스 로직을 일원화할 때도 유용합니다. 다만 이처럼 소극적으로 사용할 경우에는 이벤트를 보존할 필요성도 적고 이벤트를 최우선 순위로 취급하지도 않습니다. 이 정도가 요즘 흔히 사용하는 이벤트소싱 시나리오입니다. 이 글에서는 이벤트를 데이터 소스로 사용하도록 하겠습니다.  이벤트소스를 도입하려면 저장소에서 두 가지를 고려해야 합니다. 보존과 쿼리입니다. 여기서 보존이라함은 세가지 핵심 작업과 관련되어 있습니다. 바로 삽입/갱신/삭제입니다. 이벤트소싱 시나리오에서의 삽입은 현재 상태만 보존하는 통상의 시스템과 다를 바 없습니다. 요청을 받으면 새 이벤트로 저장합니다. 이벤트에는 GUID와 같은 고유식별자를 함께 기록하며, 그 외에 해당 이벤트의 타입 이름과 코드, 타임스탬프, 기타 정보도 저장합니다.

이벤트소싱에서 갱신 작업은 삽입의 다른 표현일 뿐입니다. 어떤 프로퍼티가 변경되었고 새 값은 무엇인지, 관련된 비즈니스 도메인이 무엇인지, 그 외에 변경 사유 등을 기록합니다. 갱신이 한 번 일어난 저장소의 데이터는 그림 2와 같습니다.

A New Record Indicates Update to Entity with ID #1
그림 2 Entity ID #1에 대해 갱신이 일어났음

이와 마찬가지로 삭제 작업은 해당 엔티티를 삭제했다는 정보를 ‘삽입’합니다.

갱신 작업은 쿼리할 때 새로운 고민거리를 줍니다. 갱신하기 전에 갱신할 대상이 이미 있는지, 현재 상태가 어떤지는 어떻게 알 수 있을까요? 먼저 간단한 쿼리 레이어를 하나 만들어서 ID를 조회하고, 그 다음 현재의 값에 기반하여 새 값으로 갱신하는 이벤트를 삽입해야 합니다.

예를 들면, Created 이벤트를 먼저 가져온 후 그 내용에 맞추어 새 데이터를 추가하는 방법입니다. 현재 상태의 값은 해당 ID에 대한 모든 이벤트를 조회한 후 처음부터 짚어나가면 구할 수 있습니다. 이런 방법을 ‘이벤트 리플레이’라고 합니다. 하지만 단순히 모든 이벤트를 재생해서 상태를 재구성하는 방법으로는 성능에 큰 문제가 발생합니다. 은행 계좌의 현재 잔액을 알려면 수 년 전의 계좌 개설일부터 현재까지의 모든 거래를 가져와야 할테니까요. 그리 좋은 방법은 아닙니다.

그래서 이와 같이 모든 이벤트를 가져와야 하는 문제를 해결하는 방법 중 하나로 스냅샷을 만드는게 있습니다. 스냅샷은 특정 시점의 상태를 저장한 레코드입니다. 스냅샷을 만들면 적어도 모든 이벤트를 리플레이할 필요는 없습니다.

구현에 있어서, 이벤트소싱은 특정 기술이나 제품이 아닙니다. 그러므로 관계형 데이터베이스를 사용하든 NoSQL을 사용하든 상관 없습니다. 그 대신, 이벤트소싱을 소프트웨어 콤포넌트 개념으로 본다면 ‘이벤트 저장소(event store)’를 구현한다고 말할 수 있겠습니다. 이벤트 저장소는 이벤트 로그를 구현하는 작업과 별반 다를 바 없습니다. 그러므로 최소한의 기능만 충족한다면 데이터베이스가 제공하는 API를 이용하여 직접 만들어도 무방합니다.

이벤트 저장소는 두 가지 전제를 가지고 있습니다. 먼저, 추가만 가능하고 갱신은 없습니다. 삭제 또한 삭제 표식을 추가할 뿐 이벤트를 지우지 않습니다. 둘째, 요청하는 이벤트 ID에 맞게 이벤트 스트림을 반환할 수 있어야 합니다. 이 두가지 기능만 있으면 이벤트 저장소의 기본 요건은 충족됩니다.

이벤스 저장소 구현 시 고려사항

앞서 말했듯이, 이벤트 저장소는 기본 기능만 충족하면 특정 기술과 상관없이 구현할 수 있습니다. 데이터를 보존하는 부분은 보통 관계형 데이터베이스나 NoSQL을 사용합니다. 관계형데이터베이스로 구현한다면 한 이벤트마다 하나의 레코드를 가지도록 하고 하나의 테이블은 하나의 엔티티 타입을 가지도록 구현하는 식입니다.

이벤트는 다양한 포멧이 있습니다. 예를 들어, 모든 이벤트에 공통된 속성도 있지만 그렇지 않을 수도 있습니다. 물론 이벤트가 최대한 공통된 속성을 가질 수록 구현하기에 좋습니다. 그렇게 하기 어렵다면 행 단위(row)로 레코드를 쌓지 않고 SQL Server 2014에 추가된 기능인 Column Store Index를 이용하여 열 단위, 즉 컬럼 하나씩 쌓도록 테이블을 구성할 수도 있습니다. 또 다른 방법이라면, 이벤트를 JSON 오브젝트로 만든 후 이를 문자열로 직렬화하여 하나의 문자열 컬럼에 넣을 수도 있습니다.

NoSQL에서는 다양한 속성을 담은 하나의 레코드를 “도큐먼트”라는 단위로 저장합니다. 일부 NoSQL 제품은 이러한 도큐먼트 저장에 특화되어있습니다. 개발자 입장에서는 클래스를 만들고, 값을 채우고, 그대로 저장하면 끝나므로 아주 간편합니다. 보통은 각 이벤트의 유형 별로 클래스를 만들어서 저장하도록 구현합니다.

진행 중인 프로젝트

이벤트소싱은 아직 구조적으로 성숙한 단계는 아닙니다. 무엇 하나 표준화된 규약이 없기 때문에 이벤트 저장소와 이를 이용한 개발 경험은 계속 발전해나갈 여지가 많습니다. 그러므로 이벤트소싱 솔루션은 직접 만들어도 좋습니다. 이 섹션에서는 이벤트 저장소를 좀 더 구조적으로 만들고 편리하게 다룰 수 있는 도구를 몇가지 소개하겠습니다.

이벤트소싱에 최적화된 이벤트 저장소를 사용하면 이벤트 기록과 읽기 작업에만 집중할 수 있어서 보다 효율적인 개발이 가능합니다. NEventStore (neventstore.org) 프로젝트는 이런 시도 중 하나입니다. 간단히 이벤트를 기록하고 다시 읽을 수 있고, 가장 중요한 점이라면 특정 저장소에 의존하지 않고 이를 선택할 수 있다는 점입니다. 아래 예시는 저장소로 SQL을 사용하고 있습니다.

var store = Wireup.Init()
  .UsingSqlPersistence("connection")
  .InitializeStorageEngine()
  .UsingJsonSerialization()
  .Build();
var stream = store.CreateStream(aggregateId);
stream.Add(new EventMessage { Body = eventToSave });
stream.CommitChanges(aggregateId);
위의 예시는 이벤트를 기록할 때이며, 이벤트를 읽을 때는 스트림을 열고 커밋된 이벤트 컬렉션을 한 흐름으로 볼 수 있습니다.
또 다른 프로젝트로 Event Store (geteventstore.com) 프로젝트가 있습니다. .NET과 HTTP API를 제공하며, 이 API로 이벤트를 취합하고 하나의 스트림으로 관리할 수 있습니다. 이벤트 스트림을 가지고 크게 세 가지의 작업을 할 수 있습니다. (1) 이벤트 쓰기 (2) 가장 최근의 이벤트 또는 이벤트의 특정 구간 읽기 (3) 갱신할 때 받아보기가 가능합니다.
받아보기(subscription) 기능은 기본적으로 스트림에 이벤트를 추가할 때마다 콜백 함수를 호출하지만 그 방식은 세가지로 나뉩니다.
  • Volatile: 설정 시점 이전의 이벤트는 무시합니다. 새로 입력한 이벤트부터 받습니다.
  • Catch-up: 정해진 시작 지점부터 이벤트를 받습니다. 이미 입력된 이벤트도 받을 수 있습니다.
  • Persistent: 하나의 이벤트를 여러 곳에서 받을 수 있습니다. 여러 곳에서 받을 경우에도 최소 한 번(at-least-once)을 보장하며 순서에 상관없이 여러번 받을 수도 있습니다.

(역자 주: 더 자세한 정보는 Event Store 기술문서를 참고해주세요.)

정리

이벤트소싱은 이벤트를 어플리케이션의 데이터소스로 사용합니다. 어플리케이션을 만들 때 데이터의 마지막 상태만 다루는게 아닌 비즈니스 이벤트의 흐름을 기준으로 설계하고 개발할 수 있습니다. 저장하는 이벤트 데이터는 아주 저수준의 정보이므로 현재 상태를 알기 위해서는 별도의 투사(projection) 과정을 거쳐야 합니다. 투사란, 이벤트 리플레이를 하면서 특정 시점에 대한 데이터의 상태값을 만드는 일련의 처리 과정을 말합니다. 이벤트를 이용하면 어떤 형태로든 다양한 형식의 투사 결과를 만들 수 있습니다. 그리하여, 현재의 상태값 또한 다양한 형식으로 맞춰서 구할 수 있습니다.


Dino Esposito “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014), “Programming ASP.NET MVC 5” (Microsoft Press, 2014)의 공동저자입니다. JetBrains에서 .NET과 안드로이드 분야의 기술 에반젤리스트이며, 세계 곳곳의 여러 행사에서 연사로 활동하고 있습니다. Esposito의 소프트웨어에 대한 비전은 다음 링크에서 볼 수 있습니다.  software2cents.wordpress.com, 트위터: @despos.

이 문서의 리뷰를 한 Microsoft 기술 전문가 Jon Arne Saeteras에게 감사드립니다.

이 문서를 번역한 김영재 교육서비스 바로풀기의 개발사 Bapul의 CTO로서 기술로 교육에 새로운 시각을 주기 위해 열심히 개발하고 있습니다.

[번역] 최신 기술 – CQRS 처음 도입하기

원문: https://msdn.microsoft.com/magazine/mt147237

도메인 주도 개발(이하 DDD;Domain-driven design)은 십여년 전부터 소프트웨어 개발자와 아키텍트들에게 많은 영향을 주었습니다. 분명한 장점과 단점을 모두 가진 DDD이지만, 객체지향 개발자들은 이를 통해 오래된 꿈을 실현하려고 했습니다. 그 꿈은 바로, 모든 이해관계자의 요구사항을 해소해주는 온전한 오브젝트 모델로 응용 프로그램을 만드는 것이지요.

지난 수십 년간, 많은 개발자들은 DDD 가이드라인에 따라 프로젝트를 수행했습니다. 그 중에는 성공한 프로젝트도 실패한 프로젝트도 있습니다. 결국 깨달은 진실이 있다면, 소프트웨어에서 기능적인 요소든 그 외의 요소든 모든 것을 아우른 오브젝트 모델이란 그저 환상일 뿐이라는 것입니다.  특히 요즘처럼 고차원의 UX, 급변하는 비즈니스 모델, 시도때도 없이 달라지는 요구사항이 들이닥치는 바쁜 세상에서 견고하고 안정적인 오브젝트 모델을 만들려는 것은 더 허황된 꿈처럼 느껴집니다.

최근 이에 대한 남다른 해결법으로 주목을 끌고 있는 용어가 있습니다. 바로 Command and Query Responsibility Segregation (CQRS; 명령과 쿼리의 역할구분) 입니다. CQRS가 소프트웨어 개발 방법론에서 새롭게 등장한 개념은 아닙니다. 구현도 그리 복잡하지 않습니다. CQRS는 그저 소프트웨어의 생애주기나 복잡도에 구애받지 않으면서 대부분의 소프트웨어 개발에 적용하기에 적합한 구현 패턴일 뿐입니다.

CQRS를 구현하는 방법은 취향따라 최소 세가지가 있습니다. 여러분이 뭐라고 이름 붙이든 상관없습니다. 호텔방이나 음료수를 구분할 때처럼 일반/프리미엄/디럭스로 이름붙여 봅시다. CQRS로 검색해서 나오는 대부분의 예시와 내용은 대부분 디럭스급이라고 보면 됩니다. 그런 것들은 평범한 어플리케이션에 적용하기엔 너무 복잡하고 과분합니다.

CQRS는 프로젝트의 복잡도와 상관없이 성공적으로 적용할 수 있는 소프트웨어 개발 방법론입니다. 최종적으로 CQRS는 전통적인 다층 레이어 아키텍처를 좀 더 많은 변화를 받아들이도록 재구성하는 것입니다.

명령과 쿼리

버트란드 마이어(Bertrand Meyer)가 1980년대에 Eiffel이라는 프로그래밍 언어를 개발할 때, 소프트웨어는 시스템의 상태를 바꾸는 것과 시스템의 상태를 읽는 두가지의 명령으로 나뉜다고 했습니다. 모든 소프트웨어 명령어는 명령과 쿼리 둘 중 하나라고 말입니다. 둘의 조합된 형태도 아니고 반드시 둘 중 하나에 속해야 합니다. 좀 더 세련된 표현으로 말하자면, 질문을 아무리 해도 답변은 변하지 않아야 합니다. CQRS는 이 이론을 현대적으로 복기한 것입니다. 명령과 쿼리를 구별하여 별개로 구현합니다.

명령과 쿼리를 논리적으로 나누기란 어려운 일입니다. 특히 둘 모두가 동일한 프로그래밍 스택과 동일한 모델을 사용한다면 더더욱 어렵습니다. 복잡한 비즈니스 시나리오에서는 더 어려운데, 왜냐하면 오브젝트든 함수형이든 뭐든간에 어떤 모델이라도 금세 손 쓸 수 없을 정도로 복잡해지곤 하기 때문입니다. 이렇게 모델이 급격하게 커지고 복잡해지면 시간과 예산을 잡아먹고 원래 의도했던대로 동작하지도 않을 것입니다.

CQRS는 기본적으로 쿼리 작업은 쿼리 작업끼리 한 레이어에, 명령 작업은 또 다른 레이어에 그룹핑해서 구분합니다. 각 레이어는 독립적인 데이터 모델과 서비스를 사용하고 독자적인 패턴과 기술을 조합해서 구현합니다. 중요한 점은, 두 레이어를 각자 고유의 2-tier로 구현할 수 있고 최적화도 구분해서 적용함으로써 서로에게 영향을 주지 않도록 하는 것입니다. 그림 1은 CQRS 구조의 기초적인 부분을 보여줍니다.

A Canonical and Multi-­Layered CQRS Architecture 

그림 1 기본적인 다층 레이어의 CQRS 구조

명령과 쿼리를 별개로 인식하면 소프트웨어 구조에 큰 변화를 줄 수 있습니다. 일례로, 각자의 도메인 레이어에 대해서만 모델링하고 코딩하기 때문에 훨씬 단순합니다. 명령 스택은 데이터, 비즈니스, 보안 규칙만 고려해서 개발하고, 쿼리 스택은 가장 간단하게는 DB 커넥션에 SQL 쿼리문만 작성하면 됩니다.

프레젠테이션 계층에 보안 규칙을 넣는다면, 쿼리 스택은 Entity Framework와 같은 ORM을 얇게 감싸고 데이터를 조회하는 수준일겁니다. 도메인 레이어마다 데이터를 해당 도메인의 요구사항에 최대한 맞춰서 표현하기에도 수월합니다. 데이터를 굳이 복사하거나 누더기로 만들지 않고도 말입니다.

DDD가 처음 나왔을 때는 소프트웨어 개발의 복잡함을 따져보려는 의미가 컸습니다. DDD를 시도하는 개발자들은 꾸준히 이 복잡함과 씨름해왔습니다. 대부분은 비즈니스 도메인에 대한 이야기였습니다. 하지만 대부분의 복잡한 것들은 명령과 쿼리의 곱집합 때문이었습니다. 쿼리에서 명령 부분을 떼어내면 복잡도가 한자릿수로 줄어듭니다. 단순히 수학적으로 표현하자면, 통상적인 도메인모델 기반으로 구현한 복잡도가 NxN이라면 CQRS는 N+N인 셈입니다.

CQRS 시작하기

기존의 CRUD 시스템도 얼마든지 CQRS 형태로 바꿀 수 있습니다. 예를 들어 사용자가 다양한 정보를 입력하는 폼이 있는 전통적인 ASP.NET MVC 웹 어플리케이션이 있다고 합시다. 대부분의 어플리케이션이 하는 일이기에 아키텍트들은 어떻게 해야 이런 어플리케이션을 빠르고 효율적으로 만들지 잘 알고 있습니다. 이제 여러분은 이를 CQRS로 재해석해서 만들어볼 것입니다. 아마도 바뀔 부분이 거의 없어서 놀랄 것입니다. 그에 비해 얻는 장점은 너무나 많습니다.

기존의 시스템은 대부분 여러 계층으로 이루어져 있습니다. 여러분의 시스템에 콘트롤러에서 직접 호출하는 어플리케이션 서비스가 있다고 가정해봅시다. 콘트롤러와 어플리케이션 서비스는 웹 서버 안에 있습니다. 그림 1과 같이, 어플리케이션 서비스는 어플리케이션 계층을 이룹니다. 그래서 어플리케이션 계층은 시스템에 명령과 쿼리를 실행하는 플랫폼이기도 합니다. CQRS를 적용한다는 말은 곧 역할이 둘로 구분된 중간계층을 가진다는 말이기도 합니다. 하나는 시스템 상태를 바꾸는 명령에 대한 것이고, 다른 하나는 데이터를 가져오는 것입니다. 그림 2는 이에 따라 ASP.NET MVC 프로젝트의 구조를 나타낸 다이어그램입니다.

 

The CQRS Architecture for an ASP.NET MVC Project
그림 2 ASP.NET MVC 프로젝트에서의 CQRS 구조

먼저 두 개의 클래스 라이브러리 프로젝트를 만듭니다. 쿼리 스택과 명령 스택 라이브러리입니다. 그리고 웹 서버 프로젝트에 모두 참조로 추가합니다.

쿼리 스택

쿼리 스택 클래스는 데이터를 가져오기만 합니다. 개발을 할 때 프리젠테이션 레이어에 최대한 일치하도록 데이터 모델을 만듭니다. 이 때 비즈니스 규칙은 거의 고려할 필요가 없습니다. 왜냐하면 비즈니스 규칙이란 주로 상태를 바꾸는 것이므로 명령 스택에서 구현하기 때문입니다.

DDD에서 유행한 도메인 모델 패턴은 도메인 로직을 조직하는 방법에 대한 것이었습니다. 이렇게 복잡하게 갈 필요 없이, 시스템의 프론트엔드에서 실행할 쿼리를 만들 때는 단지 어플리케이션 로직의 일부와 사용 시나리오만 신경쓰면 됩니다. 실상 ‘비즈니스 로직’이라는 말은, 변하지 않는 도메인 로직 위에 어플리케이션 별 로직을 엮은 결과물입니다. 보여주는 정보의 정해진 형식과 프레젠테이션 포멧을 알면 그저 SQL 쿼리로 나온 데이터를 매핑하는 작업만 해주면 됩니다.

어플리케이션 계층에서 실행하는 모든 코드는 시스템의 비즈니스 도메인을 반영한 결과입니다. 그러므로 시스템의 핵심 로직에 대한 API는 변하지 않아야 합니다. 이상적으로는, 핵심 로직에서 노출된 API는 그 자체로 완전무결해야 합니다. 즉, 어떠한 불일치도 없고 일관된 규칙을 가집니다. 쿼리 스택의 본질은 읽기전용이므로 아래 코드와 같이 Entity Framework 콘텍스트를 간단히 감싸고 있는 클래스를 만들 수 있습니다. 이제 감쌌다는 의미로 wrapper 클래스라고 부르겠습니다.

public class Database : IDisposable
{
  private readonly QueryDbContext _context = new QueryDbContext();
  public IQueryable<Customer> Customers
  {
    get { return _context.Customers; }
  }
  public void Dispose()
  {
   _context.Dispose();
  }
}

위 코드에서 QueryDbContext 클래스는 DbContext 클래스를 상속받았으며, DbSet<T> 콜렉션이 들어있습니다. 여기서는 QueryDbContext 클래스가 데이터베이스의 모든 테이블에 엑세스 할 수 있다고 가정합시다. 이렇게 하면 Linq to Entities 기능으로 쿼리를 수행할 수 있습니다.

쿼리 파이프라인을 만드는 첫 단계는 데이터베이스에 쿼리만 할 수 있도록 설정하는 것입니다. wrapper 클래스가 바로 이런 역할을 합니다. Database 가 IQueryable<T>만 노출하고 있기 때문입니다. 이로써 Database라는 wrapper 클래스를 사용하는 어플리케이션 계층은 쿼리를 구현해서 프리젠테이션으로 데이터를 보낼 수 있게 됩니다.

var model = new RegisterViewModel();
using (var db = new Database())
{
  var list = (from m in db.Customers select m).ToList();
  model.ExistingCustomers = list;
}

위 코드와 같이 데이터 원본과 프리젠테이션은 직접 연결되어있습니다. 이제 데이터를 표시하기 위한 용도로 데이터 읽기와 형식만 다루면 됩니다. 로그인 기능이나 UI에 제한을 둬서 데이터 접근을 제어하고 싶다면, 그저 데이터 접근까지 레이어를 더 추가하거나 IQueryable로 가져오는 데이터 콜렉션을 조절해서 구현할 수 있습니다. 데이터 모델은 데이터베이스와 동일하므로 1:1 관계입니다. 이렇게 IQueryable을 노출한 데이터 모델은 Layered Expression Trees (LET)라는 개념을 적용할 때도 유용하게 사용할 수 있습니다. (*역자주: LET는 Linq를 최대한 활용하여 데이터 모델을 풍부하게 표현하는 방법 중 하나입니다)

지금까지 논의한 내용 중 핵심 몇 가지를 정리해봅시다. 우선, 읽기전용 파이프라인에는 비즈니스 규칙이 없다는 것입니다. 인증 규칙과 필터링 외에는 특별히 고려할게 없습니다. 그리고 인증 규칙이나 필터링은 어플리케이션 계층에서 이미 잘 파악하고 있습니다. 데이터 전송 오브젝트(Data Transfer Object; DTO)를 복잡하게 다루지도 않습니다. 뷰에 표시하는데에 단 하나의 모델과 그 안에 실제 데이터만 있습니다. 그러므로, 어플리케이션 서비스는 아래와 같은 패턴일 것입니다.

var model = SpecificUseCaseViewModel();
model.SomeCollection = new Database()
     .SomeQueryableCollection
     .Where(m => SomeCondition1)
     .Where(m => SomeCondition2)
     .Where(m => SomeCondition3)
     .Select(m => new SpecificUseCaseDto
       {
         // Fill up
       })
     .ToList();
return model;

코드에 있는 데이터 전송 오브젝트는 프리젠테이션 전용으로만 사용하게 됩니다. 클래스를 만드는건 어쩔 수 없지만 그 클래스에는 사용자가 뷰에서 보고 싶어하는 정보만 있습니다. Where 절을 교체하는 IQueryable 확장메소드를 구현해서 그때그때마다 다른 것으로 볼 수 있도록 할 수도 있습니다. 이렇게 하면 특정 도메인에 대해 대화형으로 구현할 수도 있습니다.

쿼리 스택에서 고려할 점이 또 하나 있다면 데이터의 일관성(persistence)입니다. 간단한 형태의 CQRS는 명령과 쿼리 스택을 하나의 데이터베이스로 사용합니다. 이런 공용 구조는 CQRS로 구현해도 전통적인 CRUD 시스템과 유사해보입니다. 그러므로 변화에 저항감이 있는 개발자들에게는 CQRS를 도입하기에 보다 쉬워 보일 수 있습니다. 하지만 나중에는 명령과 쿼리 스택이 별개의 데이터베이스를 사용하도록 백엔드를 디자인해야 합니다. 각자의 목적에 맞도록 최적화해야 하기 때문입니다. 두 데이터베이스의 동기화는 다른 문제이므로 이 글에서 다루지는 않겠습니다.

명령 스택

CQRS 에서 명령 스택은 어플리케이션의 상태를 바꾸는 작업만 합니다. 어플리케이션 계층이 프리젠테이션에서 요청을 받으면 하나의 명령으로 구성한 후 이 명령을 파이프라인에 푸시합니다. 여기서 ‘명령을 파이프라인에 푸시한다’는 표현은 CQRS를 특징짓는 말이기도 합니다.

가장 단순하게는 트랜젝션 스크립트를 실행하는 것이 곧 명령을 푸시하는 것입니다. 트랜젝션 스크립트는 작업에 필요한 모든 과정을 처리한 워크플로우입니다. 그래서 어플리케이션 계층에서 명령을 푸시하는 작업은 아래 코드와 같이 구현할 수 있습니다.

public void Register(RegisterInputModel input)
{
  // Push a command through the stack
  using (var db = new CommandDbContext())
  {
    var c = new Customer {
      FirstName = input.FirstName,
      LastName = input.LastName };
    db.Customers.Add(c);
    db.SaveChanges();
  }
}

비즈니스 로직을 구현한 서비스 도메인 계층과 그에 관련된 도메인 모델을 좀 더 적극적으로 이용하면 보다 복잡한 구성을 만들 수도 있습니다. 하지만 CQRS를 구현하는데 반드시 DDD와 엮을 필요는 없습니다. DDD에서 언급하는 도메인 모델의 집합, 팩토리, 값 오브젝트와 같은 개념을 굳이 다루지 않아도 CQRS 구현에 문제되지 않습니다. 그저 명령과 쿼리의 구분을 명확히 해서 도메인 모델로 인해 만들어지는 복잡성을 줄일 수만 있다면, 그것이 바로 CQRS 도입의 장점일 것입니다.

CQRS 다음은 무엇일까

CQRS의 장점은 명령과 쿼리 파이프라인을 원하는대로 최적화할 수 있으면서도 다른 요소가 깨질 위험은 거의 없다는 것입니다. CQRS를 가장 기초적으로 시도하려면 단일 데이터베이스를 공유하고 어플리케이션 레이어에서 읽기와 쓰기를 별개의 라이브러리로 수행하는 방법이 있습니다.

좀 더 제대로 하려면 여러 개의 데이터베이스를 혼용하여 폴리글랏 저장소로 만들고 쿼리할 때나 이벤트 소싱에 대응하여 조합하는 것입니다. 이벤트 소싱은 명령을 백엔드에 보낼 때 더 유연하게 처리할 수 있으므로 중요합니다. 명령을 버스에 보내고 이를 이벤트로 배포하면 어떤 작업을 새로 정의하거나 수정하는 경우에 이를 플로우 차트를 다루듯이 관리할 수 있기 때문에 보다 유연합니다. 이와 동시에 버스의 성능과 기능을 추가하면 수직적으로 확장하는 효과를 얻을 수도 있습니다.

많은 개발자들이 CQRS에 찬사를 보내지만 대규모의 고차원적인 어플리케이션에만 어울린다고 생각하곤 합니다. CQRS 그 자체는 고수준의  아키텍처도 아니고 특정 기술에 종속적이지도 않습니다. 일부 디자인 패턴에 종속적일 수는 있지만, 단지 디자인 패턴일 뿐입니다. CQRS는 단순하고 강력하며 대부분의 어플리케이션에 잘 맞습니다.


Dino Esposito “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014), “Programming ASP.NET MVC 5” (Microsoft Press, 2014)의 공동저자입니다. JetBrains에서 .NET과 안드로이드 분야의 기술 에반젤리스트이며, 세계 곳곳의 여러 행사에서 연사로 활동하고 있습니다. Esposito의 소프트웨어에 대한 비전은 다음 링크에서 볼 수 있습니다.  software2cents.wordpress.com, 트위터: @despos.

이 문서의 리뷰를 한 Microsoft 기술 전문가 Jon Arne Saeteras에게 감사드립니다.

이 문서를 번역한 김영재 교육서비스 바로풀기의 개발사 Bapul의 CTO로서 기술로 교육에 새로운 시각을 주기 위해 열심히 개발하고 있습니다.

[번역] 최신 기술 – 이력을 기록하는 CRUD 구현하기 2부

원본: https://msdn.microsoft.com/en-us/magazine/mt707524

개념만 보면, 이력을 기록하는 CRUD는 기존 CRUD에서 하나의 파라미터가 추가된 것입니다. 바로 시간입니다. 이력을 기록하는 CRUD는 데이터베이스 레코드의 추가, 갱신, 삭제를 수행한 특정 시점과 상태를 알 수 있습니다. 이를 통해 여러분의 응용 프로그램은 보다 나은 분석과 리포팅 기능으로 비즈니스 인텔리전스를 이룰 수 있습니다.

지난번 컬럼(원문: msdn.com/magazine/mt703431, 번역: youngjaekim.wordpress.com)에서는 이력을 기록하는 CRUD에 대한 이론적 기반을 설명했습니다. 이번 글에서는 구체적인 예시를 보여주고자 합니다.

예시 시나리오

이 문서에서는 간단한 회의실 예약 시스템을 예시로 들겠습니다. 이 회의실 예약 시스템은 사내에 직원들이 이용할 수 있다고 가정합시다. 이 소프트웨어는 단순한 CRUD 기반으로 구현되어 있으며 회의실 예약을 할 때 새 레코드를 생성합니다. 사용자가 해당 예약을 변경하거나 미팅을 취소했을 때는 동일한 레코드가 갱신됩니다.

일반적인 CRUD 기반으로 예약 시스템을 만들면 시스템의 마지막 상태는 알고 있지만 삭제되거나 갱신된 정보는 잃게 됩니다. 이게 정말 문제일까요? 뭐, 경우에 따라 다르겠지요. 실제 비즈니스 환경에서는 별 문제가 되지 않기도 합니다. 하지만 사용하는 직원들의 전반적인 능률 면에서는 이력을 기록하는 CRUD를 적용하면 지나치게 많은 미팅 취소와 변경은 비효율적인 행동이라고 판단하고 사내 프로세스를 개선할 수도 있습니다.

그림 1은 빈 회의실 예약 시스템의 UI 예시입니다. 데이터베이스로는 SQL을 사용했습니다. 데이터베이스 테이블은 Rooms와 Bookings가 연결된 형태입니다.The Front-End UI for a Booking System
그림 1 예약 시스템의 프론트엔드 UI

예시로 든 응용 프로그램은 ASP.NET MVC로 만들었습니다. 사용자가 “Place request (장소 예약)” 버튼을 클릭하면, 콘트롤러 메소드가 실행되고 관련 정보를 전송합니다. 아래 코드는 요청을 받는 서버 코드를 간소화한 것입니다:

[HttpPost]
public ActionResult Add(RoomRequest room)
{
  service.AddBooking(room); 
  return RedirectToAction("index", "home");
}

이 메소드는 BookingController 클래스에 있는 것이며, 인젝션된 워커 서비스 클래스를 실행합니다. 워커 서비스는 여러 작업의 묶음으로 이해하셔도 좋습니다. 이 메소드 구현에서 주목할 점은, 예약을 생성한 후에 그림 1에서 본 첫페이지로 리디력션하는 것입니다. 예약 추가 작업을 수행한 후에 별도로 뷰를 생성하지 않습니다. 이것은 CQRS(Command Query Responsibility Segregation) 구조를 택했기에 나온 영향입니다. 예약 추가 명령이 백엔드에 전송되면, 시스템에 상태를 변경하고 끝입니다. 예시 프로그램이 AJAX를 사용하여 전송하기 때문에 새로고침할 필요도 없습니다. 명령 그 자체는 별도의 작업이 아니기 때문에 어떤 명시적인 링크도 UI에 나오지 않습니다.

기존 CRUD와 이력을 기록하는 CRUD의 가장 중요한 차이점은, 이력을 기록하는 CRUD는 시스템의 시작부터 상태를 변경하는 모든 작업을 기록으로 가지고 있다는 것입니다. 이력을 기록하는 CRUD를 만들기 위해서는 우선 비즈니스 작업은 모두 명령 단위로 만들어야 한다는 것을 명심하세요. 그렇게 해야 각 명령마다 추적 가능한 매커니즘을 만들 수 있습니다. 시스템에 전달되는 각 명령은 상태를 바꾸고 이력을 기록하는 CRUD는 그 때마다 시스템의 상태를 저장합니다.  변경된 상태는 무조건 이벤트로 저장합니다. 이벤트란 단지 어떤 일이 일어났다는 불변의 정보입니다. 이렇게 쌓인 이벤트로 목록이 만들어지면, 이를 이용해서 여러 형태의 데이터 투사체(projection)를 만들 수 있습니다. 가장 흔한 투사체라면, 단순히 엔티티의 현재 상태를 보여주는게 있겠습니다. 보통 응용 프로그램에서 이벤트는 사용자가 직접 내린 명령이거나, 다른 명령이나 외부 입력이 만들어낸 간접적인 명령입니다. 이 글의 예시 시나리오에서는 사용자가 예약 요청 버튼을 클릭하는 행위라고 할 수 있습니다.

명령을 처리하기

AddBooking 메소드는 아래와 같이 구현할 수 있을 것입니다:

public void AddBooking(RoomRequest request)
{
  var command = new RequestBookingCommand(request);
  var saga = new BookingSaga();
  var response = saga.AddBooking(command);
  // Do something based on the outcome of the command
}

RoomRequest 클래스는 전송된 데이터가 ASP.NET MVC에 의해 바인딩 된 단순한 DTO (Data-Transfer Object; 데이터 전송 오브젝트) 입니다. 그에 비해 RequestBookingCommand 클래스는 명령을 실행하는데 필요한 파라미터입니다. 이런 단순한 시나리오에서는 두 클래스는 거의 동일합니다. 이제 명령을 어떻게 처리할까요? 그림 2는 명령을 실행하는 3단계를 보여줍니다.

The Chain of Core Steps to Process a Command

그림 2 명령을 처리하는 연쇄적인 단계

핸들러는 명령을 받고 수행하는 콤포넌트입니다. 아래의 코드와 같이, 핸들러는 워커 서비스의 코드에서 호출되어 인메모리에서 직접 실행될 수 있으며, 버스를 통해서 할 수도 있습니다.

public void AddBooking(RoomRequest request)
{
  var command = new RequestBookingCommand(request);
  // Place the command on the bus for
  // registered components to pick it up
  BookingApplication.Bus.Send(command);
}

버스를 사용하면 몇가지 장점이 있습니다. 하나는, 동일한 명령에 대해 여러 개의 핸들러가 관여할 때 보다 쉽게 제어할 수 있습니다. 또 하나는, 버스로 안정적인 메시징 도구를 사용하면 메시지 전송을 신뢰할 수 있고 접속 문제도 극복할 수 있습니다. 마지막으로, 버스는 그 자체로 명령을 로깅하는 콤포넌트가 될 수 있습니다. (역자주: 버스 구현에 대하여, 온프레미스는 RabbitMQ, 클라우드 기반은 Azure Service Bus를 고려할 수 있습니다)

핸들러는 보통 일회성 콤포넌트로 하나의 요청에 한 번 시작하고 종료합니다. 하지만, 몇시간에서 몇 일간 실행하는 아주 긴 워크플로우일 수도 있고 사람이 직접 승인해줄 때까지 기다리는 형태일 수도 있습니다. 단순히 일회성 콤포넌트가 아닌 경우는 saga(긴 이야기)라고 불리기도 합니다.

통상적으로 버스나 큐를 사용하는건 확장성이나 신뢰성을 도모하고자 할 때입니다. 단지 기존의 CRUD 대신 이력을 기록하는 CRUD를 구현하기 위해 버스를 사용할 필요는 없습니다. 버스의 사용 여부와는 상관 없이, 어쨌거나 명령은 일회성 핸들러나 긴 작업 핸들러에 도달할테고 어떤 형태의 작업을 수행할 것이고, 이런 대부분의 작업은 데이터베이스의 주요 작업의 집합일 것입니다.

명령 로깅하기

전통적인 CRUD에서 데이터베이스에 정보를 기록하는 것은 입력 값을 포장한 후에 새 레코드로 추가하는 작업을 의미했습니다. 이력을 기록하는 CRUD의 관점에서 새 레코드는 새 이벤트의 생성을 의미합니다. 즉, 우리 예시에서는 새 예약 이벤트입니다. 예약 이벤트는 독립적이면서도 불변의 정보 조각으로 이벤트의 고유 ID, 타임스탬프, 이벤트 명, 이벤트 관련 변수를 포함합니다. 여기서 새 예약 이벤트 관련 변수는 전통적인 CRUD의 Bookings 테이블에 새 예약 레코드를 추가할 때 사용하는 모든 컬럼의 값입니다. 갱신 이벤트에 대한 변수는 갱신하는 필드만 해당됩니다. 그러므로, 모든 갱신 이벤트에 같은 필드가 있지는 않을 것입니다. 끝으로, 삭제 이벤트는 예약 ID 값만 있어도 충분합니다.

이력을 기록하는 CRUD는 두 단계로 동작합니다.

  1. 이벤트와 그 관련 정보를 로깅
  2. 현재 시스템 상태가 빠르게 쿼리할 수 있는지 확인

이 방법이면 시스템의 현재 상태는 언제나 최신 정보이며, 이로 인한 다음 작업도 확신을 가지고 할 수 있습니다. 참고로 기존의 전통적인 CRUD에서는 ‘시스템의 현재 상태’만 있었고 ‘시스템의 과거 상태’ 는 없었습니다. 기존 CRUD 시스템을 이력을 기록하는 CRUD로 발전시킬 때, 이벤트 로깅 단계와 시스템 상태 업데이트를 일관되게 처리하려면 하나의 트랜젝션으로 묶어야 하며, 그 결과는 그림 3과 같습니다.

using (var tx = new TransactionScope())
{
  // Create the "regular" booking in the Bookings table   
  var booking = _bookingRepository.AddBooking(
    command.RoomId, ...);
  if (booking == null)
  {
    tx.Dispose();   
    return CommandResponse.Fail;
  }
  // Track that a booking was created
  var eventToLog = command.ToEvent(booking.Id);
    eventRepository.Store(eventToLog);
  tx.Complete();
  return CommandResponse.Ok;
}
그림 3 이벤트 로깅과 시스템 업데이트

매번 예약 기록을 추가/수정/삭제할 때마다 현재 상태를 정확한 순서대로 알면서 예약 목록을 최신으로 유지할 수 있습니다. 그림 4는 예시 시나리오에 사용된 두 SQL 서버 테이블이며, 추가와 업데이트 과정을 겪은 후의 모습입니다.

Bookings and LoggedEvents Tables Side by Side
그림 4 Bookings와 LoggedEvents 테이블 비교

Bookings 테이블은 개별 예약 목록을 가지고 있으며, 각각은 현재 상태를 가지고 있습니다. LoggedEvents 테이블에는 모든 예약에서 발생한 모든 이벤트가 시간 순서대로 기록되어 있습니다. 예를 들어, 예약 #54는 예약을 만든 다음 몇 일 후에 수정했음을 알 수 있습니다. 그림의 예시에서, Cargo 열은 실행한 명령의 JSON 문자열을 그대로 직렬화해서 저장하고 있습니다.

UI로 로깅된 이벤트 보기

인증된 사용자가 예약에 대한 상세 정보를 보고 싶다고 가정해봅시다. 아마도 사용자는 달력에서 예약 정보를 불러오거나 기간 설정을 하여 쿼리할 것입니다. 두 경우 모두 기본적으로는 사용자가 언제/얼마나/누가 예약을 했는지는 알고 있으므로, 이런 상세 정보는 별로 안쓰일 것입니다. 그 대신 그림 5와 같이 예약에 대한 전체 히스토리를 보여준다면 꽤 도움이 될 것입니다.

Consuming Logged Events in the UI
그림 5 UI로 로깅된 이벤트 보기

로깅된 이벤트를 쭉 불러오면 한 엔티티(Booking 54)에 대한 상태의 목록을 표시하는 뷰 모델을 만들 수 있습니다. 예시에서, 사용자가 예약 상세 정보를 보려고 클릭하면  JSON이 백그라운드에서 다운로드되고 모달(modal) 팝업이 열립니다. 이 때 JSON을 주는 메소드는 아래와 같습니다.

public JsonResult BookingHistory(int id)
{
  var history = _service.History(id);
  var dto = history.ToJavaScriptSlotHistory();
  return Json(dto, JsonRequestBehavior.AllowGet);
}

서비스 내에 History 메소드를 실행하는 것이 전부입니다. 이 동작은 특정 예약 ID에 대한 이벤트를 쿼리해서 모두 가져옵니다.

var events = new EventRepository().All(aggregateId);
foreach (var e in events)
{
  var slot = new SlotInfo();
  switch (e.Action)
  {
    :
  }
  history.Changelist.Add(slot);
}

로깅된 이벤트를 하나씩 넘기면서, 적절한 오브젝트를 DTO에 덧붙여서 반환합니다. 그림 5에서 보이는 ToJavaScriptSlotHistory(팝업창)에는 두 상태의 차이점을 신속하게 보여주기 위한 변환 작업이 있습니다.

주목할 점은, 이벤트를 CRUD만 이용해서 로깅해도 UI가 더 나아질 수 있다는 것입니다. 여러분은 이제 시스템에서 일어나는 모든 상황을 알 수 있으며 언제든 원하는 형태, 원하는 시점으로 데이터를 투사해서 볼 수 있습니다. 예를 들어, 갱신과 삭제 작업에 대한 통계를 만들어서 애널리스트에게 회사의 회의실 예약이 얼마나 비효율적인지를 파악하게 할 수 있습니다. 또는, 특정 날짜의 예약 정보를 쿼리하고 연관된 이벤트를 산출할 수도 있습니다. 한 마디로, 이력을 기록하는 CRUD는 응용 프로그램에 완전히 새로운 가능성을 열어줍니다.

정리

이력을 기록하는 CRUD는 기존의 단순한 CRUD 응용 프로그램을 보다 똑똑하게 발전시키는 방법입니다. 이 글에서는 최근에 유행하는 CQRS, 이벤트소싱, 버스와 큐, 메시지 기반 비즈니스 로직 등 몇 가지 단어와 패턴도 언급했습니다. 이 글이 도움이 됐다면, 저의 이전 글인 2015년 7월의 글(msdn.com/magazine/mt238399)과 2015년 8월 글(msdn.com/magazine/mt185569)도 한 번 읽어보세요. 이번 예시에 이어서 더 많은 영감을 줄 것입니다!


Dino Esposito “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014), “Modern Web Applications with ASP.NET” (Microsoft Press, 2016)의 저자입니다. JetBrains에서 .NET과 안드로이드 분야의 기술 에반젤리스트이며, 세계 곳곳의 여러 행사에서 연사로 활동하고 있습니다. Esposito의 소프트웨어에 대한 비전은 다음 링크에서 볼 수 있습니다.  software2cents.wordpress.com, 트위터: @despos.

이 문서의 리뷰를 한 Microsoft 기술 전문가 Jon Arne Saeteras에게 감사드립니다.

이 문서를 번역한 김영재 교육서비스 바로풀기의 개발사 Bapul의 CTO로서 기술로 교육에 새로운 시각을 주기 위해 열심히 개발하고 있습니다.

MSDN Magazine 포럼에서 이 문서에 대한 토론 보기 (영문)

[번역] 최신 기술 – 이력을 기록하는 CRUD 구현하기 1부

원문: https://msdn.microsoft.com/ko-kr/magazine/mt703431
참고: 2부도 있습니다 [번역] 최신 기술 – 이력을 기록하는 CRUD 구현하기 2부

*역자주: 원문은 Historical CRUD이며, 몇몇 개발자 분들과의 논의 끝에 ‘이력을 기록하는 CRUD’로 번역했습니다. [대화 보기]

[번역] Microsoft Azure – Azure Service Fabric, 그리고 마이크로서비스 아키텍처

원문: https://msdn.microsoft.com/en-us/magazine/mt595752.aspx

마이크로서비스는 요즘 꽤 인기있는 단어입니다. 이에 대한 많은 발표자료가 있지만 개발자들은 여전히 혼란스럽습니다. 우리가 자주 듣는 질문은 바로 이겁니다. – “이거 SOA나 DDD와 유사개념 아닌가요?”

네, 실제로 마이크로서비스에는 SOA와 DDD 경험을 한 개발자가 만든 기술이 많이 있습니다. 마이크로서비스가 “SOA를 제대로 구현한 것”이라고 생각해도 큰 무리는 아닙니다. 서비스 간의 자주성, Bounded-Context 패턴, Event-driven과 같은 개념은 SOA와 DDD에 뿌리를 두고 있습니다.

이 글에서는 마이크로서비스의 이론과 구현에 대해 다룰 것입니다. 마이크로서비스에 대한 짧은 소개를 한 후, Azure Service Fabric에 마이크로서비스를 만들고 배포하는 것을 실제 사례를 들며 말할 것입니다. 끝으로 우리는 이 플랫폼이 왜 마이크로서비스를 만드는데 최적인지 보여줄 것입니다.

이름이 암시하듯, 마이크로서비스 아키텍처는 작은 서비스들의 집합으로 서버 어플리케이션을 만드는 방법입니다. 각 서비스는 각자의 프로세스에서 돌고 HTTP나 웹소켓으로 통신합니다. 각 마이크로서비스는 특정 도메인이나 비즈니스에 따라 Bounded-Context 별로 나뉘고 자동화된 매커니즘을 통해 자치적으로 개발하고 독립적으로 배포할 수 있도록 구현합니다. 끝으로 각 서비스는 각자의 도메인 데이터 모델과 도메인 로직을 가지고 있어야 하며, 각자가 다른 데이터 스토리지 기술을 사용할 수 있어야 하고, 심지어 다른 프로그래밍 언어로 개발할 수도 있어야 합니다.

마이크로서비스의 예시로는 프로토콜 게이트웨이, 사용자 프로필, 쇼핑카트, 재고 처리, 구매 시스템, 결제 프로세스, 큐와 캐시도 포함됩니다.

왜 마이크로서비스일까요? 한 마디로 민첩함(agility)입니다. 오랜 세월동안 마이크로서비스는 대규모의 복잡하고 확장이 중요한 시스템에서 유지 보수하기에 탁월한 장점을 보였습니다. 여러 개의 서비스를 독립적으로 배포할 수 있도록 어플리케이션을 디자인하여 세밀하게 릴리즈 계획을 세우는 것이 가능했기 때문입니다.

또 다른 장점은, 마이크로서비스는 독립적으로 확장할 수 있다는 것입니다. 거대한 모노리식 어플리케이션 블럭을 가지고 한 번에 확장하는게 아니라, 특정 마이크로서비스만 수평적으로 확장할 수 있습니다. 이를 통해 더 많은 프로세싱 파워나 네트워크 대역폭을 필요로 할 때 어플리케이션 전체를 확장하는게 아니라 필요한 것만 확장할 수 있습니다.

잘게 나눈 마이크로서비스 어플리케이션 구조로 지속적인 통합과 배포가 가능합니다. 그러므로 어플리케이션에 새 기능도 더 빠르게 구현할 수 있습니다. 어플리케이션을 잘게 나누면 테스트와 실행도 보다 격리된 형태로 할 수 있고, 서비스 간 엄격한 관계는 유지한 채 독립적으로 마이크로서비스를 개선할 수 있습니다. 인터페이스나 계약 관계를 일부러 깨지 않는 이상 각각의 마이크로서비스를 어떻게 바꾸든 새 기능을 추가하든 다른 마이크로서비스에는 영향을 주지 않습니다.

그림 1에서 보듯, 마이크로서비스로 개발하는 것은 민첩한 변화와 빠른 반복을 위한 최고의 효율을 목표로 합니다. 대규모 어플리케이션에서 특정 부분만을 바꾸고 배포할 수 있기 때문입니다.

Microservices Approach Compared to Traditional Server Application Approach

그림 1 마이크로서비스와 전통적인 서버 어플리케이션의 접근 방법 비교

마이크로서비스마다의 데이터 자주성

중요한 규칙은, 각 마이크로서비스가 독립적인 배포가 가능하도록 고유의 자동화된 생명주기를 가지고 각자의 도메인 데이터와 로직을 가진다는 것입니다. 사실 이것은 온전한 하나의 어플리케이션이 자신의 로직과 데이터를 가지고 있다는 것과 다를 바 없습니다.

개념적인 모델로서의 도메인은 서브시스템이나 마이크로서비스와 다를 수 있습니다. 예를 들어, 기업용 고객관리(CRM) 어플리케이션에서 구매내역 서브시스템과 고객 지원 서브시스템은 고객 엔티티에서 각자가 필요한 속성과 데이터를 부르고 서로 다른 Bounded-Context에서 사용합니다.

원리상으로는, DDD의 개념에서 각 Bounded-Context는 패턴의 차이로 구분되는 서브시스템 또는 서비스이며 반드시 각자의 도메인모델(데이터와 로직)을 가지고 있다는 것과 유사합니다. DDD에서의 각각의 Bounded-Context를 별개의 마이크로서비스라고 생각할 수 있습니다.

한 편으로, 많은 어플리케이션에서 사용되는 전통적인 모놀리식 구현은 그림 2와 같이 중앙집중형 데이터베이스(대개의 경우 정규화된 SQL 데이터베이스)를 가지고 전체 어플리케이션 또는 전체 내부 서브시스템이 사용하고 있습니다. 이 방법은 내부적으로는 단순해보이고 엔티티를 재사용하는 관점에서는 서로 다른 서브시스템에 일관성을 줄 수도 있습니다. 하지만 실제로는 각 서브시스템이 주는 정보를 모두 담는 거대한 테이블을 만들게 되고 그 안에 대부분의 경우에는 불필요한 속성과 컬럼이 만들어집니다. 이건 마치 짧은 코스로 하이킹할 때와 하루 종일 운전할 때, 그리고 지리학을 공부할 때에 대해 단 하나의 지도만 사용하려는 것과 같습니다.

Data Sovereignty Comparison: Microservices vs. Monolithic

그림 2 마이크로서비스와 모놀리식 구조의 데이터 자주성 비교

Stateless와 Stateful 마이크로서비스란?

앞서 말했듯이, 각 마이크로서비스는 각자의 도메인 모델을 가지고 있습니다. stateless 마이크로서비스의 경우에는, 데이터베이스가 외부에 놓일 수 있어서 SQL 서버같은 관계형 데이터베이스나 MongoDB 같은 NoSQL, 또는 Azure DocumentDB 같은 것을 사용할 수 있습니다. 더 나아가 서비스 그 자체는 stateful할 수 있는데, 이 말은 데이터가 마이크로서비스에 속해있다는 것입니다. 이런 형태는 데이터가 같은 서버에 있는 것이 아니라 같은 마이크로서비스 프로세스에 있기 때문에 데이터를 인메모리와 하드디스크에 보존하고 다른 노드에 복제본을 둘 수도 있습니다.

Stateless 방식은 기존에 흔히 사용해왔던 패턴과 유사하기 때문에 stateful에 비해 구현하기 쉽고 충분히 쓸만했습니다. 그렇지만 stateless 마이크로서비스는 프로세스와 데이터 소스가 분리되어 있다보니 어느 정도 지연이 있다는 의미이기도 한데, 이런 부분에 성능을 향상시키기 위해서 캐시와 큐를 추가적으로 사용하고 그로 인한 유동적인 부분도 늘어납니다. 결과적으로 우리는 여러 층의 티어를 가진 매우 복잡한 구조를 만들어내곤 했습니다.

Stateful 마이크로서비스는, 그와는 반대로 데이터와 도메인 로직 간에 지연이 없기 때문에 더욱 발전된 시나리오도 생각할 수 있습니다. 대용량 데이터 처리, 게임 백엔드, 서비스화된 데이터베이스 등 낮은 지연시간을 원하는 시나리오라면 뭐든 stateful 서비스로 이득을 얻을 수 있습니다. 내부적으로 상태를 더 빠르게 액세스 할 수 있기 때문입니다.

단점: Stateful 서비스는 수평 확장(scale-out)하는데 어느 정도의 복잡성을 가집니다. 외부 데이터베이스를 사용하는 기능이 있다면 stateful 마이크로서비스가 확장됨에 따라 그 안에서의 데이터 복제본 관리, 파티셔닝 등의 문제에 대해 어떻게든 해결해야 합니다. 이 부분이 바로 Service Fabric을 도입할 때 가장 도움받을 수 있는 부분입니다. stateful 마이크로서비스에 대해 개발과 생명 주기를 단순화하기 때문입니다.

Service Fabric의 장점

마이크로서비스를 적용해서 얻는 장점에는 한계점도 있습니다. 분산컴퓨팅 환경과 복잡한 마이크로서비스 배포를 모두 손수 하려고 하면 관리가 매우 어렵습니다. Service Fabric을 이용하면 마이크로서비스를 만들고, 배포하고, 실행하고, 관리하는 것을 효율적이고 효과적으로 할 수 있습니다.

Service Fabric은 무엇인가요? 클라우드에서 확장성이 뛰어나면서도 신뢰성있는 어플리케이션을 만들고 쉽게 관리할 수 있게 하는 분산 시스템 플랫폼입니다. Service Fabric은 클라우드 어플리케이션을 개발하고 관리하는데 부딧히는 중요한 문제를 해결해줍니다. Service Fabric을 사용하면 개발자와 관리자가 복잡한 인프라 문제를 해결하느라 시간을 쏟지 않는대신 확장성, 신뢰성, 관리편의가 필수적인 요구사항들을 구현하는데 집중할 수 있습니다. Service Fabric은 엔터프라이즈급 서비스를 개발하고 관리하는데 적합한 마이크로소프트의 차세대 미들웨어 플랫폼이며 최고의 클라우드 스케일 서비스입니다.

Service Fabric은 범용적인 배포 환경입니다. .NET Framework, Node.js, Java, C++ 등 실행가능한 것이면 어떤 언어도 가능하며 MongoDB 런타임과 같은 데이터베이스도 가능합니다.

그러므로, Azure Service Fabric이 마이크로서비스 어플리케이션에만 국한되는 것은 아닙니다. web app이나 service와 같은 기존의 어플리케이션도 호스팅하고 배포할 수 있으며 이 때 확장성, 로드밸런싱, 빠른 배포 등의 장점을 얻을 수 있습니다. 다만 그림 3에서 보듯, Azure Service Fabric이 밑바닥부터 새로 만들어진 플랫폼이며 하이퍼스케일, 마이크로서비스 시스템에 더 알맞도록 디자인되었을 뿐입니다.

Microsoft Azure Service Fabric

그림 3 Microsoft Azure Service Fabric

Service Fabric의 장점은 아래와 같습니다:

  • Azure에서 실행할 뿐만 아니라, 온프레미스나 다른 클라우드에서도 실행 가능. Service Fabric의 매우 중요한 특징은 Azure나 온프레미스 뿐만 아니라, 소유 중인 베어메탈 서버나 가상머신, 심지어 다른 서드파티 호스팅 클라우드에서도 구동할 수 있다는 것입니다. 특정 클라우드에 락-인(lock-in)하지 않습니다. Amazon Web Services (AWS)에서도 동작합니다.
  • 윈도우 및 리눅스 지원. 현재(2015년 말) Service Fabric은 윈도우만 지원하지만, 리눅스와 컨테이너(윈도우 이미지와 Docker 이미지) 를 지원할 예정입니다.
  • 완벽한 검증. Service Fabric은 수 년전부터 마이크로소프트의 여러 클라우드 제품에서 사용해왔습니다.

Service Fabric은 마이크로소프트 내부의 대형 서비스를 개발하기 위해서 만들어졌습니다. SQL Server 같은 제품을 클라우드에서 구동하는 서비스(Azure SQL Database)로 만들면서도 신속하고 신뢰성 있고 확장성있으면서도 비용을 절약할 수 있는 분산 기술이 필요했습니다. 이러한 복잡한 문제를 해결하기 위한 핵심 기술을 만드는 동안, 이와 같은 변화가 필요한 제품이 SQL Server 만이 아니라는 것을 알게 되었습니다. 예를 들어, Skype for Business가 마이크로서비스 기반 어플리케이션으로 발전해야 하는 과정에서도 비슷한 문제가 있었습니다. Service Fabric은 이런 문제들을 해결하면서 발전했고 마이크로소프트의 다양한 아키텍처와 요구사항이 있는 대형 서비스들에 사용해온 어플리케이션 플랫폼입니다. InTune, DocumentDB, Cortana의 백엔드, Skype for Business 모두 Service Fabric에서 동작 중입니다.

이런 중요한 시스템에 적용해온 경험을 통해 마이크로소프트는 인프라 자원과 확장성있는 어플리케이션의 요구사항을 제대로 이해하는 플랫폼을 디자인했습니다. 이 플랫폼의 자동 업데이트, 자가 치유(self-healing) 기능은 고가용성과 안정성을 보장하면서 확장성을 갖추게 합니다. 마이크로소프트는 이제 이 강력한 무기를 모두가 사용할 수 있게 만들었습니다.

Azure Service Fabric 프로그래밍 모델

Service Fabric은 서비스를 만드는데 두 개의 고수준 프레임워크를 제공합니다: Reliable Services API와 Reliable Actors API 입니다. 두 개 모두 동일한 Service Fabric 코어 위에 만들어졌지만, 그림 4에서 보는 바와 같이 동시성, 파티셔닝, 통신에 관하여 단순함과 유연성 사이에서 서로 다른 균형점을 가지고 있습니다.  만들고자 하는 서비스가 어떤 프레임워크에 더 맞는지 선택하려면 두 모델이 어떻게 동작하는지를 파악하는 것이 좋습니다. 많은 어플리케이션 시나리오에서 하이브리드 형태로 둘 모두를 사용할 수 있습니다. 일부 마이크로서비스는 Actor를 사용하면서 그 Actor에서 보내는 데이터를 Reliable Service가 취합하는 용도로 사용할 수도 있습니다. 그 외에도, reliable service는 actor service를 조율하는 형태로 많이 사용됩니다.

그림 4 Service Fabric 프로그래밍 모델 비교

Reliable Actor API Reliable Services API
서비스 시나리오에서 상태와 논리를 여러개의 독립적인 단위/오브젝트로 구성할 수 있습니다. (라이브 IoT 오브젝트 또는 게임 백엔드가 좋은 예) 서비스 시나리오에서 여러 엔티티 타입과 콤포넌트 간에 로직과 큐를 유지해야 합니다.
확장성과 일관성을 가진 많은 수의 싱글스레드 오브젝트를 사용해야 하는 경우. Reliable 컬렉션(.NET Reliable Dictionary, Reliable Queue)을 사용하여 상태와 엔티티를 저장해야 하는 경우.
동시성과 입자성을 프레임워크에서 관리하기를 원하는 경우. 상태의 동시성과 입자성을 직접 제어하고 싶은 경우.
통신은 Service Fabric이 알아서 관리하도록 하고 싶은 경우. 통신 프로토콜을 직접 선택하고 관리하고 구현하고 싶은 경우. (Web API, WebSockets, Windows Communication Foundation 등)
Service Fabric가 stateful 액터 서비스의 파티셔닝을 알아서 하고 개발시에는 통합된 모습으로 다루고 싶은 경우. Stateful 서비스의 파티션 규칙까지 직접 제어하고 싶은 경우.

 

Azure Service Fabric에서 Stateless 마이크로서비스 만들기

Azure Service Fabric의 마이크로서비스는 .NET Framework, Node.js, Java, C++를 가리지 않고 어떤 형태의 프로세스든 서버에서 실행할 수 있습니다. 현재 Service Fabric API는 .NET과 C++ 라이브러리만 제공하고 있으므로 마이크로서비스의 저수준 구현은 .NET Framework와 C++로 해야 합니다.

앞서 말했듯이, stateless 마이크로서비스는 말그대로 상태(state)를 전혀 보존하지 않는 서비스이거나 프로세스(프론트엔드 서비스나 비즈니스 로직 서비스)가 종료됨에 따라 상태도 읽게 되는 것입니다. 그러므로 동기화, 복제, 보존, 고가용성 모두 불필요합니다. 외부에 관계를 만들어서 상태를 가질 수 있습니다. 이 경우 보통 외부 스토리지로 Azure SQL 데이터베이스, Azure 저장소, Azure DocumentDB, 서드파티 스토리지 엔진(관계형 또는 NoSQL) 등을 사용합니다. 이런 이유로, 클라우드에서 운영 중인 ASP.NET Web API, Worker Role, Azure 웹앱 같은 것은 쉽게 Service Fabric stateless 서비스로 마이그레이션할 수 있습니다.

개발 환경을 셋업하려면 Service Fabric SDK를 설치해야 합니다. Service Fabric SDK로 개발용 로컬 클러스터를 만들 수 있는데 이것은 에뮬레이터가 아니고 Azure에 동작하는 것과 완벽히 동일합니다. 또한, 비주얼스튜디오 2015와 연동하여 개발과 디버깅을 더 쉽게 할 수 있습니다.

로컬에서 서비스를 배포하고 디버깅하기 위해서는 노드의 묶음인 클러스터를 만들어야 합니다. 파워셸을 열어서 DevClusterSetup.ps1 스크립트를 실행하면 됩니다. 문서를 보려면 bit.ly/1Mfi0LB 링크에서 “Install and Start a Local Cluster” 섹션에 “Prepare Your Development Environment”를 참고하세요. 링크로 소개한 문서를 보면 설치 절차에 대해 하나씩 따라할 수 있습니다.

Stateless Service Base Class: Service Fabric의 모든 Stateless 서비스는 Microsoft.ServiceFabric.Services.StatelessService에서 상속받아야 합니다. 이 클래스는 API 메소드, Service Fabric 클러스터에 대한 콘텍스트와 실행 환경을 제공하고 여러분이 만드는 서비스의 라이프사이클에 관여합니다.

여러분이 만드는 서비스가 stateful이든 stateless든 상관없이 Reliable Service는 개발 코드에 간단히 추가만 하면 시작하는 단순한 라이프사이클을 제공합니다. Service Fabric 서비스를 실행하는데 한두 개의 메소드만 구현하면 됩니다. 보통은 RunAsync와 CreateServiceReplicaListeners 만 하는데, 이에 대해서는 통신 프로토콜을 다룰 때 자세히 설명하겠습니다.

그림 5에 RunAsync 메소드가 보입니다. 이 부분에서 여러분이 구현하는 서비스가 백그라운드로 동작합니다. cancellation token이 있어서 메소드가 반드시 멈춰야 하는 신호를 받을 수 있도록 했습니다.

그림 5 Azure Service Fabric에서 Stateless 서비스의 기본 구조
using Microsoft.ServiceFabric.Services;

namespace MyApp.MyStatelessService
{
public class MyStatelessService : StatelessService
{
//... Service implementation
protected override async Task RunAsync(CancellationToken cancellationToken)
{
int iterations = 0;
while (!cancellationToken.IsCancellationRequested)
{
ServiceEventSource.Current.ServiceMessage(this, "Working-{0}",
iterations++);
// Place to write your own background logic.
// Example: Logic doing any polling task.
// Logic here would be similar to what you usually implement in
// Azure Worker Roles, Azure WebJobs or a traditional Windows Service.
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
}
}

Azure Service Fabric Reliable Service의 통신 프로토콜: Azure Service Fabric Reliable Service는 플러그 가능한(pluggable) 커뮤니케이션 모델입니다. HTTP를 통한 ASP.NET WebAPI, 웹소켓, WCF(Windows Communication Foundation), 직접 만드는 TCP 프로토콜 등을 골라서 사용할 수 있습니다.

선택한 통신 프로토콜을 직접 구현할 수도 있고 Service Fabric이 제공하는 통신 스택을 사용할 수도 있습니다. 그 중 ServiceCommunicationListener/ServiceProxy는 Reliable Services 프레임워크에서 기본적으로 제공하는 통신 스택입니다. 그 외에 Service Fabric에 플러그-인 할 수 있는 별도의 통신 스택도 NuGet 패키지에서 설치할 수 있습니다.

Service Fabric에서 ASP.NET Web API를 마이크로서비스로 사용하기: 앞서 말했듯이, Service Fabric에서는 서비스가 어떻게 통신하는지를 선택할 수 있습니다. 가장 흔히 사용되는 것은 .NET 프레임워크에서 ASP.NET WebApi를 이용한 HTTP 서비스입니다.

Service Fabric의 Web API는 여러분이 이미 아는 ASP.NET Web API와 같습니다. Controller, HttpRoutes, MapHttpRoute나 메소드에 속성(attribute)을 적용하여 REST 서비스를 구성하는 것 모두 그대로 사용할 수 있습니다. Service Fabric에서 다른 점은 Web API 어플리케이션을 호스팅하는 방법입니다. 우선, IIS는 Service Fabric에서 사용할 수 없습니다. 왜냐하면, 단순한 실행 프로세스를 클러스터에 복제하는 형태이기 때문입니다. 그러므로 코드에 HTTP 리스너를 추가하고 self-host로 구현해야 합니다. 이 방식대로 Web API 서비스를 구현하려면 두 가지 선택지가 있습니다.

  • ASP.NET 5 Web API (코드네임 “Project K”, 번역시점에는 ASP.NET Core로 명명)를 사용하고 HTTP 리스너를 Service Fabric 마이크로서비스 프로세스에 추가하여 self-host 하는 방법.
  • OWIN/Katana와 ASP.NET 4.x Web API를 사용하고  마찬가지로 Service Fabric 마이크로서비스 프로세스에 추가하여 self-host 하는 방법.

ASP.NET 5는 Azure Service Fabric에서 마이크로서비스를 구성하기에 가장 알맞습니다. 왜냐하면, 앞서 말했듯이 Service Fabric에서는 여러 개의 서비스를 각 노드/VM에 배포할 수 있고 클러스터마다마이크로서비스를 빽빽하게 구성할 수도 있기 때문입니다. 이렇게 고밀도로 구성할 때 Service Fabric은 진가를 발휘합니다. ASP.NET 5에서 .NET Core 5와 CoreCLR 지원은 향후 지원될 예정이지만, 최선의 선택이라는 점은 맞습니다. .NET Core 5는 경량 프레임워크로 .NET 4.x 프레임워크보다 메모리를 적게 차지하기 때문에 마이크로서비스를 매우 밀도 높게 구성할 수 있기 때문입니다.

MVC 형태의 웹앱을 사용한 Service Fabric 마이크로서비스: ASP.NET MVC는 통상의 웹사이트 개발에 흔히 사용되는 프레임워크입니다. 하지만 Service Fabric에서는 MVC 5 또는 그 이하의 기존 MVC 프레임워크를 사용할 수 없습니다. 왜냐하면, Service Fabric은 self-host로 동작해야 하고 HTTP 리스너를 프로세스에 추가해야 하는데 OWIN/Katana는 ASP.NET 4.x MVC를 지원하지 않고 ASP.NET 4.x Web API만 지원하기 때문입니다. 그러므로, MVC 형태의 웹 어플리케이션을 Service Fabric에서 구현하려면 다음과 같이 구현해야 합니다.

  • ASP.NET 5 MVC를 사용하여 Service Fabric 마이크로서비스 프로세스에 HTTP 리스너를 self-host 합니다. ASP.NET 5, Web API, MVC가 모두 하나의 프레임워크에 통합되었기 때문에 IIS에 의존하지 않는 동일한 콘트롤러 매커니즘을 사용하기 때문입니다.
  • OWIN/Kanata의 ASP.NET 4.x Web API를 사용하여 Service Fabric 마이크로서비스에 HTTP 리스너를 self-host합니다. MVC 콘트롤러를 Web API 콘트롤러로 처리하는 방법으로, Web API가 JSON/XML을 출력하지 않고 HTML/JavaScript를 출력하도록 하는 것입니다. 구현 방법은 다음 문서[bit.ly/1UMdKIf]에 나와있습니다.

ASP.NET 4.x Web API와 OWIN/Katana를 이용하여 Self-Hosting 구현하기

이 방법은 여러분이 만든 Web API 어플리케이션에서 바뀌는 것이 없습니다. 기존에 작성했던 코드와 다른 것이 전혀 없고 거의 모든 코드를 곧바로 옮길 수 있습니다. 다만 IIS에서 호스팅했다면 호스팅하는 부분은 조금 다를 수 있습니다.

Service Class의 CreateServiceReplicaListeners 메소드: 이 메소드에서 서비스가 사용할 커뮤니케이션 스택을 정의합니다. 커뮤니케이션 스택은 서비스의 끝점(endpoint)을 정의하고 메시지가 다른 서비스 코드와 어떻게 연계되는지를 정하는 것으로 Web API도 사용할 수 있습니다.

서비스 클래스에 대해 언급했던 그림 4 이전의 내용을 다시 보면, CreateServiceReplica­Listeners는 내가 사용할 최소 하나 이상의 커뮤니케이션 스택입니다. 이번 예시에는 그림 6과 같이 Web API를 사용하는 OwinCommunicationListener 를 사용했습니다.

그림 6. CreateServiceReplicaListeners 메소드를 Stateless Service 클래스에 추가하기

using Microsoft.ServiceFabric.Services;

namespace MyApp.MyStatelessService
{
public class MyStatelessService : StatelessService
{
//... Service implementation.
protected override async Task RunAsync(CancellationToken cancellationToken)
{
// Place to write your own background logic here.
// ...
}
protected override IEnumerable
CreateServiceReplicaListeners()
{
return new List()
{
new ServiceReplicaListener(parameters =>
new OwinCommunicationListener(
"api", new Startup()))
};
}
}
}

한 서비스에 여러개의 communication listeners 추가가 가능하다는 것을 꼭 기억해주세요.

OwinCommunicationListener 클래스는 모든 마이크로서비스에서 재사용할 수 있습니다. 마찬가지로,  WCF, WebSockets 등 다른 프로토콜도 플러그인 개념으로 사용할 수 있습니다.

Web API 마이크로서비스를 만든다면 그에 대한 콘트롤러를 만들 것이고 이는 아래 코드와 같이 Web API 서비스를 만드는 것과 전혀 차이가 없습니다:

// Typical Web API Service class implementation
public class CustomerController : ApiController
{
//// Example - GET /customers/MSFT
[HttpGet]
[Route("customers/{customerKey}", Name = "GetCustomer")]
public async Task GetCustomer(string customerKey)
{
// ... Stateless method implementation.
}
}

이 문서는 Azure Service Fabric과 마이크로서비스에 대한 전반적인 내용을 다루는 것이므로 Service Fabric 마이크로서비스에서 OWIN을 구현하는 방법은 깊게 다루지 않겠습니다. 이에 대한 예시는 GitHub(bit.ly/1OW5Bmj)에 Web API and Service Fabric HelloWorld Example (ASP.NET 4.x) 예시를 참고하시기 바랍니다.

Self-Hosting ASP.NET 5 Web API 구현하기

가장 추천하는 것은 ASP.NET 5 (ASP.NET Core)를 Azure Service Fabric의 마이크로서비스로 구현하는 것입니다. 왜냐하면, Service Fabric은 다수의 서비스를 노드/VM에 배포할 수 있고 클러스터 별로 고밀도의 마이크로서비스를 수용할 수 있기 때문입니다. ASP.NET 5에는 다음과 같은 훌륭한 기능이 있습니다.

  • 유연한 호스팅: IIS 뿐만 아니라 개별 프로세스로 ASP.NET 5 어플리케이션을 호스팅할 수 있습니다.
  • Web API, MVC, Web Pages: 이 모두를 하나로 통합해서 보다 단순한 개념을 가집니다.
  • 완전한 side-by-side 실행: ASP.NET 5 어플리케이션은 한 머신에 다른 어플리케이션에 영향을 주지 않습니다. 심지어 다른 버전의 다른 .NET Framework 버전을 사용할 경우에도 문제 없습니다. 이는 IT 관리에 있어서 큰 이점을 줍니다.
  • 크로스플랫폼: ASP.NET 5는 Windows, Max OS X, Linux에서 동작합니다.
  • 클라우드: 진단, 세션 상태, 캐시, 설정과 같은 기능이 로컬에서 동작하는 것의 수정없이 클라우드에서도 동작합니다.

좀 더 미래지향적으로 생각해보면, Service Fabric이 .NET Core 5와 CoreCLR을 지원하게 되면 고밀도를 염두하는 것이 훨씬 좋습니다. 아직은 .NET Core와 CoreCLR을 지원하기까지는 작업이 남아있지만 지원할 때의 그 장점은 명확합니다. ASP.NET 5를 .NET Core 5에서 구동하는 것은 경량의 .NET Framework이며, CoreCLR 런타임은 기존의 .NET 4.x보다 적은 메모리를 사용합니다. 이 조합은 그림 7의 “Microservice B”의 경우로, 마이크로서비스의 밀도를 매우 높일 수 있습니다.

Comparing ASP.NET 5 Running on the CLR vs. CoreCLR Within the Service Fabric Context
그림 7 ASP.NET 5가 Service Fabric Context 내에서 CLR과 CoreCLR의 동작 비교.

.NET Core 5와 CoreCLR 지원은 향후 지원될 예정입니다. 현재 엔터프라이즈 환경에서는 Service Fabric에서 .NET 4.x 기반의 ASP.NET 5 개발을 선호합니다. 이렇게 하면 향후 CoreCLR로의 마이그레이션이 수월합니다.

하이퍼스케일 마이크로서비스의 운영과 배포

운영과 배포는 Service Fabric이 하이퍼스케일 마이크로서비스를 구축하는데 왜 좋은지를 보여주는 강점 중 하나입니다. 여러분은 마이크로서비스 개발에만 집중하도록 Service Fabric이 복잡한 것들을 도맡아서 해줍니다.

고밀도의 마이크로서비스는 인프라 비용을 낮추는데 큰 도움이 됩니다. Service Fabric 클러스터는 많은 VM/서버(향후에는 컨테이너)의 풀로 구성되어 있으며 이를 노드라고 부릅니다. 각 노드마다 Service Fabric은 자동으로 수 개의 서비스를 배포합니다. 그러므로 각 서버/VM의 성능에 따라 초고밀도로 마이크로서비스를 클러스터에서 구성할 수 있습니다.

Azure Service Fabric에서는 수백개의 서비스 인스턴스를 각 머신이나 VM에서 실행할 수 있습니다. 이로써 총소유비용(TCO; Total Cost of Ownership)을 대폭 절약할 수 있습니다. 같은 양의 서비스라도 더 적은 하드웨어를 필요로 합니다. 그러므로 Azure Service Fabric의 고밀도는 Azure Cloud Service가 서비스마다 하나의 VM을 필요로 하는 것에 비하면 중요한 차별점입니다. 만약 마이크로서비스를 Web Role 또는 Worker Role에 배포한다면 단일 마이크로서비스에 너무 많은 리소스를 할당하게 될 것입니다. 예를 들어, 그림 8은 서비스 인스턴스 별 하나의 VM이 할당된 모습입니다 (색상은 서비스의 유형을 표시했으며, 각 도형은 서비스 인스턴스를, 박스 모양은 VM을 표현했습니다). 이 방법은 마이크로서비스를 고밀도로 구성하기에는 별 도움이 안되고 작은 VM 여러개를 사용하게 됩니다. 또한, Azure Service Fabric에 배포하는 것과 동등한 수준의 고밀도를 달성할 수는 없습니다.

Services Density Comparison—Azure Cloud Services vs. Service Fabric
그림 8 서비스 밀도 비교—Azure Cloud Services vs. Service Fabric

반면, Service Fabric에서는 노드마다 여러 개의 마이크로서비스를 배포할 수 있습니다. 그러므로 서비스의 밀도 면에서 훨씬 효율적이며 비용 또한 낮아집니다. 클러스터마다 수십, 수백, 수천 개의 VM/서버를 만들 수 있고 또 수십, 수백, 수천 개의 마이크로서비스 인스턴스와 복제본을 노드/VM마다 만들 수 있습니다. 그리고 각 마이크로서비스는 단순히 프로세스일 뿐이므로, 배포와 확장은 서비스마다 새로 VM을 만드는 Azure Cloud Service보다 훨씬 빠릅니다.

Azure 클라우드에 클러스터 만들기: 마이크로서비스를 배포하기 전에 Azure 또는 온프레미스에 노드들의 집합인 클러스터를 만들어야 합니다. Azure 클라우드에 클러스터를 만들면 Azure 포털에서 바로 작업을 할 수도 있고 Azure Resource Manager (ARM)을 사용할 수도 있습니다. 그림 9에서, 우리가 Azure 구독에서 Azure 포털로 만든 Service Fabric 클러스터를 볼 수 있습니다. 내부적으로 클러스터 생성 과정은 Azure ARM과 Azure Resource Group을 기반으로 이루어집니다. 이번에 우리는 5개의 노드/VM로 이루어진 클러스터를 Azure Resource Group에 만들었습니다.

Service Fabric Cluster in the Azure Portal
그림 9 Azure 포털에서의 Service Fabric Cluster

클러스터에 어플리케이션/서비스 배포하기: 노드/VM이 실행되면 서비스를 배포할 수 있습니다. Azure에 있는 프로덕션 클러스터에 배포할 때는 보통 Windows 파워셸 스크립트를 사용하여 배포하곤 합니다. 스테이징/테스트 환경에 배포할 때는 비주얼스튜디오에서 바로 배포할 수 있습니다.

비주얼스튜디오 2015로 개발 PC에 있는 로컬 클러스터에 배포할 때는, IDE에 Service Fabric 어플리케이션 프로젝트를 오른쪽 클릭하여 Deploy 버튼을 클릭합니다. 물론 Windows 파워셸을 이용해서 여러분의 랩톱에 있는 개발용 클러스터에 배포할 수도 있습니다. Azure 클라우드에 있는 클러스터든 개발용 클러스터든 모두 동일한 Service Fabric 바이너리로 동작하기 때문입니다.

장기적으로 서비스를 운영할 때, 매일매일 배포에 대한 운영과 관리를 하는 것이 서비스를 부드럽게 발전하는데에 필수적입니다. Service Fabric에는 어플리케이션 라이프사이클 관리(ALM; Application Lifecycle Management) 기능도 있기 때문에 마이크로서비스 접근법을 염두하면서 해낼 수 있습니다. Service Fabric에 있는 운영과 관리 기능은 빠른 배포, 무중단(zero-downtime) 업그레이드, 서비스 헬스 모니터링, 클러스터의 확장/축소 등을 할 수 있습니다. Service Fabric의 무중단 업그레이드 기능은 순차적인 업그레이드 진행과 자동 헬스 체크 기능의 조합이며, 업그레이드 도중 어플리케이션이 불안정할 때는 롤백을 합니다. Service Fabric의 클러스터와 어플리케이션 관리는 Windows 파워셸 명령어로 할 수 있으며, CLI(Command Line Interface)와 스크립팅 모두 가능합니다. 그 외에 비주얼스튜디오에서 GUI 도구도 지원하므로 쉽게 관리할 수 있습니다.

업그레이드는 단계별로 진행됩니다. 각 단계마다 클러스터 내에 노드의 부분집합 별로 업그레이드가 적용되는데, 이를 업그레이드 도메인이라고 합니다. 그래서 업그레이드를 하는 동안에도 어플리케이션은 사용 가능한 상태로 있습니다. 버전을 붙일 수 있기 때문에 같은 마이크로서비스의 버전1과 버전2가 동시에 배포된 상태에서 요청에 따라 각각에 리다이렉트할 수도 있습니다. 더 자세한 정보는 문서(bit.ly/1kSupz8)를 참고해주세요.

PC에 설치된 개발용 로컬 클러스터에서 디버깅을 할 때 비주얼스튜디오는 서비스 프로세스가 이미 실행중이어도 바로 디버깅을 할 수 있도록 해주기 때문에 편합니다. IDE가 자동으로 프로젝트와 관련된 모든 마이크로서비스 프로세스에 붙여주고 평소처럼 중단점(breakpoint)을 이용하면서 Service Fabric의 마이크로서비스 코드를 디버깅할 수 있습니다. 그저 중단점을 찍고 F5만 누르면 끝입니다. Visual Studio에서 어떤 프로세스에 디버거를 붙일 것인지 찾을 필요도 없습니다.

Service Fabric Explorer: Service Fabric Explorer(탐색기)는 그림 10과 같은 모습입니다. 웹 기반 도구로 클러스터에 배포된 어플리케이션의 상태와 각각의 노드를 볼 수 있고, 다양한 관리를 할 수 있습니다. 탐색기 도구는 Service Fabric REST API가 제공하는 것과 동일한 HTTP Gateway 서비스를 이용하고 있습니다. 접속하려면 http://<your-cluster-endpoint&gt;:19007/Explorer 형식의 주소를 이용하면 됩니다. 로컬 클러스터의 경우, URL은 다음과 같을 것입니다. http://localhost:19007/Explorer.

The Service Fabric Explorer
그림 10 Service Fabric Explorer

Service Fabric Explorer에 대한 더 자세한 내용은, 다음 문서(bit.ly/1MUNyad)에 “Visualizing Your Cluster Using Service Fabric Explorer”를 참고해주시기 바랍니다.Service Fabric 플랫폼은 복잡한 시스템 건강 관리나 업그레이드에 시간을 쓰지 않고 어플리케이션 개발에 최대한 집중할 수 있도록 다양한 기능을 제공합니다. 그 기능은 다음과 같습니다:Stateless Services 수평확장(scale-out): Service Fabric 오케스트레이션 엔진은 새 노드가 클러스터에 추가되면 자동으로 웹앱을 더 많은 머신으로 확장합니다. stateless 서비스를 만들 때 몇 개의 인스턴스를 만들고 싶은지 설정할 수 있습니다. Service Fabric은 한 클러스터의 노드들에 그만큼의 인스턴스를 배치하고 각 노드에는 하나 이상의 인스턴스는 만들지 않도록 합니다. 모든 노드에 하나의 인스턴스를 생성하고 싶다면 인스턴스 숫자를 “-1″로 지정하면 됩니다. 이렇게 하면 클러스터를 수평확장할 때 stateless 서비스의 인스턴스 또한 새로운 노드에 생성될 것입니다.

자동 자원 분배: Service Fabric은 클러스터 내에 총 사용 가능한 자원을 파악하여 자원 밸런싱(또는 자원 오케스트레이션)을 합니다. 그림 11에서 보는 바와 같이, 정의된 정책과 제약사항 하에 생성된 마이크로서비스를 자동으로 VM간에 이동시키고 최적화합니다. 이런 작업은 비용 대비 성능에 초점이 맞추어져 있습니다.

Cluster Showing Services Distribution Across the Nodes and Automatic Resource Balancing
그림 11 클러스터 내에 노드 간 자동 자원 분배를 통한 서비스 분산

 

내장된 오류 해결과 복제 기능: 데이터센터 내의 머신이나 퍼블릭 클라우드는 예기치 못한 하드웨어 장애를 겪곤 합니다. 이런 경우를 방지하기 위해 Service Fabric은 내장된 오류 해결과 복제 기능을 제공합니다. 다시 말해, 하드웨어에 문제가 있더라도 서비스의 사용성은 영향을 주지 않습니다. Service Fabric에서는 각 서비스가 여러 인스턴스와 여러 실패영역(failure domain)에서 실행되기 때문입니다.

배치 제한(Placement Constraints): 클러스터에 프론트엔드 마이크로서비스는 미들티어 마이크로서비스와 같은 머신/노드에 배치하지 않도록 하거나, 특정 타입의 마이크로서비스는 같은 노드에 배치하지 않도록 할 수 있습니다. 이렇게 하면 충돌상황을 최소화할 수 있고 자원에 대한 특정 마이크로서비스에 자원 액세스에 대한 우선권을 줄 수도 있습니다.

요청에 대한 부하 분배: 자동 자원 분배와는 다른 의미로, 요청에 대한 부하 분산은 Service Fabric에서 처리하지 않습니다. 이들은 Azure 부하 분산 장치(Azure Load Balancer) 또는 Service Fabric 외부의 플랫폼에서 처리합니다.

헬스: 시스템을 모니터링하고 진단할 수 있습니다. 또한, 어플리케이션을 업그레이드할 때 안정성을 확인할 때 사용되며, 업그레이드가 불안정할 때 롤백할 수 있도록 합니다. 더 많은 정보는 다음 문서(bit.ly/1jSvmHB)에 “Introduction to Service Fabric Health Monitoring”을 참고해주세요.

Azure Service Fabric의 Stateful Microservices

Stateful 서비스는 Azure Service Fabric에서 흥미로우면서도 중요한 요소입니다. Stateful 서비스에 대해서 자세히 알아보기에는 다소 복잡하고 광범위하기 때문에 이 글의 주제를 벗어나므로 간단히 설명하도록 하겠습니다. 곧 나올 Service Fabric에 대한 문서가 있으니 거기서 중점적으로 다루도록 하겠습니다.

Service Fabric의 Stateful 마이크로서비스는 마이크로서비스 자체적으로 계산 능력과 데이터(인-메모리와 로컬디스크 모두) 뿐만 아니라 상태까지 함께 가지고 있습니다. 상태의 신뢰성은 데이터의 로컬본과 다른 노드/VM의 복제본을 통해 얻습니다. 기본적으로 각 데이터 파티션은 복제본 서비스와 연결되어 있습니다. 각 복제본은 고가용성을 위해 다른 노드/VM에 배포됩니다. 이건 마치 Azure SQL 데이터베이스가 복제본을 관리하는 형태와 유사한데, 그 이유는 다름이 아니라 Azure SQL도 Azure Service Fabric에 기반으로 개발했기 때문입니다.

복잡하고 확장성을 염두한 어플리케이션이라면 stateful 서비스는 기존의 전통적인 3-tier 아키텍처에 외부 캐시와 큐를 사용하는 것보다 구조를 단순화할 수 있고 콤포넌트 수도 줄일 수 있습니다. stateful 서비스를 사용하면 외부 캐시를 사용하던 것을 stateful 서비스 내부에 둘 수 있기 때문입니다. 그 외에, Service Fabric 마이크로서비스 안에 큐를 구현할 수 있으므로 외부에 큐도 필요하지 않습니다.

Stateful 서비스를 사용하면 자동으로 2차(secondary) 핫백업을 생성합니다. 이것은 1차(primary) 마이크로서비스에 하드웨어 장애가 있을 때 동일한 지점을 받아서 작업합니다. 서비스는 사용자 증가에 대응하여 꾸준히 확장해야 하는데, 이는 곧 하드웨어의 증설을 필요로 합니다. Service Fabric은 파티셔닝과 같은 기능을 통해 개발자가 개입할 필요 없이 자동으로 새 하드웨어에 서비스를 배치합니다.

Stateful Reliable Service는 데이터 파티셔닝을 지원하며, 복제본과 리더 선출 기능도 지원합니다. 또한, 복제본 서비스의 네이밍, 게이트웨이 서비스를 이용한 주소 디스커버리도 지원합니다. 트랜젝션을 통한 state 변화에 대한 동시성(concurrency)과 입자성(granularity)을 관리하며, 상태의 복제를 통한 상태 유지, 신뢰성있고 분산처리된 key/value 컬렉션(Reliable Dictionary and Reliable Queue)을 제공합니다. 좋은 점은, Service Fabric은 이 모든 것에 대한 세부적인 것과 복잡한 것을 처리해주므로 여러분은 어플리케이션 개발에 최대한 집중할 수 있다는 것입니다.

Azure Service Fabric의 Reliable Actors Services

Azure Service Fabric Reliable Actor는 Service Fabric의 액터 프로그래밍 모델입니다. 비동기적이며, 싱글스레드인 액터 모델입니다. 액터는 상태와 계산의 한 단위를 표현합니다. 이것은 Microsoft Research에서 만들고 있는 오픈소스 프로젝트 “Orleans”와 유사한 점이 있습니다. 중요한 점은, Service Fabric Reliable Actors API의 인프라는 Service Fabric이 제공한다는 것입니다.

IoT(Internet of Things)의 액터 인스턴스를 구현하는 것이 액터 서비스 사용에 대한 좋은 예시입니다. Vehicle이라는 타입의 액터를 C# 클래스로 구현해서 ‘자동차’ 도메인 로직을 캡슐화하고 여기에 GPS 좌표와 몇몇 데이터를 상태 정보로 구현합니다. 그러면 우리는 수백만개의 액터 오브젝트, 다시 말해 수백만개의 vehicle 클래스의 인스턴스를 클러스터의 여러개의 노드에 분산시킬 수 있을 것입니다.

물론 액터는 라이브 IoT 오브젝트만 의미하는게 아닙니다. 어떤 주제든 상관없습니다. 다만 ‘라이브 IoT 오브젝트’가 좋은 예시일 뿐입니다.

액터는 클러스터에 고가용성과 고확장성을 위해 분산 배치되며 각 VM에 인-메모리 오브젝트로 취급됩니다. 뿐만 아니라, 로컬디스크에 보존되며 클러스터 내에 복제본을 둡니다.

액터는 state와 behavior를 독립적으로 가진 형태의 격리된 싱글스레드 오브젝트입니다. 모든 액터는 Actor 타입의 인스턴스이며, .NET 코드로는 아래와 같습니다:

// Actor definition (State+Behaviour) to run in the back end.
// Object instances of this Actor class will be running transparently
// in the service back end.
public class VehicleActor : Actor, IVehicleActor
{
public void UpdateGpsPosition(GpsCoordinates coord)
{
// Update coordinates and trigger any data processing
// through the State property on the base class Actor.
this.State.Position= coord;
}
}

그 다음, 아래의 클라이언트 코드는 proxy object를 통해 actor를 사용하고 있습니다.

// Client .NET code
ActorId actorId = ActorId.NewId();
string applicationName = "fabric:/IoTVehiclesActorApp";
IVehicleActor vehicleActorProxy =
ActorProxy.Create(actorId, applicationName);
vehicleActorProxy.UpdateGpsPosition(new GpsCoordinates(40.748440, -73.984559));

모든 커뮤니케이션 인프라는 Service Fabric의 Reliable Actors 프레임워크가 알아서 해줍니다.

클러스터에 수백만개의 VehicleActor 오브젝트를 여러 개의 서로 다른 노드/VM에서 구동할 수 있습니다. 이에 대해 Service Fabric은 수백만개의 액터가 골고루 퍼지고 균형을 유지하도록 파티션과 복제본을 조절해줍니다.

Actors 클라이언트 API는 액터 인스턴스와 액터 클라이언트 간의 통신 기능을 제공합니다. 액터와 통신하려면 클라이언트는 액터 인터페이스를 구현한 액터 프록시 오브젝트를 만들어야 합니다. 클라이언트는 액터에 있는 메소드를 프록시 오브젝트를 통해 실행할 수 있습니다. 액터가 어울리는 시나리오라면 Stateful 서비스보다 마이크로서비스 구현을 매우 단순화시킬 수 있습니다. Stateful 서비스는 더 많은 제어권을 주지만 파티션된 데이터의 처리, 파티션 주소와 복제본 관리도 할 필요가 있기 때문입니다. 이렇게까지 세부적인 제어까지 할 필요가 없다면 Reliable Actor로 보다 수월하게 작업할 수 있습니다.

요약

마이크로서비스 아키텍처는 분산형 구조 하에 통제된 아키텍처와 디자인 프로세스가 필요합니다. Azure Service Fabric과 같은 새로운 기술은 팀을 더 소규모 개발 팀 단위로 구성할 수 있게 하므로 보다 역동적으로 변화시킬 것이며 마이크로서비스마다 애자일 원리를 적용할 수도 있습니다.

 


Cesar de la Torre Redmond, Wash에 사는 Microsoft의 시니어 프로그램 매니저입니다. 마이크로서비스 아키텍처와 도메인주도개발 방법론 등을 Microsoft Azure와 .NET 개발에 적용하는데에 관심이 많으며 서비스를 직접 다루는 모바일 앱 개발도 관심이 많습니다.

Kunal Deep Singh Microsoft Azure Service Fabric 팀의 시니어 프로그램 매니저입니다. Azure 이전에는 Windows Phone, Silverlight에서 여러 릴리즈 기간 동안 일했고 게임 개발자로 Xbox 타이틀 작업도 했습니다. Seattle, Wash에 삽니다.

Vaclav Turecek마이크로소프트의 시니어 프로그램 매니저입니다. 차세대 PaaS(Platform-as-a-Service)인 Azure Service Fabric의 뛰어난 사람들과 지치지 않고 일하고 있습니다.

이 문서의 공동저자이자 리뷰를 한 Microsoft 기술 전문가 Mark Fussell Mark Fussell은 클라우드 어플리케이션을 만드는데 열정을 쏟고 있고 Microsoft에서 오랫동안 데이터 액세스와 서버측 통신 기술을 만들었습니다. Service Fabric을 이용하여 개발자들이 마이크로서비스 어플리케이션을 만드는 것에 대해 관심이 많습니다.

이 문서를 번역한 김영재 교육서비스 바로풀기의 개발사 Bapul의 CTO로서 기술로 교육에 새로운 시각을 주기 위해 밤낮없이 개발하고 있습니다.