Notice
Recent Posts
Recent Comments
Link
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

Jason's Blog

Monolithic Architecture에서 Microservice Architecture로의 Migration - 1 본문

Microservice Architecture

Monolithic Architecture에서 Microservice Architecture로의 Migration - 1

Jason Jongjin Lim 2018. 5. 25. 10:02

Monolithic Architecture에서 Microservice Architecture로의 Migration


나는 Kubernetes와 Container, 그리고 Middleware에 관련된 일을 한다. 일을 하면서 최근에 나에게 질문하는 키워드 중 가장 많은 비율을 차지하는 것이 바로 'MSA(Microservice Architecture)'이다. 특히 한국 환경에서 MSA를 어떻게 적용하고, 기존 아키텍처에서 어떤 방법으로 변환할 수 있는 지에 대한 조언을 구하는 내용이 많았다. 그래서 이번 기회에 Monolithic to Microservice Architecture에 대한 내용을 간단히 이야기 해보겠다. 본 내용은 Microservice Architecture의 개념은 이해하고 있는 사람에게 유용한 자료일 수도 있을 것 같다.(MSA의 개념은 본 블로그의 포스트를 찾아보면 설명이 되어있다.)


먼저 본 내용에 들어가기 전에, Monolithic Architecture에서 Microservice Architecture로 변환하기 위해 몇 가지 사전 이야기를 해보겠다.
- MSA는 모든 Application에 적용할 수 있는 것은 아니다.
- MSA로 이동해야 한다면, 기존 Monolith에서 감수해 내야만 하는 것들을 결정해야 한다
- 매우 드문 경우에만 Monolith의 모듈 그대로를 분리하여 MSA화 할 수 있다. 그 외의 경우에는 기존 프로세스를 유지하지 못한다.(코드 레벨까지 수정 혹은 재개발이 필요하다.)
- MSA가 비즈니스 확장성 및 생산성에 큰 이점이 있는 것은 사실이지만, 어떤 경우에는 Monolith를 그대로 유지하여 개발하고, 운영하는 것이 더 큰 business value를 가져다 주기도 한다.
- Monolith는 단일 객체 그 자체이기 때문에, 해당 architecture에 속해있는 데이터 모델 및 데이터베이스를 변경하는 것은 매우 어렵거나 거의 불가능에 가깝다. 이를 위해서는 데이터에 대한 Refactoring이 필요하다. 



Microservice 분리
Monolith에서 Microservice를 분리하는 것에 대해 구글링해보면, 원론적인 이야기가 주를 이룬다. 이 이야기들은 너무 어려운 이야기여서 우리가 실 업무에 적용하기 유용하지 않다. 그래서 아키텍처를 진화하는 방법을 christian의 논리를 참조하여 Step별로 하나씩 이야기 해보겠다. 이 내용은 코드의 API화나 Integration 등에 대한 이야기 보다는 종속된 데이터 관계를 어떻게 분리시킬 것인지, 분리된 데이터 관계로 인한 모듈화에 대한 이야기이다.
먼저, 다른 포스트에서 찾은 이야기 중 몇 가지 중요한 접근 방식에 대해 이야기해보겠다.
- 모듈에 대한 식별 : 기존 모듈을 유지하거나, 새로운 모듈 작성
- 데이터베이스 재정의 : 각각 모듈에 해당하는 테이블을 분리하고 서비스로 wrap up
- 코드 업데이트 : 새로운 서비스를 호출하기 위해 DB 테이블에 직접 호출했던 코드 업데이트
- 위 방법을 지속적으로 강화 및 반복
이 내용을 Step 별로 자세히 살펴보겠다. 



Step 1 : 모듈에 대한 식별


이 시나리오는 다루기 어려운(nasty) monolith를 가지고 있다는 것으로 부터 시작한다. 위의 이미지에서 보면 각 서비스 모듈과 데이터베이스의 관계를 단순화하여 표현했다. 우리는 어떤 기능(모듈)을 분리하고 싶은지, 해당 서비스가 어떤 테이블에 연관되어 있는지에 대해 판단한 다음 그 곳에서 부터 변화를 시작해야 한다.

Step 2 : Database table 분리, 서비스로 포장, dependencies 업데이트




다음 단계는 Cart 모듈이 사용하는 테이블을 찾아내고, 테이블을 자체 서비스로 분리하는 것이다. 분리한 서비스는 이제 Cart 테이블에 접근할 수 있는 유일한 모듈이다. Cart 테이블만은 더이상 공유할 수 있는 다른 서비스가 존재하지 않는다. 이 것이 To MSA를 위한 가장 중요한 내용이다. 각 모듈의 독립성을 유지하기 위해서는 반드시 데이터가 분리되어야 한다.

또한 다른 모듈에서 Cart에 대한 내용을 참조하기 위해선 새로 생성된 서비스의 API를 통해야 한다. 이 통신은 Message Queue나 API Gateway 등을 통해 더 뛰어나게 구현할 수 있지만, 이 내용은 차후에 다시 설명하겠다. 

Step 3 : 개선과 반복

마지막 단계는 더이상 모듈화 단계를 반복할 수 없을 때까지 이 작업을 반복하고 반복한다. 위 그림에서 우리는 Store 서비스에 대해 동일한 작업을 수행했으며, Store 서비스가 데이터를 소유하고, 공유를 위해 API를 공개하는 아키텍처로 변화했다는 것을 알 수 있다. 이 것은 우리가 그동안 이론적으로 접해왔던 Microservice와 매우 유사한 방식으로 볼 수 있다. 이 방식은 일반적으로 봤을 때 적절한 가이드 라인이지만, 이 가이드를 따르게 되어도 서비스에서 정말로 필요한 요소를 놓치게 될 수도 있다. 그 요소들은 다음과 같다.

- 현재 존재하는 많은 수의 monolith가 위와 같은 방법으로 깔끔한 모듈화가 어렵다.
- 테이블 간의 정규화, 엔티티 결합, 무결정 제약 조건 등을 유지하기 까다롭다.
- Monolith에 복잡한 Query 코드를 작성했을 때, 어떤 테이블을 어떻게 사용했는지 이해하기 힘들 수 있다.
- 단순하게 모듈을 쪼개는 방식이 아닌 매우 까다롭고 비용이 많이 드는 migration이 있을 수 있다.
- 모듈화를 진행하다 보면, 오히려 쪼개지 않는 쪽에 이점이 많은 지점이 생길 수 있다.
- 등등등등

샘플 아키텍처 변화
아래에서 구체적인 예시를 보고 방법론/패턴이 어떻게 나타나는지, 우리가 실제로 이 변화를 수행할 때 가질 수 있는 옵션에 대해 살펴보겠다.

이 예시들은 monolith를 분해하는 데 이용할 수 있지만, monolith에 새로운 기능을 추가하는 데에도 사용할 수 있는 패턴이다. 

1. Monolith 판단
이 Monolith 예시는 실제로 developers.redhat.com의 TicketMonster(https://developers.redhat.com/ticket-monster/)라는 어플리케이션으로 이야기 할 것이다. 이 튜토리얼은 일반적인 Java EE 어플리케이션을 구축하는 과정을 보여주는 아주 좋은 예이다. 지나치게 복잡하진 않지만, 핵심 포인트를 설명하는 데 사용할 수 있는 충분한 규모의 서비스이다.


아래 이미지를 보면 Monolith는 모든 모듈, DB, UI가 동시에 배포되어 있음을 판단할 수 있다. 이 것은 새로운 배포가 발생했을 때, 모든 서비스가 반드시 멈추어야만 하는 속성을 지니고 있다. 고로, 새로운 변화를 일으키는 데에는 조직, 프로세스, 예산 등의 큰 장애물들이 존재하는 것을 인지할 수 있다. 하지만 우리는 새로운 고객 및 비즈니스를 창출하기 위해 해야만 하는 변화를 제시할 것이다.



고려사항

- Monolithic code와 DB는 변화하기 매우 어렵다.
- MSA로의 변경은 팀간의 높은 수준의 협업과 커뮤니케이션이 필요하다.
- 우리는 반복적인 분석을 통해 수 없이 많은 테스트를 실시해야 한다. 비즈니스는 안정성이 필수이기 때문이다.

- 완벽한 자동화를 이용하여 새로운 프로세스를 배포해야 한다. 



2. UI 추출


이 단계에서는, UI를 Monolith에서 분리한다. 사실 이 아키텍처에서 실제로 Monolith에서 모듈을 분리하는 작업은 수행하지 않는다. 단지, 그 안에 있는 UI와 같은 형태로 구성이 된 새로운 모듈을 배포하면서 Monolith 분리에 대한 리스크를 감소시킬 수 있다. 다만, 이렇게 만든 새로운 UI 모듈은 기존의 UI 구조를 그대로 따라야 하며, Monolith의 REST API를 다시 호출하는 로직을 만들어야 한다. 물론 이 방법은 기존 Monolith에서 UI에 대한 API Interface가 설계되어 있어야 한다.

우리는 이 새로운 UI 모듈을 아키텍처에 배치하고 여러가지 라우팅 플랫폼 사용하여 Monolith에 천천히 라우팅 할 수 있다. 이 방법으로 다운 타임 없는 변화가 될 수 있다. 이 방법은 다른 포스트에서 언급한 DevOps 배포 프로세스 중 하나인 canary/blue-green deployment에 대한 이야기가 될 수 있다.


고려사항

- 이 첫 번째 단계에서 Monolithic Application을 수정하면 안된다. 단순히 Monolith에 포함되어있는 UI를 Copy-Paste로 복사를 하여 분리를 시켜야 한다.
- 작업을 수행하기 전, UI와 Monolith 사이에 적절한 API Interface가 설계되어 있어야 한다.
- Ingress 구간이 두 채널이 되기 때문에, 보안에 대한 고려도 필요하다.

- UI와 Monolith 두 구간으로의 트래픽 분산을 위한 플랫폼이 설계되어있어야 한다. 또한 Canary, Blue-green, Rolling Deployment 정책을 수행하기 위한 프로세스도 존재해야 한다. 



3. UI를 Monolith에서 분리



이전 단계에서, 새로운 UI를 배포하고 기존에 Monolith와 직접 통신을 했던 트래픽을 서서히 UI로 라우팅을 변경해 주는 작업을 진행했다. 모든 트래픽이 새로운 UI 모듈로 이동했을 때, Monolith에 존재하던 UI에 관련된 모듈을 중지시키고, 제거한다. 만약 이 과정에서 문제가 생겼을 시에는 바로 전 버전인 UI - Monolith로 나뉘어 라우팅되던 버전으로 Rollback을 수행한다.

UI를 완전히 분리함으로써, Monolith의 분해의 첫 시작을 할 수 있었고, Blue-green, canary deployment들의 리스크를 감소시킬 수 있다.



고려사항

- 결론적으로는 UI 모듈을 Monolith에서 완전히 제거하는 작업이다.
- 이 작업을 수행하려면 Monolith에 대한 최소한의 변경이 필요하다.

- 위에서 언급했지만, Routing/Shaping 방식을 사용하여 다운타임 없이 변화를 수행해야 한다. 




4. 새로운 서비스 도입

다음 단계에선 Coupling이나 DDD(Domain Driven Design)과 같은 복잡한 구조는 넘어가고, 새로운 서비스인 Orders라는 서비스를 아키텍처에 삽입한다.(실제 구조에선 새로운 기능 등이 될 수 있다.) 이 서비스는 비즈니스 로직 자체가 나머지 Application보다 자주 변경되는 매우 중요한 시스템이며 상당히 복잡한 구조를 지니고 있다. 여기서 CQRS같은 아키텍처 패턴을 이용한 구현을 적용 할 수 있다.

새로운 이 서비스는 Backend(기존의 Monolith에서 UI가 제거된 구조)와의 연계 측면에서 Service Boundary와 API에 중점을 두고 설계해야 한다. 실제로 이 새로운 서비스는 기존 코드에서 포팅한다기 보단 새로 작성될 가능성이 매우 크지만, Backend와 구조상으론 비슷한 아키텍처를 가져가야 한다. 또한 위 그림에서 보면 Orders는 개별 데이터베이스를 가지고 있다. 이것은 De-coupling 측면에서 매우 좋은 그림이라는 것을 알 수 있다. 


이 단계에서 부터 전체 서비스에서 발생하는 이벤트가 발생하거나 처리할 수 있는 로직에 대한 고려가 되어야 한다. 데이터베이스가 나눠지며 트랜잭션에 대한 이슈가 발생할 수 있기 때문에, 보상 트랙잭션에 대한 프로세스도 설계가 되어있어야 한다.
- 새로운 Orders 서비스는 기존 Backend와의 API 디자인 및 Boundary에 대한 우선적인 설계가 될 것이다.
- 이 Orders는 Backend에서 분리된 것이 아니라, 새롭게 작성된 기능이다.
- API 설계를 결정한 후에는, 간단한 Scaffolding 혹은 Place holder를 구현할 것이다.
- 새로운 서비스는 자체 데이터 베이스를 가지고 있다.

- 아직 이 단계에서는 Orders에 트래픽이 접근하지 않는다. 




5. 새로운 서비스와 API 통합

이 시점에서 부터 서비스의 API와 Domain model을 코드에서 구현되는 방식으로 지속적으로 개선해야 한다. 이 서비스는 새로운 트랜잭션 워크로드를 자체 데이터베이스에 저장하고 다른 모든 서비스와 별도로 유지해야 한다. 이 데이터에 대한 액세스가 필요할 땐 Orders의 API를 통해 호출해야 한다.

단, 실제 구현된 서비스에선 이 데이터베이스를 완벽하게 분리하기 힘들 수도 있다. 실제로 새로운 서비스와 기존의 서비스의 데이터는 밀접하게 관계되어 있으며, 이 관계를 분리하기 위해서는 데이터 모델의 정규화, FK 제약 조건, relationship 등으로 인해 많은 이슈를 직면할 수 있다.

단 한번에 완벽한 데이터베이스 분리가 어렵기 때문에, 이 단계에서는 임시로, Backend와 OrdersDB의 임시 커넥션을 만들어 놓고, 필요할땐 직접 데이터를 쿼리한다. 물론 이 처리를 하기 위해 Consistency model에 대한 고려가 필요하다.

이 방식이 매우 복잡하고 위험하다고 생각할 수도 있지만, 이 아키텍처가 최종 그림이 아니라는 것을 인지하고, 최대한 효율적으로 데이터를 분리하는 걸음 중 한 가지라고 판단해야 한다. 실제로 이 방식은 레퍼런스에서 많은 성과를 거뒀다. 물론 이 방법보다 좋은 방법이 존재한다. 그건 이 게시물의 2탄에서 소개하도록 하겠다. 



고려사항

- 새로운 서비스인 Orders는 Monolith의 데이터 모델과 긴밀하게 결합된 모델을 가지고 있다.
- 실 상황에서 Monolith는 데이터를 얻기위한 API가 설계되어있지 않을 가능성이 크다.
- Read-only 쿼리를 위해 임시적으로 Backend DB에 다이렉트로 접근할 수 있다.

- Monolith의 DB가 수정되는 일은 거의 없다. 




6. 새로운 서비스의 Dark launch

이제 Orders라는 새로운 서비스로 트래픽을 이동시키는 로직을 구현해야 한다. 여기서 중요한 점은 항상 Microservice로 이전할 때에는 빅뱅 방식으로 구조를 잡지 않는 것이다. 이 것은 테스트가 되지 않은 로직을 프로덕션 환경으로 넘기는 것을 막는 것을 의미한다.

기존의 Backend v1로 들어가는 트래픽 및 다이렉트로 Orders의 데이터를 가져오는 로직은 그대로 유지하되, Orders의 DB를 다이렉트로 연결하지 않고, REST API로 서로 데이터를 주고 받는 로직으로 이루어진 Backend v2를 배포한다. 이 것은 추후에 Blue-green 배포로 안정적인 트래픽 이동이 이루어 질 워크플로우를 구성해야 한다. 이제 UI 모듈에서 Backend의 Blue와 Green 버전으로 트래픽을 제어하는 것은 힘들어진다. 그래서 새로운 게이트웨이 모듈/플랫폼을 UI와 Backend 사이에 위치시키고 트래픽을 제어해야 한다. 



고려사항

- Code path에 새로운 Orders 서비스를 적용하면 많은 이슈가 발생할 수 있다.
- 새로운 서비스의 영향도를 체크하고 모니터링 해야한다.
- 트래픽 제어를 위한 게이트웨이 플랫폼에 대한 구성이 필요하다.

- 이 게이트웨이 플랫폼을 이용하여 특정 사람 혹은 집단의 트래픽을 새로운 Backend 서비스로 연결해야 한다. 




7. 새로운 서비스로 Canary/Rolling 배포

바로 전 단계에서 v1의 프로덕션 모듈에서 v2의 섀도우 모듈로 특정 그룹 테스트 트래픽을 이전시키면서 섀도우 모듈이 안정적인 모듈이라는 판단이 될 때 까지 테스트를 반복한다. 이 테스트가 완료되었을 때, 시나리오에 따라 Rolling Release 방식으로 트래픽을 서서히 넘겨준다. 이슈가 발생될 시 즉시 서비스 타겟을 Old 버전으로 이전시켜준다.

여기서 구체적으로 고려해야 할 한 가지는, 바로 세션 유지이다. Rolling Release로 New version으로 넘어간 세션은 반드시 그 버전으로 계속 트래픽을 유지해야 한다. v1과 v2를 Load-balancing 방식으로 계속 트래픽을 공유하면 안된다. 이럴 경우 Rollback 시나리오시 더욱 더 복잡한 로직을 지니게 될 수 있다.



고려사항

- 바로 이 ‘cohort group’을 식별하고 새로운 Microservice에 실시간 트랜잭션 트래픽을 보낼 수 있어야 한다.
- 트랜잭션이 여전히 두 코드 경로로 진행되는 일정한 기간이 있기 때문에, 여전히 Monolith에 직접 데이터베이스 연결이 필요하다.
- 모든 트래픽을 새로운 버전인 Microservice로 이동 한 후에 그 전 기능을 제거해야한다.

- 라이브 서비스를 새로운 v2 서비스로 보내면 이전 버전으로 Rollback 할 때 이슈가 발생할 수 있다. 




8. Offline data ETL/Migration


이 시점에서 부터 Orders Microservice가 실제 프로덕션 트래픽을 가져가기 시작한다. Backend는 여전히 다른 문제들에 직면해 있지만, Orders 및 UI에 대한 서비스 기능은 성공적으로 monolith에서 마이그레이션 할 수 있었다. 이 상황에서 가장 먼저 직면할 이슈는 새로운 Orders Microservice와 Backend 사이에 다이렉트 DB 연결을 한 부분에 대한 수정이 필요하다는 것이다. 이것은 아마도 Monolith DB에서 새로운 서비스로 일종의 ETL을 필요로 하게 될 것이다. 이 상황에서 많은 생각이 필요하다. 예를 들어, Monolith의 데이터와 Microservice 데이터가 공유 가능한 데이터인지 확인해야 하며, 공유 및 변경이 가능하다면 데이터 소유에 대한 이슈를 해결해야 한다.



고려사항

- 데이터 분리에 따른 다양한 이슈에 주의해야 한다. 



9. Datastore와 연결 해제 및 Decouple



이전 단계를 완료한 후 Orders의 API Interface를 backend 다이렉트로 연결된 것이 아닌 gateway 모듈로 연결하여 완전한 De-coupling 구조로 만든다.
최종적으로 정리하자면 본 포스트에서는 간단한 Monolithic Architecture를 Microservice Architecture로 변환하고, 새로운 서비스가 연결되었을 때, 독립된 구조로 연동하는 방식에 대해서 설명하였다. 이보다 더 복잡한 구조를 지닌 서비스라면 위에서 언급한 작업들을 지속적으로 반복하여 가장 생산성이 높을 로직으로 만들어 주는 것이 숙제로 남아있다. 
Monolith에서 Microservice로 변환하는 것은 매우 어렵다. 많은 이슈들이 잠재하고 있으며 비용도 많이 소요될 것이다. 그러나 그 것들을 감수할 만큼 적용한 서비스의 비즈니스 가치에 대한 이익은 생각지도 못한 수치일 것이라고 생각한다.

다음 포스트에서는 앞에서 언급한 예제 서비스로 이런 단계를 수행하는 방법을 보여주고, 변환에 도움을 주는 도구, Framework 및 Infrastructure에 대해 자세히 살펴 볼 것이다.  


* 이 내용은 같은 회사의 christian posta(http://blog.christianposta.com) 블로그를 참조하여 작성되었습니다.





Comments