MSA가 좋지만은 않은 이유

MSA가 좋지만은 않은 이유

마이크로서비스 아키텍처(MSA)의 목표는 하나의 거대한 서비스를 일정한 기준으로 쪼갠 여러 stand-alone 서비스로 구동하여 각 애플리케이션의 복잡도와 이들 간의 결합도를 낮춤으로써 궁극적으로는 전체 시스템을 좀 더 생산적으로 개발하고 운영하는 것이라고 볼 수 있다.

시스템의 요구사항이 많아지면 애플리케이션은 점점 덩치가 커져 하나의 거대한 모놀리식 애플리케이션이 되고, 그렇게 되면 다음과같은 단점이 있기 때문에 서비스를 더 작은 부분들로 쪼개는 것이다.

모놀리식 애플리케이션의 단점

낮은 개발 생산성

시스템의 요구사항이 많아질 수록 코드의 양은 늘어날 것이다. 그러면 개발자가 모든 피쳐를 파악하기가 점점 더 어려워지기 때문에 새로운 기능을 추가하거나 코드를 변경하기도 더 어려워 질 것이다. 빌드 시간도 함께 늘어날 것이다. 예를 들어 배달 서비스를 개발한다고 할 때, 음식을 주문하는 로직에 아주 약간의 코드를 변경했다고 해보자. 그럼에도 불구하고 거의 전체 애플리케이션을 다시 빌드하고 재배포해야 할 수도 있다. 빌드 시간이 느려지면 코드를 변경하고 재배포 될 때 까지의 시간이 늘어나기 때문에 개발 생산성이 떨어질 수 있다. 그런데 만약 주문 서비스가 분리되어 있다면 주문 서비스만 빌드하고 재배포하면 될 것이다.
게다가 코드의 양이 과도하게 많으면 IDE 의 실행 속도도 느려지기 때문에 개발 환경에 따라 개발 생산성에 영향을 미칠 수도 있다.

팀 운영의 어려움

애플리케이션의 규모가 커질 수록 그에 비례하여 필요한 인력도 많아질 것이다. 한 팀이 너무 많은 인원으로 이루어진다면 다양한 의견을 수렴하거나 의사결정하는데 어려움이 있을 수 있다. 만약 팀을 효율적으로 운영하기 위해서 여러개의 팀으로 나누어 하나의 코드 베이스를 개발하고 운영하더라도 역할의 경계가 명확하지 않아 책임소재도 불분명해지는 등 다양한 어려움이 있을 수 있다. 하지만 만약 하나의 거대한 서비스가 여러개의 작은 서비스로 분리된다면 팀 별로 서비스를 나누어 독자적으로 개발 및 운영을 할 수 있게 된다. 그리고 팀마다 서로 다른 프로그래밍 언어, 또는 기술을 사용할 수 있게 되어 새로운 기술의 도입이 비교적 자유롭다는 추가적인 이점도 있다.

결함 격리의 어려움

주식 거래 서비스를 예로 들면, 모놀리식 애플리케이션에서 사용자의 주식 주문 히스토리를 반환하는 API 에 문제가 생겨 애플리케이션이 다운되었다고 해보자. 그러면 다시 복구가 될 때까지 사용자는 원활하게 주문을 하지 못할 수도 있고, 결과적으로 회사는 막대한 금전적인 손실을 입을 수도 있다. 하지만 만약 주문 히스토리 서비스와 주문 서비스가 분리되어 별도의 애플리케이션으로 실행 중이라면, 주문 히스토리 서비스가 다운되더라도 사용자가 자신의 주문 히스토리를 볼 수 없게 될 지언정, 주문을 하는데는 아무런 문제가 없기 때문에 상대적으로 금전적인 손실을 덜 입을 가능성이 높다.

확장의 어려움

예를 들어 이미지 프로세싱은 CPU 집중적인 작업이기 때문에 높은 CPU 성능이 필요할 것이고, 많은 데이터를 메모리에 유지하는 작업은 많은 메모리 용량이 필요할 것이다. 하지만 이미지 프로세싱 모듈과 많은 메모리를 사용하는 모듈이 하나의 애플리케이션으로 실행 중이라면 각 모듈을 독립적으로 확장할 수가 없기 때문에 상대적으로 리소스 사용 효율성이 떨어질 수 있다.

MSA 에서 추가로 고민해야 할 점

시스템을 MSA 로 구성하면 위와 같은 모놀리식 애플리케이션의 문제를 어느정도 해결할 수 있다. 하지만 모든 기술에 trade-off 가 있듯이 MSA 에도 단점이 있다. 한 줄로 요약하자면 할 일이 많아지고 고민해야 할 부분이 많아진다는 것이다. 그렇기 때문에 개발 및 운영 생산성이 단순히 향상된다고만 볼 수는 없다.

IPC

도메인 서비스 간의 상호작용 시 모놀리식에서는 하나의 프로세스 내에서의 메서드 콜이었지만 MSA 에서는 각 서비스가 별도의 프로세스로 실행되므로 IPC(Inter-Process Communication) 가 된다. 보통 각 서비스의 프로세스는 네트워크 상에서 떨어져서 실행된다. 그렇기 때문에 이 네트워크 상에서 떨어진 서로 다른 프로세스 간의 통신을 위한 방법을 고민해야 한다. 가장 떠올리기 쉬운 방법은 역시 HTTP 나 gRPC 같은 동기 방식이다. 하지만 동기 방식으로 통신하기 위해서는 통신의 양쪽 주체가 모두 정상적으로 실행 중이어야 하기 때문에 상대적으로 가용성이 떨어진다.
비동기 메시징 방식으로 통신할 수도 있다. Akka 와 같은 별도의 메시지 브로커 없이 동작하는 액터 기반의 도구를 사용할 수도 있을 것이다. 관리 대상 컴포넌트가 추가되지 않는다는 장점이 있지만 가용성은 낮다. 카프카와 같은 메시지 브로커를 통한 비동기 메시징 방식을 사용하면 관리 대상 컴포넌트가 추가되고 SPOF 가 되지 않도록 클러스터링을 해야하기 때문에 복잡도는 증가하지만 가용성이 높고 여러 이점이 있기 때문에 event-driven microservice architecture 를 구현할 때 보통 많이 사용되는 방식이다. 또한 카프카와 같이 at-least-once 전달 방식의 메시지 브로커를 사용한다면 애플리케이션의 메시지 컨슈머를 멱등하게 작성해야한다는 사실도 잊으면 안된다. 각 IPC 방식의 장단점이 있기 때문에 필요에 따라 적절히 선택하여 사용하는 것이 가장 좋을 것이다. 서로 다른 서비스의 엔드포인트를 알아내기 위해 서비스 디스커버리도 필요할 수 있다. Netflix 의 유레카 같은 도구를 쓰기도 하지만 쿠버네티스를 사용한다면 굳이 별도의 서비스 디스커버리를 위한 도구를 고려할 필요는 없다. 아무튼 어떤 방법을 사용하던 관련 코드가 추가되고, 관리 대상 컴포넌트가 추가될 수 있기 때문에 모놀리식 애플리케이션과는 다른 이유에서 어느정도의 개발 및 운영 비용이 증가한다고 볼 수 있다.

분산 트랜잭션

모놀리식 애플리케이션에서는 하나의 로컬 DB 트랜잭션으로 여러 하위 도메인의 데이터를 ACID 하게 변경할 수 있다. 예를 들어 주식 거래 앱으로 사용자가 했던 주문이 체결되었을 때 하나의 로컬 트랜잭션으로 사용자가 보유한 현금은 줄이고, 주문 상태는 '체결됨' 상태로 변경하고, 보유 주식은 늘리는 것이 가능하다. 하지만 MSA 에서는 각 서비스가 별도의 DB 를 가지고, 기존에 하나의 DB 에 모두 저장되었던 데이터는 각 마이크로서비스의 DB 에 나뉘어져 관리된다. 그렇기 때문에 만약 주식 거래 서비스를 주문 서비스, 주식 잔고 서비스, 회계 서비스 등으로 구성한다면 각 서비스에서 실행되는 여러 로컬 트랜잭션을 하나의 글로벌 트랜잭션으로 묶어 ACID 하게 할 필요가 있는 것이다. ACID 트랜잭션에 대한 이해가 없다면 분산 트랜잭션 없이 그냥 매번 HTTP/gRPC 등으로 다른 서비스에 요청하여 데이터를 변경시키면 되는거 아닌가 할 수도 있는데, 이러면 데이터의 변경이 Atomic 하지 않기 때문에 중간에 서버나 인프라 오류 등 발생 시 데이터 정합성이 깨질 수 있으며, 트랜잭션 컨텍스트도 유실될 가능성이 있다. 아무튼 분산 트랜잭션을 하는 방법은 몇 가지가 있는데 우선 2PC[1] 방식으로 처리할 수 있다. 이 방식의 단점은 가용성이 낮다는 것이다. 분산 트랜잭션을 성공적으로 수행하기 위해서는 트랜잭션의 시작과 끝까지 트랜잭션에 참여하는 모든 컴포넌트가 정상적으로 실행 중이어야 하며 트랜잭션 커밋 요청을 정상적으로 수행해야 한다. 만약 한 컴포넌트라도 실행 중이지 않다면 전체 트랜잭션은 성공할 수 없을 것이다. 즉 트랜잭션에 참여하는 컴포넌트들에게 강한 의존성을 갖는다고 볼 수 있다. 또 다른 방법으로는 Saga 패턴이 있는데, 트랜잭션에 참여하는 컴포넌트들 간의 결합을 느슨하게 하여 2PC 방식보다 높은 가용성을 가질 수 있다[2]는 장점이 있어 요즘 많이 사용되는 방식이다. 사가 패턴에서는 각 로컬 트랜잭션이 비동기 메시지를 통해 순차적으로 커밋이 된다. 만약 중간에 문제가 발생하면 지금까지와 반대의 순서로 보상 트랜잭션을 실행한다.
하지만 사가 패턴은 ACID 에서 I(Isolation)가 빠진 ACD 트랜잭션이다. 즉 여러개의 트랜잭션이 동시에 실행될 경우 격리가 되지 않아 서로에게 영향을 미칠 수가 있다는 뜻이다. 이 때 발생할 수 있는 대표적인 문제는 한 트랜잭션이 변경한 데이터를 다른 트랜잭션이 덮어 쓰는 lost updates 나 한 트랜잭션이 실행되는 도중에 다른 트랜잭션이 변경한 데이터를 읽는 dirty reads 와 같은 것들이 있다. 이런 비격리로 인해 발생하는 문제를 해결하기 위한 여러 방법들이 있다. 높은 일관성을 요구하는 데이터만 사가 패턴이 아닌 2PC 를 사용하는 식으로 혼용해서 사용할 수도 있다. 아무튼 사가 패턴을 사용할 경우 이런 부분을 꼼꼼하게 고려하지 않으면 큰 문제로 이어질 수 있기 때문에 신경써야할 부분이 더 많다.

뿐만 아니라, 사가 패턴을 사용하거나 도메인 이벤트를 발행해야 하는 경우 이벤트의 발행이 누락되면 안된다. 그래서 데이터의 변경과 메시지의 발행을 Atomic 하게 동작하도록 하기 위한 Transactional Messaging 도 고려해야 한다. DB 와 메시지 브로커는 트랜잭션 메커니즘이 다르기 때문에 서로의 operation 을 하나의 트랜잭션으로 묶을 수가 없다. 그렇기 때문에 이 둘에 따로 따로 쓰기를 수행할 경우(일명 dual-writes) 일부 데이터가 유실되어 inconsistent 하게 될 수 있기 때문에 DB 던 메시지 브로커던 둘 중 하나에만 쓰기를 수행해야 한다. 만약 메시지 브로커에만 쓰기를 수행하는 경우를 본다면, 메시지 브로커에 데이터 변경 이벤트를 쓰고 서버가 이를 구독하여 DB 업데이트를 할 수 있다. 이 경우 메시지의 발행과 구독 사이의 lag 이 존재하기 때문에 유저가 방금 변경한 데이터를 즉시 볼 수 없는 문제가 발생할 수 있다. 로컬 캐시를 통해 해결할 수도 있지만 다수의 인스턴스가 실행 중인 경우에는 문제가 복잡해진다. 그렇기 때문에 이 방식 보다는 반대로 DB 에만 쓰기를 하는 방식이 더 선호된다. DB 에 저장된 데이터의 변경과 발행하고자 하는 이벤트의 쓰기를 하나의 트랜잭션으로 묶는 Transactional Outbox Pattern 을 사용하는 것이다.
Outbox 패턴을 구현하는 대표적인 방법이 CDC(Change Data Capture) 를 사용하는 것이다. CDC 는 polling 방식이나 transaction log tailing 방식을 사용하여 구현할 수 있는데, polling 방식은 주기적으로 DB 쿼리를 하는 방식이기 때문에 구현이 간단한 대신 DB에 불필요한 부담을 줄 수 있으며 데이터 변경을 포착하는데 있어 약간의 delay 가 있을 수 있다는 단점이 있다. 반면, transaction log tailing 방식은 이름처럼 DB 트랜잭션 로그를 테일링하는 방식으로, 구현이 복잡하고 별도의 운영 프로세스가 늘어난다는 단점이 있지만 DB에 불필요한 부담을 주지 않고 데이터 변경 이벤트를 실시간으로 메시지 브로커에 보낼 수 있기 때문에 확장성 측면에서는 더 좋다고 할 수 있다. Debezium 은 Kafka Connect 를 활용한 대표적인 transaction log tailing 방식의 CDC 도구이다. 물론 CDC 를 위한 별도의 시스템 컴포넌트가 필요할 수 있다. 예를들어 Debezium 을 사용한다면 Kafka Connect cluster 를 운영해야한다.

사가 패턴을 사용한다면 시스템 요구사항에 따라 Orchestration Saga 와 Choreography Saga 중에 어떤 종류의 사가를 사용할지도 결정해야 한다. 만약 오케스트레이션 사가를 사용한다면 사가 코디네이터에 오케스트레이터 상태도 저장하고 관리해야 한다.
아웃박스 패턴이 아닌 Event Sourcing 을 사용할 수도 있다. 이벤트소싱에서는 이벤트가 데이터의 source of truth 이므로 데이터를 변경해야할 때는 Event Store 에만 이벤트를 발행하고 별도의 로컬 DB 업데이트를 할 필요가 없어지기 때문이다. Axon Framework 는 Outbox 패턴 대신 Event Sourcing 을 사용하는 대표적인 예다.[3]

쿼리

앞서 말한 것처럼 모놀리식 애플리케이션에서는 보통 모든 데이터가 하나의 DB 에 저장된다. 그렇기 때문에 여러 데이터를 조인해야하는 경우 간단하게 조인 쿼리를 수행할 수 있다. 하지만 MSA 의 경우 데이터가 서로 다른 물리 DB 에 저장되므로 조인 쿼리를 할 수가 없다. 이를 해결하기 위한 방법은 크게 2가지가 있다. 첫번째는 API Composition Pattern 을 사용하는 것인데, 쉽게 말해 데이터를 인메모리 조인하는 것이다. 하지만 이 방식은 한계가 명확하다. 우선 데이터를 인메모리 조인하면 당연히 DB 조인보다 속도가 느리기 때문에 너무 많은 양의 데이터의 경우 응답 속도가 느려질 수 있다. 또 각 서비스에서 조인의 대상이 되는 데이터만 가져와야 하는데, 조건문에 해당하는 필드가 특정 서비스의 데이터에는 존재하지 않을 수가 있다. 그렇다고 모든 데이터를 가져와서 조인할 수도 없을 것이다. 조인을 위한 또 다른 방법은 CQRS 를 사용하여 별도의 쿼리 용 뷰(Materialized View)를 만드는 것이다. 데이터 조회를 위한 뷰 서비스를 만들고 도메인 이벤트를 구독하여 필요한 데이터의 clone 을 하나의 DB 에 저장하는 것이다. 이러면 조인에 필요한 데이터가 하나의 DB 에 저장되어 있으니 조인 쿼리가 가능하다. 각 데이터의 clone 을 최신 상태로 유지하기 위해 원본 데이터가 변경될 때마다 각 마이크로서비스에서 이를 알리기위해 이벤트를 발행해야하고, 뷰 서비스에서는 이를 구독하여 적절히 최신화 해주어야 한다. 이 때도 데이터의 변경과 이벤트의 발행이 atomic 해야하므로 아웃박스 패턴을 사용할 수가 있다. 하지만 이렇게 애플리케이션 레벨에서 이벤트를 발행하는 경우에는 코드 상의 버그로 인해, 또는 DML 등을 통해 DB 에 직접 변경을 가하는 경우에는 이벤트가 발행되지 않아 원본과 복제본의 sync 가 깨질 수 있다. 이때는 CDC 를 통해 인프라 레벨에서 이벤트 발행을 보장해주는 것이 해결책이 될 수 있다.

테스트

앞서도 말했지만, 모놀리식 애플리케이션에서는 도메인 서비스간의 데이터 상호작용은 단순 메서드 콜이었다. 하지만 도메인이 서로 다른 서비스로 분리되면서 IPC 를 통해 상호작용해야하게 된다. 서비스간의 통신은 REST 나 gRPC 같은 동기 방식일 수도 있고, 카프카를 통한 비동기 메시징 방식일 수도 있다. 어떤 방식이 되었건 같은 범위에 대한 테스트를 작성하더라도 모놀리식에서는 인프라가 개입되지 않았기 때문에 단순히 일부 객체의 mock 만 떠서 주입시켜 주면 되는 경우에도 MSA 에서는 네트워크 통신이 개입되기 때문에 mock server 를 띄우거나 Spring Cloud Contract 와 같은 별도의 라이브러리가 필요할 수도 있다. 만약 각 서비스를 서로 다른 팀이 개발/운영하는 경우 각 API 에 대한 테스트가 Consumer-Driven Contract Testing 과 같은 좀 더 복잡한 과정을 수반해야할 수도 있다.

운영

모놀리식 애플리케이션의 단점 중에 개발 및 운영이 어렵다는 것이 있었는데 MSA 도 다른 이유로 개발 및 운영 비용이 올라간다. 우선 아무래도 관리의 대상이 되는 컴포넌트의 수가 많아지기 때문이다. 하나의 서비스가 여러개의 작은 서비스로 나누어지고, 위에서 말한 쿼리용 서비스를 포함하여 추가 컴포넌트가 필요할 수도 있다. 클라이언트가 여러 종류의 데이터를 필요로 하는 경우를 생각해보자. 모놀리식 애플리케이션의 경우 이를 위한 API 를 만들면 클라이언트는 한 번의 요청으로 필요한 데이터를 모두 가져올 수 있다. MSA 는 필요한 데이터가 서로 다른 서비스에 의해 관리되고 있으니 각 서비스에 직접 데이터를 요청하기 위해서는 여러번 요청해야할 것이다. 하지만 이렇게 했을 경우에는 서버 API 스펙이 클라이언트 코드에 존재하여 유지보수 측면에도 좋지 않고, 성능 면에서도 좋지 않을 것이다. 이를 해결하기 위해서는 이를 모아주는 API 가 필요하다. 기존 서비스 중 하나에 이 API 를 구현할 수도 있지만 관심사의 분리 관점에서 좋지 않으니 별도의 컴포넌트에 API 를 구현하여 이를 수행하도록 하는 것이 좋을 것이다. API Gateway 가 이 역할을 수행할 수도 있다.
로그 파일이 분산된다는 단점도 있다. 물론 로깅 라이브러리나 Elastic Stack 등을 활용하여 로그 수집 파이프라인을 구축하면 해결할 수 있다.
트러블 슈팅과 모니터링이 상대적으로 어렵다는 단점도 있다. Istio[4] 나 Linkerd 같은 서비스 메쉬를 사용하면 어느정도 커버할 수 있다.

보안

도메인 서비스 간의 상호작용 시 모놀리식에서는 하나의 프로세스 내에서의 메서드 콜이었지만 MSA 에서는 네트워크 통신이기 때문에 암호화에 더 신경을 써야한다. Istio 를 활용하여 기존의 코드를 변경하지 않고도 mTLS 를 통해 비교적 쉽게 암호화를 적용할 수 있다.

결론

위와 같은 부분을 몸소 경험해보면 MSA 의 여러 장점에도 불구하고 MSA 을 도입하는 것이 최선인지 더 고민을 하게 된다. 개인적으로는 만약 팀의 규모가 작거나 시스템의 규모가 작다면 처음부터 MSA 로 시스템을 개발하기 보다, 우선 모놀리식으로 서비스를 개발하고 나서 추후에 필요성이 느껴질 때 MSA 구조로 바꾸는 것이 좋지 않을까 하는 생각이다. 어떻게 보자면 모놀리식 애플리케이션을 개발하고 운영하는 비용이 MSA 로 개발하고 운영하는 비용보다 높아질 때가 MSA 로 전환해야하는 시점일 것이다. 비용을 정량화하기는 어렵겠지만 가능한 이 시점을 잘 파악하여 마이그레이션 하는 것이 좋을것이다. 또한, 초기에 모놀리식으로 개발을 할 때 추후 별도의 서비스로 떼어내기 쉽도록 도메인 주도 설계(DDD)를 하고 모듈을 잘 분리해두면 좋다. 예를 들어 만약 여러 엔티티 클래스들이 서로를 레퍼런스 참조하도록 구현했다면 추후 각 서비스를 추출하는 것이 굉장히 어려운 작업이 될 것이다. 당연한 얘기지만 다른 JVM 에 있는 객체를 참조할 수는 없기 때문이다. 따라서 MSA 전환을 고려한다면 애그리거트 루트끼리의 참조는 PK를 통해 간접 참조하도록 설계하는 것이 좋다.
모놀리스를 여러개의 마이크로 서비스로 분해할 때 처음부터 다시 개발하거나 한 번에 마이그레이션 하는 것은 굉장히 시간이 오래 걸리며 비즈니스 상황에서는 비현실적인 작업이다. 리팩토링보다는 당장 새로운 기능을 개발하거나 기존의 기능을 수정하는 작업이 가장 중요할 것이기 때문이다. 그렇기 때문에 스트랭글러 패턴(Strangler Pattern)[5] 를 활용하거나 앞단에 별도의 routing layer 를 두는 식으로 새로운 비즈니스 요구사항을 처리하면서 동시에 한 서비스씩, 혹은 한 API 씩 점진적으로 분리하는 것이 좋다.


  1. 2PC 는 2 Phase Commit 의 약자로 간단하게 말하자면 분산 트랜잭션을 관장하는 코디네이터가 분산 트랜잭션에 참여하는 컴포넌트에게 로컬 트랜잭션 커밋을 요청하는 1단계와 실제로 로컬 트랜잭션을 커밋하는 2단계로 이루어진 방식이다. ↩︎

  2. 2PC 는 분산 트랜잭션에 참여하는 모든 컴포넌트가 동시에 구동 중이어야 트랜잭션이 실행될 수 있으므로 2PC 의 가용성은 각 컴포넌트의 가용성의 곱이 된다. 반면 사가의 경우 만약 어떤 트랜잭션 참여자가 다운이 되더라도 나중에 재구동되고 난 후에 멈춰진 부분부터 시작할 수 있으니 꼭 모든 컴포넌트가 동시에 구동 중이지 않더라도 트랜잭션이 진행될 수 있으므로 상대적으로 높은 가용성을 가질 수 있는 것이다. 보통 사가에 참여하는 컴포넌트는 메시지 브로커를 통해 통신한다. ↩︎

  3. : https://discuss.axoniq.io/t/the-outbox-pattern/2031 ↩︎

  4. Istio 는 각 파드에 Envoy 사이드카 프록시를 주입하여 기존의 코드를 (거의)전혀 변경하지 않고도 Circuit Breaking, Retry, Telemetry 같은 다양한 기능을 쉽게 사용할 수 있게 한다. 그리고 Jaeger, Kiali, Prometheus, Grafana 애드온을 통해 비교적 쉽게 분산 트레이싱, 트래픽 관리, 시스템 모니터링 등을 할 수 있게 하여 마이크로서비스 간의 통신을 high-level view 에서 관장하는데 큰 도움이 된다. ↩︎

  5. 스트랭글러 덩굴은 열대 우림 지대에서 흔히 자라는 식물로, 숲 꼭대기 너머로 햇볕을 쬐기 위해 나무 주위를 칭칭 감고 자란다. 언젠가 나무 전체를 덩굴로 뒤덮어 나무가 수명이 다 되어 죽으면 나무 모양의 덩굴만 덩그러니 남게 된다. 스트랭글러 패턴은 MSA 에서 스트랭글러 애플리케이션이 기존 모놀리스의 역할을 점점 작게 만들어 결국에는 소멸시키는 모양을 이 스트랭글러 덩굴에 비유한 것이다. 모놀리스에서 한 서비스씩 분리해 나갈 때 이 분리된 서비스들을 통틀어 스트랭글러 애플리케이션이라고 부르는데, 이 스트랭글러 애플리케이션이 점점 커지다 보면 어느새 원래의 모놀리스는 점점 작아져 사라져 버리거나 하나의 마이크로 서비스가 된다. ↩︎

Comments