The Sapzil Ditto Kim's Dev Log

나도 MSA 한번 해보자 (1)

마이크로서비스 아키텍처에 대한 이야기는 최소 5년 전부터 꾸준히 들려왔던 걸로 기억한다. 하지만 회사에서 하던 프로젝트가 (MSA라고 하기는 조금 뭐하지만 어쨌든) 여러 서비스의 조합으로 구성되어 있었는데, 나쁜 경험을 많이 해서 막연한 거부감이 있었다. 그래서 서비스를 어떻게 잘 나누는 것이 좋은지 가끔 생각해보긴 했어도 ‘웬만하면 모노리스가 낫지’라는 마음가짐으로 살아왔다. (DHH가 작성한 The Majestic Monolith라는 글의 영향도 어느 정도 있었다.)

Monolithic vs Microservices
결국 다 똥인가…

세월이 흘러 회사에서 다른 프로젝트를 하게 되었고 이전의 경험을 반면교사 삼아 이번에는 하나의 코드베이스에서 최대한 서비스를 나누지 않았다. 초기에 프로젝트를 빠르게 진행할 수 있었고 지금도 어느 정도 레거시가 쌓이긴 했지만 기능을 추가하는데 크게 무리는 없는 상태다. (다른 팀원들은 어떻게 생각하는지 모르겠다 ㅠㅠ)

하지만 사업 측 이해당사자가 많아지고 팀원도 늘어나면서 서로 다른 기능 영역의 릴리즈 스케줄이 서로 꼬이기 시작했다. 어떤 기능 영역 하나에서 발생한 성능 문제가 서비스 전체에 영향을 주는 일이 생기기도 했다. 그러다보니 서비스를 적절히 나누면 독립적인 배포가 가능할 수도 있겠다는 생각에 MSA에 대해 다시 관심이 생겼다. 또 다른 계기로는 이전보다 MSA로 전환하는 사례가 많이 보이고, 채용공고에도 ‘MSA 경험자’ 같은 말이 등장하기 시작하는 분위기에 약간의 위기감을 느꼈던 것도 있다.

그리하여 MSA 기반의 연습 프로젝트를 해봐야겠다고 마음먹었다. 가장 먼저 프로젝트 주제를 정했는데, 문제 영역이 충분히 복잡해야 한다는 생각이 있었다. 예를 들어 투두리스트 앱이라면 머리를 최대한 짜내도 현실에서는 필요하지 않을 법한 이상한 기능을 추가하지 않는 이상 서비스를 여러 개로 나누기 힘들다고 봤다. 결론은 왓챠 (왓챠 플레이 말고!)를 베끼기로 했다. 오래 해오던 사이드 프로젝트와 비슷해서 구조에 대한 아이디어가 어느 정도 있었기 때문에 내게는 꽤 자연스러운 선택이다. 프로젝트 이름은 microservices + watcha = matcha로 정했다.

마이크로서비스에 대한 뇌피셜

MSA에 대해 나름대로 정의를 내려야 프로젝트의 목표가 좀 더 확실해질 것이다. 내가 생각하기에 대충 다음 조건을 만족하면 마이크로서비스라고 부르는 것 같다.

  • 다른 서비스와는 네트워크로 통신: OS 프로세스를 다른 서비스와 공유하지 않는다.
  • 독립적인 배포가 가능한 단위: 다른 서비스와 의존 관계일 수는 있지만 대부분의 경우에.
  • (논리적인) 데이터베이스를 다른 서비스와 공유하지 않음: 같은 데이터베이스 서버를 사용하더라도 Foreign Key를 걸거나 테이블을 Join하지 않는다.

이러한 조건 때문에 발생할 것으로 예상되는 여러가지 어려움이 있고, 어떻게 해결할 지 아이디어가 있는 것도 있고 없는 것도 있다. (없다면 공부하게 될 영역이다.) 하나씩 살펴보자.

분산 트랜잭션

가장 골치아플 것 같은 문제는 데이터의 일관성을 보장하는 것이다. 이전에는 데이터베이스 트랜잭션이 보장해주던 원자성을 잃어버리기 때문이다. 서비스 A가 담당하는 데이터와 서비스 B가 담당하는 데이터를 함께 변경해야 한다면, A의 데이터를 변경하고 나서 B의 데이터를 변경할 것이다. 하지만,

  1. 외부에서는 A의 데이터는 변경되었지만 B의 데이터는 아직 변경되지 않은 상태를 볼 수 있게 된다.
  2. A의 데이터 변경은 성공했지만 B의 장애로 B의 데이터 변경은 실패했다면 A와 B의 상태는 일관성이 깨진 채로 남아있게 된다.
  3. A의 데이터 변경은 성공했지만 B의 데이터 변경이 성립하는 제약 조건이 더이상 성립하지 않으면 A를 원래 상태로 되돌려야 한다.

1번 문제는 중간 상태가 보여도 문제가 없도록 서비스 경계를 잘 나눠서 회피할 수 있다고 본다. 하지만 회피할 수 없다면?

2번 문제는 A의 데이터 변경과 함께 원자적으로 이벤트를 발행하면 될 것 같다. 이러한 이벤트를 받아서 확실히 성공할 때까지 B에 데이터 변경을 전파해주는 녀석을 만들어야 한다. 원자적인 이벤트 발행을 위해서는 DB에 이벤트를 같이 쓰는 방법(outbox 패턴), DB의 데이터 변경 이벤트를 이용하는 방법(change data capture), 변경 자체를 이벤트로 나타내는 방법(이벤트 소싱) 등이 있을 것이다.

3번 문제는 saga 패턴을 적용하면 된다고 들은 적이 있는데 뭔지 잘 모르니 알아봐야 할 것 같다.

서비스 간 통신

서비스끼리 통신하려면 잘 정의된 API가 필요하다. RESTful API를 사용할 것인지, gRPC를 사용할 것인지, 서비스 코드 사이에 프로토콜은 어떤 식으로 공유하고 프로토콜 버전 관리는 어떻게 할 것인지 등의 고민을 해야한다.

서비스끼리 네트워크로 통신한다는 것은 더이상 다른 서비스를 믿을 수 없다는 말이다. 언제나 서비스 호출이 실패할 수 있다고 가정해야 한다. 여러 서비스가 의존하는 서비스가 느려지면 서비스 전체에 문제가 생길 수 있기 때문에 장애가 전파되는 것을 미리 차단할 필요가 있다. (circuit breaker)

또한 함수를 호출하는 것과 달리 서비스 호출은 스택 트레이스가 남는 것이 아니므로 기존 도구로는 디버깅이 어려워질 수 있다. 이를 위해 여러 서비스에 걸친 작업을 추적해야 한다. (distributed tracing)

데이터 통합

프론트엔드에서 ‘화면’을 그리기 위해서는 여러 서비스에 분산되어 있는 데이터를 각각 가져와서 적절히 합쳐야 한다. 하지만 프론트엔드를 구현하기 위해 어떤 데이터가 어느 서비스에 있는지 알아야 한다면 불편할 것이다. 그리고 내부 구조가 변할 때 외부 API의 소비자가 모두 업데이트 되어야 하는 문제도 있다.

이를 해결하기 위해 외부에서 오는 모든 API 요청을 받아주는 API 게이트웨이를 도입할 수 있을 것이다. 프론트엔드가 사용하기 쉬운 형태로 데이터를 통합해서 내려주는 역할이다. 가능하다면 프론트엔드에서 필요한 데이터만 가져올 수 있도록 GraphQL을 활용해보면 좋겠다.

또한 모든 요청이 API 게이트웨이를 통해 들어온다면 인증 처리를 API 게이트웨이에서만 하고 내부 서비스들은 검증된 아이덴티티를 그대로 사용할 수 있을 것 같다. 보안상 좋은 구조인지는 모르겠다.

배포와 테스트

고민할 것이 많다.

  • 서비스마다 다른 소스 코드 저장소를 사용할지, 아니면 모든 서비스를 한 저장소에서 관리할 것인지
  • 서비스 간에 코드를 공유할 것인지 말 것인지
  • 새로운 서비스를 쉽게 추가하려면 어떻게 해야 하는지
  • 개발할 때 여러 개의 서비스를 쉽게 띄우려면 어떻게 해야 하는지
  • 여러 서비스에 걸친 통합 테스트를 어떻게 구성할 것인지

마치며

혼자서 마이크로서비스 아키텍처를 구성하면 당연히 실제 프로젝트와 같은 경험을 얻지는 못할 것이다. 특히 서비스 분리 단위에 대한 비즈니스적 제약조건이 없기 때문이다. 그래도 앞서 나열한 것처럼 서비스를 분리하면 생길 기술적인 문제를 해결하는 경험은 해볼 수 있을거라고 봤다. MSA에 대한 많은 자료가 있지만 직접 코드를 작성해보지 않으면 알 수 없는 부분에 부딪혀보는 것이 목표다.

다음 글에서는 프로젝트 초기에 잡은 설계에 대해 소개해 볼 예정이다.

태그로 찾기: announcement architecture backend build-tools code-review database dependency-injection distributed docker github gradle graphql java javascript jersey jpa jquery kotlin linux maven microservice python react reactive redux relay rxjs spring ssr sysadmin testing tools translation ubuntu upstart web windows