FeGwan-Training / FeGwan

0 stars 0 forks source link

Chapter 1 마이크로서비스 아키텍처 #35

Closed MyeoungDev closed 1 year ago

MyeoungDev commented 1 year ago

Discussed in https://github.com/FeGwan-Training/FeGwan/discussions/34

Originally posted by **MyeoungDev** July 18, 2023 # 📗 스프링 부트로 개발하는 MSA 컴포넌트 1장 마이크로서비스 아키텍 # 1.0.0 마이크로 서비스 아키텍처 모든 시스템은 하나 이상의 컴포넌트로 구성되어 있다. 컴포넌트와 컴포넌트의 관계를 정리하는 것을 소프트웨어 아키텍처(Architecture) 라고 한다. **서비스 기능(Feature)을 하나의 API 컴포넌트에서 처리하는 구조**를 **모놀리식 시스템(Monolithic System) 아키텍처**라고 하고, **기능을 분리하여 두 개 이상의 API 컴포넌트에서 처리하는 구조**를 **분산 처리 시스템(Distributed Processing System) 아키텍처**라고 한다. 마이크로서비스 아키텍처는 분산 시스템 아키텍처 중 하나다. 특히 마이크로서비스 아키텍처는 다량의 요청을 처리하고 애플리케이션 복잡도를 낮춰 주는 여러 장점이 있다. 그러나 구현이 어렵고, 여러 시스템이 유기적으로 얽혀 있어 운영이 쉽지 않은 복잡한 아키텍처다. # 1.1 모놀리식 아키텍처 소개 **모놀리식 아키텍처(Monolithic Architecture)는 하나의 시스템이 서비스 전체 기능을 처리하도록 설계한 것이다.** ## 1.1.1 모놀리식 아키텍처 장점 모놀리식 아키텍처로 애플리케이션을 설계할 때는 하나의 WAS(Web Application Server)에서 모든 기능을 처리하도록 구성한다. 그리고 데이터를 저장하기 위해 하나의 데이터 저장소를 사용한다. 그러므로 전박적으로 구조가 매우 간단하다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/067c6894-5004-4b94-8fe2-e09d5d300051) - **간단한 구조 덕분에 시스템 운영과 개발이 편리한 장점이 있다.** - 개발자는 하나의 코드베이스에서 클래스 단위로 기능을 개발하면 된다. - 서비스의 기능들은 클래스들의 유기적인 조합으로 이루어진다. - **모놀리식 아키텍처는 네트워크로 인한 지연이나 데이터 유실은 걱정할 필요가 없다.** - 모놀리식 아키텍처의 데이터는 객체들 사이에서만 전달하면 된다. - 마이크로서비스에서 데이터는 네트워크를 통해 전송된다. - **시스템 장애나 버그의 원인을 쉽게 파악할 수 있다.** - 여러 클래스에서 발생하는 로그를 하나의 파일에 기록할 수 있다. - 개발자는 하나의 애플리케이션에서 원인을 파악하면 된다. - **데이터 저장소가 하나이므로 RDB의 트랜잭션 기능을 쉽게 사용할 수 있다.** - **테스트 환경을 쉽게 구성할 수 있다.** - **소규모 개발 팀이 비교적 간단하고 작은 기능을 제공하는 서비스를 개발하기에 효율적이다.** - **빠른 시간 안에 개발할 수 있고, 운영과 유지 보수가 편하다.** ## 1.1.2 모놀리식 아키텍처 단점 **하나의 애플리케이션 서버에서 여러 기능을 제공하므로 서비스 기능이 많아지면 더욱 복잡해지고 스파게티 코드가 되기 쉽다** - **하나의 코드베이스에 클라이언트 코드와 서버 코드를 포함해야 한다.** - 모놀리식 아키텍처는 하나의 애플리케이션에서 HTML, JS 등 같은 정적 파일 뿐만 아니라 서비스 기능까지 웹 서비스에 필요한 모든 기능을 제공한다. - 따라서 서버 기능과 클라이언트 기능이 뒤섞인 채 개발할 수밖에 없다. - **클라이언트 코드의 버그를 수정하여도 서버를 다시 실행해야 한다.** - 하나의 애플리케이션에 여러 기능이 서로 얽혀있기 때문에 이러한 문제가 생길 수 밖에 없다. - **코드 관리와 추가 기능에 많은 충돌이 발생한다.** - 서비스 규모가 커지고 제공하는 기능도 많아지만 개발자가 늘고 GIT 브랜치도 많아지게 된다. - 이를 관리할 때 서로 충돌하는 코드를 수정하느라 고생하게 된다. - **비즈니스 영역의 확장이 어렵다.** - 모놀리식 아키텍처로 만들어진 웹 어플리케이션의 경우 비즈니스 영역이 확장되어 iOS와 안드로이드 사용자에게 서비스를 제공하려한다. - 이런 경우 추가적으로 REST-API 기능을 제공해야 한다. - 결과적으로 코드베이스에 더 많은 코드가 추가된다. - 이런 비기능적 요구 사항 때문에 소스 코드와 복잡도는 증가하고, 빌드 시간이 길어지고 복잡하게 얽힌 코드를 수정하는 일도 힘들어지게 된다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/97b201cc-27e9-4b28-8681-671d008a458f) - **모놀리식 아키텍처로 설계된 애플리케이션을 스케일아웃하는 것은 비효율적일 수 있다.** - 비지니스가 성공하여 사용자의 요청이 늘어나 시스템의 아정성과 고가용성을 확보하며 이를 처리하기 위해 서버를 스케일 아웃(Scale-Out)(수평적 확장)하였다. - 로드 밸런서를 통해 모든 1차 요청을 받고 재분배 한다. 이를 **분산 부하**라고 한다. - 하지만 로드밸런서도 한계가 존재하며, 사용자의 요청이 상품의 검색에 많은것인지, 상품의 주문이 많은지를 파악하여 적절한 서버를 확장하는 것이 올바른 것이다. - **결론적으로 확장에 한계가 있으며 비효율적이고, 기능이 많아질수록 개발 속도나 생산성이 낮아지므로 서비스 고도화에 한계가 생긴다.** 아래 사항들 중 **3개 이상의 상황**에 처해 있다면 **마이크로 서비스 아키텍처 전환을 고려**하자. - [ ] 클라이언트 요청이 점점 많아지는데 로드 밸런서로 확장해도 한계가 있을 때 - [ ] 데이터베이스 성능을 높여도 더 이상 성능 개선의 여지가 없을 때 - [ ] 기능 확장 요구가 많지만 현재 시스템 구조로 불가능할 때 - [ ] 소스 코드가 너무 복잡해서 리팩터링이 필요할 때 - [ ] 기능 중 하나라도 변경되면 전체 QA를 해야 할 때 - [ ] 기능을 수정하면 다른 기능에 연쇄적으로 버그가 발생할 때 - [ ] 개발자는 늘었는데, 개발 속도는 이전과 같지 않을 때 # 1.2 마이크로서비스 아키텍처 소개 **마이크로서비스 아키텍처(Microservice Architecture)는 기능 위주로 나늰 여러 애플리케이션이 있고, 각각 독립된 데이터 저장소를 사용한다.** 기능별로 쪼개진 작은 서비스 혹은 시스템을 마이크로서비스(Microservice)라고 한다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/6d5fa54d-6d51-4e96-acc5-f032ef589e8b) - 마이크로서비스 아키텍처의 특징은 **대규모 시스템**, **분산 처리 시스템, 컴포넌트들의 집합, 시스템 확장** 등이 있다. - 서비스 지향 아키텍처(Service Oriented Architecture, SOA) 와 공통점이 많다. - 서비스 지향 아키텍처는 대규모 시스템을 설계할 때, 서비스 기능 단위로 시스템을 묶어 시스템 기능을 구현한 것을 의미한다. - 두 아키텍처 모두 서비스 기능 단위로 시스템을 만들고, 각 시스템은 서로 표준화된 인터페이스를 통해 서비스를 통합한다. - 두 아키텍처 모두 엔터프라이즈 시스템이라는 대규모 시스템을 구축하기 위한 것이다. - **기능이 복잡하고 처리량이 많은 시스템에 적합하며, 기능이 복잡한 시스템을 편리하게 개발하고 운영하기 위해 소프트웨어 기능을 서비스 단위로 분류하고, 컴포넌트들은 네트워크를 통해 데이터를 통합한다.** - 마이크로서비스 아키텍처의 **마이크로서비스들은 각각 다른 마이크로서비스에서 독립적으로 구성되어야 한다.** - 서로 의존하면 복잡도는 증가하고, 개발 속도와 운영에 문제가 생기며 하나에 장애가 발생하면 연쇄적으로 장애가 발생한다. - 즉, 의존성을 최소화 해야 한다. 이를 **느슨한 결합(Loosely Coupled)** 이라 한다. - **마이크로서비스마다 각각 독립된 데이터 저장소가 필요하다.** (클라우드 서비스나 오브젝트 스토리지 같은 저장소는 제외) - 데이터 저장소를 공유한다면, 데이터 저장소가 단일 장애 지점이 될 수 있다. - 하나의 마이크로서비스가 공유 데이터 저장소의 리소를 전부 차지하는 상황이 생긴다면, 장애는 연쇄적으로 전파되어 큰 문제를 일으킬 수 있다. - 마이크로서비스들은 **기능과 성격에 맞게 잘 분리(Fine-Graind)되어야 한다.** - 기능이 너무 작거나 너무 크게 설계되어서는 안된다. - 기능이 너무 작으면 수많은 마이크로서비스를 관리해야 한다. - 너무 크게 설계하면 하나의 마이크로서비스에 많은 기능이 집중된다. 모놀리식 아키텍처와 다른점이 없다. - 특정 마이크로서비스에 기능을 집중해서 설계한다면 서비스 전체의 기능이 하나의 마이크로서비스에 의존하게 된다. - **통신에 사용되는 네트워크 프로토콜은 가벼워야 한다.** - 각 마이크로서비스 컴포넌트들은 기능을 연동할 때, API를 통해 서로 데이터를 주고받는다. - 즉, 네트워크 프로토콜이 성능 저하의 원인이 될 수 있다. - 직렬화(Serialize)(객체 → 바이트 코드), 역직렬화(Deserialize)(바이트 코드 → 객체) 상황에서 데이터 크기에 따른 성능 저하, 많은 리소스 사용이 발생한다. - 따라서, 시스템의 전반적인 성능 최적화를 하기 위해 가벼운 프로토콜을 사용해야 한다. - 보통 JSON 사용. 이외에 gRPC(Google Remote Procedure Call), AMQP(비동기 처리, 메시징 큐 시스템 기반 데이터 교환에 사용, 대표적으로 RabbimMQ, Kafka) ## 1.2.1 마이크로서비스 아키텍처 장점 - **독립성** - 하나의 마이크로서비스는 하나의 비즈니스 기능을 담당하므로 다른 마이크로서비스와 간섭이 최소화된다. - 하나의 마이크로서비스는 독립된 데이터 저장소를 갖고 있으므로 데이터 간섭에도 자유롭다. - **대용량 데이터를 저장하고 처리하는데 비교적 자유롭다.** - RDB는 스케일아웃(선형 확장)이 쉽지 않다. 그래서 데이터 샤딩을 통해 데이터를 분산해서 저장하기도 한다. - 하지만, 데이터가 분산 저장되면 RDB를 운영하기 쉽지 않다. - 확장성이나 성능에서 뛰어나다고 하는 NoSQL도 무한의 선형적 성능 향상을 기대하기는 어렵다. - 마이크로서비스 숫자만큼 독립된 데이터 저장소가 존재하기 때문에 마이크로서비스는 대용량 데이터를 처리하는데 이점이 존재한다. - **시스템 장애에 견고하다.** - 마이크로서비스는 서로 느슨하게 결합되어 있고, 각각 독립되어 있기 때문에 서로 간에 미치는 영향이 적다. - 하나의 서비스에 장애나 버그가 발생하더라도 다른 마이크로서비스는 이상 없이 서비스 된다. - 또한, 하나의 서비스에서 치명적인 버그가 발생했다면 해당 마이크로서비스만 격리하면 된다. - **마이크로서비스와 클라우드 서비스에서 제공하는 몇몇 기능이 결합되면 애플리케이션은 탄력 회복성(resilience)을 갖게 된다.** - 탄력 회복성이란 애플리케이션 서버에 장애가 발생하면 새로운 컴퓨팅 자원을 추가해서 빠른 시간 안에 서비스를 다시 제공하는 것을 의미한다. - 예를 들어 AWS 클라우드를 이용하여 인스턴스 서버의 CPU 사용량이 70% 이상 사용하는 상황에 마지막으로 배포된 JAR 파일을 이용하여 인스턴스를 새로 추가하고, 로드 밸런서를 이용하여 분산처리를 하여 서버의 가용률을 늘릴 수 있다. - **서비스 배포 주기가 빠르다.** - 모놀리식 아키텍처는 하나의 코드베이스에서 작업하기 때문에 배포 일정을 정하고 그 기간 동안 개발된 모든 기능을 한 번에 배포한다. - 마이크로서비스는 모든 기능이 분리되어 있으므로 필요한 기능만 먼저 배포할 수 있다. 심지어 하루에 수십번도 가능하다. - 서비스 일부분만 배포하는 것 또한 견고한 서비스 운영을 가능하게 한다. - 이런 배포는 CI/CD 시스템을 추가적으로 구축해야만 빠른 배포가 가능해진다. > CI(Continuous Integration) / CD(Continuous Deploy) > > > ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/1ec68675-8c69-4e77-92d8-a009ac509f47) > > CI(Continuous Integration) 은 지속적인 통합을 의미한다. > > 즉, 개발자가 개발한 소스 코드들은 지속적으로 코드베이스에 통합되어야 하며, 이때 자동으로 빌드 및 테스트가 진행되어야 한다. > > 배포 빌드 테스트 과정을 지속적이고 자동으로 계속할 수 있도록 구축된 시스템이 CI다. > > 대표적인 오픈 소스 CI 시스템으로는 젠킨스(Jenkins)가 있다. > > CD(Continous Deploy)는 지속적인 배포를 의미한다. > > CI를 통해 자동으로 테스트 및 패키징되었다면 CD를 이용하여 자동으로 시스템에 배포할 수 있다. > > CD가 없다면 개발자는 패키징된 파일을 각 서버에 분배한 후 직접 서버를 재기동해야 한다. > > 분배 작업은 시간이 많이 소요될 뿐만 아니라, 다른 작업도 필요하다. > 이런 부분들을 모두 자동화하면 배포 시간이 단축될 뿐만 아니라 하루에도 수십 번 배포할 수 있다. > - **마이크로서비스 단위로 확장할 수 있어 서비스 전체적으로 확장성이 좋아진다.** - **사용자 반응에 민첩하게 대응할 수 있다.** - 사용자 반응에 따라 시스템을 고도화하거나 빠르게 시스템에서 제외할 수 있다. ## 1.2.2 마이크로서비스 아키텍처 단점 - **개발하기 어려운 아키텍처다.** - 가장 큰 원인은 **다른 시스템이 네트워크상에 분산되어 있기 때문이다.** - 크게 고려할 것들은 분리된 데이터, 네트워크를 통한 데이터 통합이다. - 분리된 데이터 - RDB의 최대 장점인 DB 트랜잭션을 사용할 수 없다. (분산 트랜잭션은 시스템 전체의 리소스를 많이 사용하므로 권장하지 않는다.) - 따라서 데이터 정합성이 맞지 않거나 중복된 데이터가 발생할 수 있다. - 네트워크를 통한 데이터 통합. - 데이터를 통합하기 위해서는 표준화된 인터페이스로 데이터를 처리해야 한다. - **네트워크는 신뢰할 수 없고, 커넥션을 맺는 비용이 비싸다.** - 네트워크는 언제든지 장애가 발생할 수 있고, 패킷은 언제든 누락될 수 있다. - 요청한 응답을 받지 못하는 상황이 올 수 있고, 네트워크 지연이 발생할 수 있다. - 따라서, 개발자는 커넥션 풀을 설정할 때 반드시 커넥션을 맺는 타임아웃(Connection Timeout)과 상대방 데이터를 기다리는 타임아웃(Read Timeout)을 고려해야 한다. - 네트워크를 사용하는 데이터 통합은 전반적인 시스템 성능 하락을 가져오고, 데이터를 직렬화/역직렬화 하는데 비용또한 발생한다. - **운영하기 매우 어려운 아키텍처다.** - 우리가 사용하는 하나의 프로세스에도 많은 과정과 상황이 있고, 현업은 우리가 상상하는 것보다 더 복잡한 상황이 발생할 수 있다. - 이 복잡한 과정의 모든 요청이 이상 없이 처리되어야 사용자의 요청이 성공적으로 처리가 된다. - 즉, 수많은 마이크로서비스가 통합되어야 한다. - **마이크로서비스 아키텍처로 설계된 서비스 운영에는 여러 가지 자동화된 시스템이 필요하다.** - 마이크로서비스 아키텍처를 도입하는 이유는 빠른 서비스 개발과 운영, 대규모 서비스를 처리하기 위해서다. - 인스턴스를 빠르게 배포하기 위해 CI/CD 시스템이 필요하고, 수많은 인스턴스를 모니터링하기 위해 모니터링 시스템이 필요하고, 각 인스턴스에서 발생하는 로그를 통합하고 관리해주는 시스템 또한 필요하다. - 이외 자동화된 여러 시스템을 효과적으로 요구한 곳에 사용하는 것도 필요하고 자동화된 시스템들을 운영하고 유지 보수하는 것도 쉬운 일이 아니다. - **마이크로서비스를 운영하고 개발하는 개발자의 기술력이 좋아야 한다.** - 하나의 마이크로서비스 개발 속도는 개발자 숫자와 비례해서 선형적으로 증가하지 않는다. - 오히려 사람이 많을 수록 커뮤니케이션에 쏟는 시간과 노력은 기하급수적으로 증가한다. - 따라서 하나의 마이크로서비스를 개발하는 팀은 적은 인원을 유지하도록 한다. - 인원이 적은 팀이 하나의 시스템 운영부터 개발까지 수많은 일을 처리해야 되기 때문에 각각의 개발자의 기술력이 뛰어나야 한다. - 오히려 각 팀원의 기술 성숙도가 낮은 팀은 마이크로서비스를 하기에 부적합하고, 개발 속도와 운영, 안정성에서 역효과가 발생할 수 있다. # 1.3 마이크로서비스 아키텍처 설계 **잘 분리된 마이크로서비스는 각 개념이 서로 겹치지 않고 독립적으로 서비스되는 것을 의미한다.** 결국 마이크로서비스 아키텍처 **설계의 가장 기본은 마이크로서비스로 잘 분리하는 것이다.** 마이크로서비스 아키텍처를 설계할 때 **정해진 법칙은 존재하지 않고**, **서비스 성격에 따라 각각 다르게 설계되어야 한다.** 또한, **설계가 끝났다고 해서 해당 설계가 영원하지도 않다.** 서비스가 추가되거나, 비즈니스 상황이 바뀜에 따라 설계도 언제든지 변경될 수 있다. ## 1.3.1 서비스 세분화 원칙 **서비스 세분화 원칙(Service granularity Principle)은 서비스 지향 아키텍처의 여러 핵심 원칙 중 하나**로, 네 개의 요소로 구성되어 있으며, 이 네 가지 요소를 기반으로 서비스를 나누도록 제안한다. 해당 원칙을 이용하여 서비스를 분리할 경우 **느슨하게 결합된 서비스들은 독립성을 갖게 되고** 다른 서비스를 신경 쓰지 않아도 되므로 **각각 서비스 복잡도는 낮아진다.** ### 비즈니스 기능 - 비즈니스 기능으로 서비스를 나눈다. - 이상적으로 각각의 서비스는 비즈니스 동작과 일대일로 견결되어 있따. - 하나의 서비스가 여러 비즈니스 동작을 제공하면 복잡도가 높아지고 유지 보수가 어려워진다. - 따라서, 마이크로서비스를 비즈니스 기능으로 나누는 것을 고려해 보자. ### 성능 - 마이크로서비스의 성능이 떨어진다면 해당 서비스를 나누는것을 고려할 만하다. - 단, 성능이 떨어지는 것이 애플리케이션의 비효율적인 개발이 원인인지 서비스 크기가 너무 커서 오버헤드가 발생하는 것인지 판단해야 한다. - 성능이 떨어지는 마이크로서비스가 너무 많은 기능을 처리하고 있지는 않은지 파악해서 나누는 것도 고려해 보자. ### 메시지 크기 - API를 설계하는 데 메시지 크기가 크다면 마이크로서비스를 나누는 것을 고려하자. - 메시지 크기가 너무나 크면 메시지를 직렬화/역직렬화하는 데 성능 문제를 일으킨다. - 단, 비즈니스 기능이나 일관성을 유지하는 트랜잭션에 문제가 없다면 이는 무시해도 좋다. ### 트랜잭션 - 데이터 정합성을 유지하는 트랜잭션으로 서비스를 나누는 것도 좋다. - 트랜잭션으로 보장되는 서비스라면 에러가 발생했을 때 데이터 유실이나 정확한 데이터를 유지하는 데 도움이 된다. - 트랜잭션을 유지할 수 있는 데이터들은 하나의 비즈니스 로직을 처리하는 데 하나의 세트로 동작하기 때문에 최소한의 마이크로서비스를 설계하는 데 도움이 된다. ## 1.3.2 도메인 주도 설계(Domain Driven Design)의 바운디드 컨텍스트 도메인 주도 설계는 에릭 에반스가 제안한 개발 방법론으로, 도메인에 집중해서 개발할 수 있는 여러 방법을 설명한다. 여기서 도메인이란? → 도메인은 비즈니스 전체나 조직이 행하는 일을 의미한다. → 도메인은 서브 도메인들로 구분할 수 있는데, 개발자는 어떤 서브 도메인을 포함하고 도메인을 어떻게 소프트웨어로 구현할지 집중해야 한다. 이때 도메인 모델이 존재하는 **다른 도메인 모델과 확연히 구분되는 명시적인 경계**를 **바운디드 컨텍스트**라고 한다. 바운디드 컨텍스트는 다른 도메인 모델과 구분되느므로 매우 독립적인 영역이다. 그러므로 구분된 바운디드 컨텍스트로 마이크로서비스를 설계하면 다른 마이크로서비스와 중복될 확률이 매우 줄어든다. ## 1.3.3 단일 책임 원칙 객체 지향 설계 원칙 중 하나인 **단일 책임 원칙(Single Responsibility Principle)** 또한 마이크로서비스를 설계하는데 도움이 된다. 우리가 **무언가를 변경할 때 함께 변경되는 것들은 연관성이 있음을 의미**한다. 즉, 연관성이 있으면 하나로 모으면 된다. 그리고 서로 다른 이유로 변경되는 것들은 분리하면 된다. 이렇게 설계된 구조는 **응집도(Coherence)는 높아지고 결합도(Coupling)는 낮아**지게 되며 **각 클래스의 책임 영영이 확실해진다.** 이는 마이크로서비스에서도 마찬가지로 적용된다. ## 1.3.4 가벼운 통신 프로토콜 마이크로서비스들은 데이터를 통합하기 위해 네트워크를 사용하여 기능을 제공하는데 이 때문에 여러 문제가 발생한다. 이 부하를 최소한으로 줄이는 설계가 필요하다. 즉, 마이크로서비스 사이에 **네트워크 통신은 가벼**워야 하며, **프로토콜은 특정 기술이나 언어에 의존성이 없어야 한다.** 한번 정해진 프로토콜은 운영 중에 변경하기 어렵다. 따라서 복잡한 프로토콜 대신 **HTTP 기반의 REST-API를 많이 사용**한다. ## 1.3.5 외부 공개 인터페이스 마이크로서비스들은 공개된 인터페이스를 이용하여 네트워크를 통해 서로 통신한다. **운영 중에는 즉각적인 수정이나 변경이 힘들다.** 그래서, **버전 변경 같은 방법을 이용**해 **점진적으로 인터페이스를 변경**해야 하고, **인터페이스 설계할 때는 매우 신중**해야 한다. ## 1.3.6 마이크로서비스마다 독립된 데이터 저장소 마이크로서비스로 분리하였는데 **데이터 저장소를 같이 사용한다면** 마이크로서비스 **독립성에 위배되는 행위**이며, **서비스를 운영할수록 심각한 문제가 된다.** 공통된 데이터베이스를 사용할 경우 하나의 마이크로서비스에서 과도한 조회 등이 발생하면 다른 마이크로서비스의 **장애**로 **전파**되며 이는 **심각한 상황을 초래**할 수 있다. 비용상, 여건상 문제가 발생할 경우 적어도 논리적으로 데이터 저장소를 분리하여 관리해야 한다. # 1.4 스프링 투어의 아키텍처 변화 해당 책에서 설계하며 진행하는 프로젝트를 스프링 투어라고 부른다. 1장에서는 스프링 투어의 아키텍처가 어떤 모습으로 변화하며 설계되는지에 대해서 간략하게 설명한다. ## 1.4.1 스프링 투어의 시작 빠른 구현을 위하여 모놀리식 아키텍처로 서비스를 개발하기로 결정하였다. ## 1.4.2 서비스 안정성 확보 서비스가 성공하여 사용자가 늘어남에 따라 장애가 발생하며 하나의 서버로 운영하기 어렵게 되어 서비스 안전성을 위해 고가용성을 선택하여 아키텍처를 변경하기로 하였다. **고가용성(High Availability)은 지속적으로 서비스를 제공할 수 있는 성질을 의미한다.** **하나의 애플리케이션 서버는 고가용성 모델이 아니다.** 서버를 배포 도중 서비스를 제공할 수 없고, 시스템을 구성하는 컴포넌트 중 한곳에 장애기 발생하면 장애가 발생한 만큼 서비스를 제공할 수 없다.(이를 단일 장애 지점 Single Point Of Failure, SPOF 라고 한다.) 그래서 이중화 혹은 다중화를 통해 고가용성을 확보하는 것이 중요하다. **고가용성을 확보하는 일반적인 방법으로는 로드 밸런서를 사용한다.** 로드밸런서가 **가상 서버 역할**을 하고 **사용자 요청을 모두 받는다**. **로드 밸런서에 연결된 애플리케이션 서버에 알고리즘에 따라 사용자 요청을 분산한다.** **요청량 변화에 따라 로드 밸런서에 연결된 애플리케이션 서버를 늘리는 것을 스케일아웃(scale-out)이라고 한다.** 데이터베이스도 고가용성을 확보할 수 있는 방법이 있다. **MySQL에서 가장 많이 사용하는 방법은 MMM(Multi-Master Replication Manager for MySQL)이다.** **여러 MySQL 인스턴스를 두고 데이터를 복제하는 방식이다.** **원본 데이터베이스를(master)** 라고 하고 **복제 데이터베이스들을 슬레이브(slave)**라고 한다. MMM 클러스터에는 **한 개의 마스터와 여러 개의 슬레이브**가 있다. 애플리케이션은 **마스터 데이터베이스에 데이터를 변경**하고, 변경된 데이터들은 클러스터 내 **여러 슬레이브로 전달**되어, 결국에는 MMM 클러스터의 **데이터베이스는 모두 같은 데이터를 저장**하게 된다. 이러한 과정을 **리플리케이션(Replication)**이라고 한다. **마스터에 장애가 발생**하면 **슬레이브 중 하나를 마스터 데이터베이스로 역할을 변경**하고, **슬레이브 데이터 베이스에 장애가 발생**하면 **마스터 데이터베이스**에서 쿼리를 **실행**한다. 이를 **롤 체인지(Role Change)**라고 한다. 이러한 방식으로 데이터베이스의 고가용성을 유지할 수 있다. ## 1.4.3 확장의 시작 **시스템 확장 시기를 결정하기 위해서는 반드시 모니터링 시스템이 필요하다.** 애플리케이션의 스레드 상태나 애플리케이션 성능을 모니터링 하기 위해선 스카우터 같은 APM(Application Performance Monitoring) 시스템을 모니터링 시스템과 함께 사용하길 제안한다. 혹은 클라우드 서비스에서 제공하는 모니터링 서비스를 사용하는 것도 추천한다. 서비스를 모놀리식에서 기능단위로 확장하기로 결정하였다면, **웹 서버와 웹 어플리케이션 서버로 분리해야 한다.** 즉, 프론트 애플리케이션과 백엔드 애플리케이션으로 분리해야 한다. 이를 통해 프론트 애플리케이션과 백엔드 애플리케이션을 **독자적으로 운영 개발 할 수 있고, 버그가 발견되어도 각자 수정하고 배포하면 되는 환경이 구축된다.** 이런 아키텍처에서 REST-API를 클라이언트 사이드로 노출하려면 웹 서버의 리버스 프록시(reverser proxy) 기능을 사용해야 한다. 또한, HTTPS 처리는 웹 서버가 담당하도록 변경해야 하고, 백엔드 애플리케이션은 이에 따른 인증, 인가 기능 등을 더욱 강화해야 한다. > 리버스 프록시란? 러비스 프록시는 Apache, Nginx 등과 같은 웹 서버에 설정하는 것으로, 웹 서버에 리버스 프록시를 설정할 경우 웹 애플리케이션의 성능, 보안, 가용성 등을 향상 시킬 수 있다. 리버스 프로시 동작 방식 > > 1. 클라이언트로부터 요청이 들어오면, 리버스 프록시는 해당 요청을 받는다. > 2. 리버스 프록시는 요청을 받은 후, 해당 요청을 실제 웹 서버(뒷단 서버)로 전달한다. > 3. 웹 서버는 요청을 처리하고, 결과를 리버스 프록시에게 반환한다. > 4. 리버스 프록시는 웹 서버의 응답을 클라이언트에게 전달한다. > > 리버스 프록시 방식을 이용하여 **로드 밸런서를 통해** **서버의 요청을 여러 서버에 분산 시킴**으로써 **웹 서버의 부하를 줄이고**, **높은 가용성과 빠른 응답시간을 제공할 수 있고**, **백 애플리케이션 서버에 직접적인 요청을 막음으로써 보안을 강화할 수 있다.** > ## 1.4.4 데이터 저장소의 확장 MySQL 같은 관계형 데이터베이스는 스케일아웃이 매우 어려운 데이터 저장소다. 데이터베이스 인스턴스가 수평적으로 확장되어 **데이터가 여러 인스턴스에 저장되면 트랜잭션 기능을 사용하기 어렵다.** (분산 트랜잭션을 사용하면 트랜잭션 관리가 가능하지만, 리소스를 많이 사용하며 성능이 떨어진다는 단점이 존재한다.) 데이터베이스를 확장하는 **가장 쉬운 방법은 스케일업(scale-up)**이다. 하지만 **수직 확장은 한계가 명확하며 성능 또한 선형적으로 증가하지 않는다.** 이를 극복하기 위한 튜닝 방벙 중 샤딩(sharding)이라는 기법이 존재한다. **샤딩은 데이터를 샤드라는 단위로 여러 데이터베이스 인스턴스에 나누어서 관리한다.** **데이터는 샤드의 숫자만큼 분류하여 저장된다.** 이때, **가장 중요한 분류 설정에 따라 데이터가 적절히 분배되어야 균등하게 샤드를 운영할 수 있다.** 예를들어, 기존에는 하나의 데이터베이스에 100개의 데이터를 저장했다면, 다섯 개의 샤드로 구성된 데이터베이스에는 하나의 데이터베이스에 20개의 데이터만 저장하고 관리한다. 샤드에 데이터를 분배하는 여러 방법 중에 이 책은 로케이션 서비스(Location Service)를 이용항 샤딩 방법을 설명한다. **로케이션 서비스 방식은 데이터 위치를 중앙에서 관리한다.** - 애플리케이션 서버가 ‘Benjamin’ 사용자 정보를 질의하기 위한 과정 1. 애플리케이션 서버는 ‘Benjamin’ 정보를 셀렉(select)하기 위해 로케이션 서비스에 질의한다. 2. 로케이션 서비스가 ‘Benjamin’ 정보는 샤드 #1에 있다고 응답한다. 3. 애플리케이션 서버가 샤드 #1의 데이터베이스 인스턴스에 질의한다. 이러한 로케이션 서비스를 이용하면 **데이터 리밸런싱(데이터를 재분배 하는 과정)**과 **논리적으로는 균등한 데이터 분배가 가능하다.** 그러나, **모든 요청 트래픽이 로케이션 서비스에 집중되는 경우가 발생**할 수 있고, 이를 통해 **장애**가 발생할 수 있다. 또한, **샤딩 전 데이터베이스 구성보다 통신 횟수가 2배 증가**하게 된다. 따라서, 로케이션 서비스는 **경량화된 네트워크 프로토콜을 사용**해야 하며, 응답이 매우 빨라야 한다. ## 1.4.5 마이크로서비스 아키텍처의 시작 서비스 이용자가 늘어남에 따라 위에 기술한대로 서버를 스케일 아웃하고, 데이터베이스 샤딩을 통해 데이터베이스 또한 스케일 아웃에 성공적으로 진행 하였다. 그러나, 애플리케이션에 대한 복잡도는 해결되지 않았다. 모놀리식 아키텍처 방식으로 구현된 백 애플리케이션 서버는 하나의 코드베이스에 여러 개발자가 동시에 작업함으로써 병합 하는 과정에서 코드가 누락될 수도 있고, 배포 주기또한 영향을 받게 되고, 새로운 기능을 추가하더라도 다른 기능에 영향 미치는 등 좋지 않은 영향을 끼치게 된다. 따라서, 서버의 스케일 아웃에는 성공하였지만 서비스 신뢰도는 높이지 못하였다. 결국, 마이크로서비스 아키텍처 도입을 통해 서비스 신뢰도를 높이고, 개발자들의 개발 생산성을 향상시키도록 마지막으로 아키텍처를 변경했다는 이야기이다. # 1.5 12요소 애플리케이션 **클라우드 컴퓨팅 환경에서 적합한 유연한 애플리케이션을 개발하는 방법론이 바로 12요소 애플리케이션(12Factors App) 이다.** ## 1.5.1 코드베이스: 버전 관리되는 하나의 코드베이스와 다양한 배포 프로젝트에 담긴 코드들을 코드베이스(codebase)라고 한다. 코드베이스에 소스 코드를 수정할 때는 깃(Git)이나 SVN(Subversion) 과 같은 버전 컨트롤 시스템을 사용하여 코드를 관리한다. 코드 버전을 관리하는 툴이므로 개발자들이 코드를 생성하고 수정할 때마다 추적이 가능하다. 이처럼 **개발자는 버전 컨트롤 시스템, 이슈 관리 소프트웨어 등을 이용하여 코드를 관리하여야 한다**. 또한, **어느 Productuon 이나, OS에 종속되지 않는 소스코드를 작성해야 한다.** ## 1.5.2 의존성: 명시적으로 선언할 수 있고 분리할 수 있는 의존성 스프링 부트 애플리케이션을 개발 할 때 기본 라이브러리 외에 다른 라이브러리가 필요하다. 이때 **애플리케이션은 특정 라이브러리에 의존성(dependency)이 있다고 말한다.** 그래서, **여러 라이브러리가 포함되어 있는 프레임워크를 사용하는 애플리케이션은 다른 라이브러리에 의존성이 생기는 것이 당연하다.** 이 **의존성은 반드시 명시적으로(explicitly) 선언되어야 하고 분리(isolate)되어 관리되어야 한다.** 즉, **의존성 관리 도구와 의존성 선언 파일로 라이브러리 의존성을 관리해야 한다.** **(Maven → pom.xml, Gradle → build.gradle)** ## 1.5.3 설정: 환경 변수를 이용한 설정 하나의 코드베이스는 여러 환경에서 동작할 수 있다. 또한 애플리케이션은 수평 확장할 수 있다. 이때 배포된 환경마다 혹은 스케일아웃된 애플리케이션마다 달라지는 값을 코드 내부 혹은 외부에서 주입 받을 수 있다. 코드 내부에서 처리하는 것 보다는 **외부에서 받아서 처리하는 것이 더 유연한 방법이다.** ```java java -Dspring.profiles.actvie=dev -jar ./spring-tour-1.0.0.jar ``` 스프링 프레임워크에서는 애플리케이션의 환경 변수를 사용하기 위한 `o.s.c.env.Environment` 인터페이스와 그 하위 구현 체인 `Properties` 와 `Profiles` 클래스를 제공한다. 이를 통해 유연한 개발 환경을 설정하는 것이 중요하다. ## 1.5.4 지원 서비스: 지원 서비스는 연결된 리소스로 처리 지원 서비스(Backing Service)는 애플리케이션이 네트워크를 이용해서 사용하는 모든 서비스를 의미한다. 지원 서비들은 네트워크로 연결되어 언제든지 애플리케이션과 연결하고 분리할 수 있어 리소스라고 한다. **네트워크를 통해 연결하고 분리할 수 있으므로** **지원 서비스들은 애플리케이션과 느슨하게 결합되어 있다.** 같은 맥락에서 마이크로서비스 아키텍처도 마이크로서비스들을 연결하기 위해 REST-API 나 RabbitMQ, Kafka 같은 메시지 브로커 시스템을 사용한다. 스프링 프레임워크에서는 REST-API를 쉽게 사용하기 위해 `o.s.web.client.RestTemplate` 와 `o.s.web.reactive.function.client.WebClient` 클래스를 제공한다. RabbitMQ는 `o.s.amqp.rabbitmq.core.RabbiTemplate`, Kafka는 `o.s.kafka.core.KafkaTemplate` 클래스를 제공한다. ## 1.5.5 빌드, 릴리스, 실행: 소스 빌드와 실행은 완전히 분리되어야 한다 코드베이스는 빌드, 릴리스, 실행 이 세 단계 과정으로 시스템에 배포된다. 각 단계는 다음과 같이 완전히 분리되어야 한다. - 빌드 단계는 먼저 의존성이 있는 라이브러리들과 코드베이스를 조합하여 컴파일(compile)한다. 메이븐을 사용한다면 ‘컴파일’ 단계를 의미한다. - 릴리스 단계는 스프링 프로파일 혹은 메이븐 프로파일 설정을 이용하여 resources에 위치한 설정 파일들을 조합한다. 이때 스프링 부트에서 제공하는 플러그인은 컴파일된 코드와 설정 파일을 조합하여 실행 가능한 JAR 파일(executable Jar file)을 생성한다. 이는 메이븐의 ‘패키지’ 단계에 해당한다. - 앞서 생성된 실행 가능한 JAR 파일은 java 명령어를 이용해서 애플리케이션을 실행한다 이를 ‘실행’ 단계라고 한다. **빌드된 소스 코드를 고칠 수 없고 실행된 애플리케이션은 빌드된 상태에서 동작한다.** **소스 빌드와 실행이 완전히 분리되면 각 릴리스는 중가에 기능 수정이 있을 수 없다.** **따라서 하나의 릴리스 파일로 실행하는 모든 애플리케이션은 같은 기능을 제공하게 된다.** ## 1.5.6 프로세스: 애플리케이션은 하나 이상의 무상태 프로세스로 실행되어야 한다. **애플리케이션을 구성**하는 프로세스들은 하나 혹은 그 이상의 **프로세스들로 구성**된다. **이 프로세스들은 무상태(Stateless)이며 공유하는 것이 없어야(Shared-Nothing) 한다.** 만약, 두 개의 서버를 로드밸런서를 통해 가동하고 있다고 가정을 하고, 서버 1이 서버 2의 상태를 얻어와 프로세스를 처리한다고 가정하자. 이때, 서버 2의 상태가 좋지 않다면 제대로 된 상태를 얻지 정상적으로 처리되지 못하거나 장애를 이르킬 수 있다. 따라서, **프로세스는 무상태이거나 공유하는 것이 없어야 한다**. **상태를 저장해야 하는 상황이라면 프로세스에 저장하는 것이 아니라 DBMS 같은 지원 서비스에 저장해야 한다.** ## 1.5.7 포드 바인딩: 포트 바인딩을 통한 서비스 공개 **클라우드 환경에서 모든 애플리케이션은 특정 포트가 바인딩되도록 설계한다.** **애플리케이션이 웹 서비스를 제공하더라도 80번 포트가 아닌 8080번과 같은 포트로 바인딩 되어야 한다.** > 80번 포트가 아닌 다른 포트를 사용하는 이유. Nginx나 Apache 웹 서버들이 80번 포트로 사용자 요청을 직접 처리하고 리버스 프록시 설정을 통해 애플리케이션 서버로 요청을 포워딩하는데, 이는 대개 애플리케이션을 외부 공격에서 보호하기 위해서다, > ## 1.5.8 동시성: 프로세스들을 통한 수평 확장 애플리케이션에 스레드가 많아지면 메모리를 많이 사용하고, 컨텍스트 스위칭 때문에 더 큰 사양의 VM이 필요하다. 따라서 클라우드 환경에서는 작은 크기의 애플리케이션을 스케일 아웃하는 것을 추천한다. ## 1.5.9 폐기 가능: 프로세스는 빠르게 시작해야 하고 안정적으로 종료해야 한다 애플리케이션에 문제가 발생해서 **서비스할 수 없는 상황**이나 과부하인 상태에서는 **빠르게 애플리케이션을 스케일아웃해서 원상 복구하는 형태로 설계해야 한다.** ## 1.5.10 Dev 환경과 Production 환경 일치 마이크로서비스를 운영하면 개발 완료된 프로그램이나 기능은 수시로 배포된다. 따라서, Dev 환경과 실제 Production 환경을 가능한 비슷하게 설정하고 유지해야 한다. ## 1.5.11 로그: 로그는 이벤트 스트림으로 다룬다 실행 중인 애플리케이션의 내부 상태를 보는 여러 방법 중에서 로그는 전통적이면서 가장 많이 사용하는 기능이다. **로그는 이벤트가 시간 순서대로 정렬되어 있으므로 스트림이라고 표현한다.** 12요소에서 **로그는 이벤트 스트림으로 처리되는 것을 중요하게 여긴다.** 즉, 터미널에 로그를 출력하든 로그 분석 시스템을 사용하든 **로그는 이벤트 시간 순서대로 정렬되어 관리되어야 한다.** ## 1.5.12 admin 프로세스: 시스템 유지 보수를 위한 일회성 프로세스 서비스 운영 중에 개발자의 실수로 데이터가 잘못 조작된 경우가 발생할 수 있다. 이 경우 시스템을 운영하면서 서비스 중단 없이 데이터를 마이그레이션하기도 한다 이런 **유지 보수 프로그램을 admin 혹은 maintenance 프로세스라고 한다.** 12요소 애플리케이션에서는 **커맨드 라인에서 바로 실행할 수 있는 일회성 프로그램의 필요성을 강조한다.** 스프링 부트는 실행 가능한 JAR파일로 패키징할 수 있다. 이 JAR 파일은 커맨드 라인에서 실행하여 쉽게 콘솔 애플리케이션을 만들 수 있다. Spring Boot는 커맨드 라인의 인자를 받을 수 있는 `o.s.boot.CommandLineRunner` , `o.s.boot.ApplicationRunner` 인터페이스를 제공한다. # 1.6 정리 1. 마이크로서비스 아키텍처 개발은 매우 힘들다. 2. 자율적인 팀 문화가 있어야 각각의 마이크로서비스가 독립성과 자율성을 가지고 발전할 수 있다. 3. 마이크로서비스 아키텍처 전환할 때 반드시 목적이 있어야 한다. 4. 마이크로서비스 아키텍처를 개발하는 개발자의 성숙도가 높아야 한다. 5. 마이크로서비스 아키텍처에서 하나의 마이크로서비스를 개발하는 개발자는 1 ~ 8 명의 인원이 적합하다.