[MSA스터디] 4. 마이크로서비스 시작하기

vector13·2022년 9월 14일
0

1. 작은 일체형 접근법

처음부터 마이크로 서비스 맡아서 여러 팀이 개발하는 것은 어려움. 그래서 많은 사람들이 일체형 부터 개발하는 방법 선호함
처음부터 마이크로서비스로 시작하지 말아야하는 이유는
1. 마이크로서비스는 시스템을 배포, 관리, 테스트 하기가 기술적으로 더 어렵기 때문
2. 시스템 설계가 일체형보다 더 안좋아진다. (각자가 맡은 부분만 관심 두기 때문)

마이크로서비스로 시작해서 동시에 개발한다면 개발, 통합 테스트, 표준 정하기, 명확한 api, 로깅과 모니터링, 에러처리, 커뮤니케이션 등 주의 필요.

일체형 애플리케이션으로 시작하고

  • 일반적인 패턴을 찾아 나중에 공통 라이브러리로 뽑아낼 부분 정의
  • peer review 진행
  • PM과 향후 일체형 분리 시간 정하고, 일체형 분리 전략에 대해 설명하고 리팩터링은 꼭 필요하고 아무 문제 없다는 것에 모두 공감하는 문화 만들기

저자는 최소 첫 릴리스까지 작은 일체형 유지를 권장

사용자 스토리 3

매일 문제 풀기 위한 동기부여가 필요하다! -> 게임화 (gamification)

게임화는 곱셈을 푸는 것과는 아무 상관이 없다. 그러므로 새로운 모델을 정의하고 분리된 스프링부트 애플리케이션을 만들 수 있는 좋은 기회임!

기존 스프링 부트 애플리케이션은 곱셈 마이크로 서비스가 되고 새로운 애플리케이션은 게임화 마이크로 서비스가 된다.

2. 게임화 기초

무엇이 게임화 ? -> 사람 조종 x 플레이어가 무언가를 좋아하도록 어디에나 적용 할 수 있는 것이 아님

게임 아이디어 : '점수' 도입, '리더보드' 로 모두가 볼 수 있도록 점수 공개, 무언가를 달성할 때 '배지' 부여

정답마다 10점씩 부여, 상위점수 사용자 리더보드 페이지에 보임, 동,은,금 배지 + 첫 정답 배지(정답10개, 25개, 50개)

3. 마이크로서비스 아키텍처로 전환하기

독립적 배포 가능하고 기존 비즈니스 로직과 분리된 마이크로서비스
기존의 스프링부트 애플리케이션과 연결해야하고 독립적으로 확장가능해야함

3.1 왜 일체형으로 만들면 안되나

3.1.1 관심사 분리, 결합도 낮춤

로직을 합치면 에러 발생 위험, 관심사 분리의 이점이 사라짐
마이크로서비스로 구현하면 관심사를 분리하고 결합도를 낮추는 방향으로 생각하게 된다. 데이터를 부분적으로 복제하거나 다른 서비스를 필요할 때 호출하는 식 (조인 쿼리나 하나의 클래스에 여러 도메인을 넣는 등의 일체형과 다르게 )

3.1.2 독립적인 수정사항

독립적으로 배포가능한 서비스들로 만들면 각자 api이용해 따로 테스트 가능
게임화 팀은 곱셈 팀의 개발 주기 상관없이 서비스 개선 가능. "서로 영향 주지 않고, 서로 방해하지 않는다."

통신 위해 새로운 인터페이스 필요하면 -> dummy 호출이나 메시지 만들고 각자 개발

3.1.3 확장성

사용자 많아져서 리소스 부족 -> 시스템 확장하고 로드밸런싱 기술 적용하고자 할 때
-- 전체 시스템 하나로 배포시 -> 인스턴스 여러개 만드는 방법 밖에 없다.
-- 서비스 여러 개 있다면 유연하게 선택가능.

4. 마이크로서비스 연결하기

<문제 풀고, 피드백 받는 것에서 문제 풀고, 피드백 받고, 점수 얻는 로직 >
마이크로서비스를 이벤트 중심의 방식으로 개발하지 않는다면

  • 1 두개의 서비스가 db 공유해 게임화 서비스가 데이터 바로 사용
  • 2 게임화 서비스가 곱셈 서비스에서 '주기적으로 데이터 끌어와' 점수와 배지 줌 (기존 서비스에 rest api추가)
  • 3 곱셈 서비스에 무슨일 있을 때마다 (답안 받으면) 게임화 서비스 호출해 데이터 전송하고 게임 통계 업데이트 (RPC : 원격 프로시저 호출 방식)

1번은 서비스들이 서로 데이터 접근해 컨텍스트 분리하는 이점이 사라짐
2번은 좀 더 낫지만 데이터 지속적으로 끌어와야함. 이미 처리된 답안 추적해야함 (게임화 됐는지)
3번 두번째보다 낫지만 곱셈 서비스는 게임화 서비스를 알 필요가 없다. 곱셈 서비스를 게임화서비스 없이 '그 자체로' 동작하도록 설계해야함!

반응형(reactive) 패턴 이용해 설계하면 유연성 높아짐 ==> 이벤트 중심 아키텍처 또는 반응형 시스템

이벤트 중심 전략이 마이크로서비스 간 상호작용에 항상 맞진 않음.
이벤트 없이 서로 데이터 원할 경우 -> 요청-응답 패턴이 더 적합

5. 이벤트 중심 아키텍처

중요한 행위가 일어날 때마다 여러 마이크로서비스가 이벤트 전송
각 마이크로서비스는 메시지 브로커(이벤트 버스) 통해 이벤트 주고받음
각자 관심있는 이벤트 구독하고 이벤트 처리한다.

이벤트 : 이미 일어난 사건, 바꿀 수 없고, 미리 예방도 불가 -> 이름은 과거형으로!

해당 이벤트를 구독한 마이크로서비스는 각자의 비즈니스 로직 수행하고, 다른 이벤트를 발행 publish 할 수 있음

이렇게 작용-반작용 패턴 기반 하는 시스템 ---> 반응형 시스템

관련 기법

이벤트 소싱 (Event suorcing), 도메인 주도 설계, CORS같은 기법과 밀접한 연관.
이 기법들은 독립적 적용가능, 항상 합리적으로 사용 해야함

  • 이벤트 소싱
    : 비즈니스 개체 저장 방식, 시간 지나면서 변하는 정적 상태로 모델링하는 대신 변경 불가의 일련의 이벤트로 모델링
  • 도메인 주도 설계 (DDD, Domain-Driven Design)
    : 비즈니스 도메인이 시스템의 핵심이라는 소프트웨어 설계 철학, 시스템 별도 처리가능하도록 하위 도메인 같은 bounded context 정의
  • CORS(Command-Query Responsibility Segregation, 명령-쿼리 책임 분리)
    : 데이터 조회 쿼리 모델과 데이터 업데이트 커맨드 모델 분리하는 패턴. 훨씬 복잡한 시스템 사용하는 대신 데이터 매우 빠르게 읽음

이벤트 중심 아키텍처 장단점

프로세스 분산으로 시스템 내 통제하거나 복잡하게 꼬이는 부분이 없어져 결합도 낮춤.
서비스 전체에 ACID 트랜잭션이 없다. -> 궁극적으로 모델 이벤트 전파하고 소비하면 일관된 상태
트랜잭션이 없거나 최소화 되면서 장애 허용 능력이 중요해짐. 잘 구현하면 일체형보다 더 고가용성을 갖춘 분산 시스템 만든다. (일체형은 트랜잭션 범위 넓어 롤백되면 아무것도 할 수 없음)
마이크로서비스는 분산되어있어서 모니터링을 중앙 집중식으로는 따라갈 수 없고 이벤트 흐름 추적 위한 메커니즘 구현해서 서비스 간 상태 기록하는 공통 로깅 시스템이 필요하다.
(이벤트 상호 연관시키는 서비스만의 메커니즘 구현 하거나 또는 Zipkin같은 툴 사용 )

이벤트 중심 아키텍처 적용

6. RabbitMQ와 Spring AMQP로 이벤트 중심 설계하기

RabbitMQ

RabbitMQ는 스프링 부트와 잘 연동되는 오픈소스 메시지 브로커. AMQP(Advanced Message Queuing Protocol)을 구현하고 있어서 도구에 의존적이지 않은 일반적인 방법으로 코드를 짤 수 있음

  • 메세지 전송하는 채널인 익스체인지 만들고 기본적 MultiplicationSolvedEvent 메시지 전송. 메시지는 JSON을 직렬화 -> 쉽게 확장할 수있고 읽기 쉬움
  • 익스체인지는 메시지 보내는 가장 유연한 방식인 토픽으로 만듦
  • multiplication.solved 라는 라우팅 키(routing key)이용해 이벤트 메시지 전송
  • 구독하는 쪽 (게임화MS)에서 Queue 생성하고 토픽 익스체인지 바인딩해서 메시지 받음
  • 큐가 내구성 갖추게 한다. == 메시지 브로커(RabbitMQ)가 다운되도 메시지는 유지되기 때문에 언제든 이벤트 처리 가능

-- 토픽 익스체인지와 라우팅 키로 메시지를 유연하게 사용
-- 라우팅 키 다르게 해서 여러 구독자가 같은 익스체인지로 바인딩, 각자 다른 메시지 받음

온라인 쇼핑몰 예제
주문 취소 서비스 - order, cancelled 구독
이메일 전송 서비스 - order.* (주문 취소, 지연 등 주문 관련 모든 이벤트) 구독

스프링 AMQP

스프링 AMQP로 RabbitMQ와 메시지 주고받는다.
RabbitMQ에서 익스체인지와 큐를 커맨드 라인이나 UI로 바로 설정 가능하지만 스프링 AMQP통해 자바 코드로 설정
장점 : 모든 서비스가 AMQP 설정을 제어하기 때문에 중앙 집중적인 설정에 의존 않는다
단점 : RabbitMQ를 완전히 설정할 수 없다.

7. 곱셈 서비스에서 이벤트 보내기

실습 코드는 아래 링크의 코드로 보면 됨

https://github.com/wikibook/springboot-microservices/tree/master/microservices-v5

org.springframework.boot:spring-boot-starter-amqp 에는 rabbitMQ(spring-rabbit)와 스프링 AMQP(spring-messaging) 가 포함되어있음

  @Bean
  public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
    final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
    rabbitTemplate.setMessageConverter(producerJackson2MessageConverter());
    return rabbitTemplate;
  }

  @Bean
  public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
    return new Jackson2JsonMessageConverter();
  }
  • producerJackson2MessageConverter 메서드는 자바 객체를 JSON으로 직렬화하는 Jackson2JsonMessageConverter를 주입.
  • rabbitTemplate() 빈 설정은 스프링에서 주입해주는 기본 RabbitTemplate을 오버라이드.
    매개변수로 스프링에서 주입해주는 Connectionfactory를 받아 RabbitTemplate 빈을 생성. 그리고 JSON 메시지 컨버터를 설정
    (나중에 RabbitTemplate을 주입하고 이벤트 메시지를 발행)

기본 자바 직렬화 메커니즘 대신 JSON으로 직렬화 했을 때 장점
-- 자바 직럴화는 헤더(_ TypeId __ )를 이용해 클래스 전체 이름에 태그를 지정. 같은 클래스명을 이용해 메시자를 역직렬화하려면 구독자가 같은 패키지 ---> 그럼 서비스 강한 결합 생겨버림
-- 향후 다른 언어로 이뤄진 서비스와 연걸하려면 자바 직렬화를 사용할 수X
-- 사람이 읽을 수 있는 포맷을 사용하지 않는다면 채널(큐와 익스체인지)에서 발생한 에러를 분석하기가 매우 어려움

이벤트 모델링

해결된 곱셈 답안과 구독자가 곱셈 마이크로서비스와 무관하다는 걸 알아보자

Json 메세지 변환 위해 Serializable 구현

이벤트 객체가 커지면 좋지 않다.
객체의 변화하는 데이터를 이벤트에 포함시키는 것은 위험하다. 이보다 사용자 정보가 수정될 때마다 식별자를 알리고, 메시지를 소비하는 쪽에서 로직 처리할 때 최신 상태를 요청하게 하는 편이 나을 수 있음.

이벤트에는 불변 값을 넣는 것이 좋다. (ex. 답안은 곱셈 서비스가 한번 처리하면 수정 불가한 불변값 이므로 사용자에 대한 참조userId와 정답 여부를 boolean으로 전달 가능)

명확한 건 이벤트 모델링은 도메인 모델링 만큼 중요! 처음에는 꼭 필요 정보만 담아서 최대한 작고! 단순하게 유지!
구독자에게 일관되고 일반적인 정보를 충분히~ 전달해야함.

이벤트 전송: 디스패처 패턴

비동기 통신 일반적 패턴
1. 이벤트 디스패처(이벤트 발행자)
2. 이벤트 핸들러 (이벤트 구독자)
모든 클래스에서 서로 이벤트 발행/소비 대신 중앙에서 이벤트 입출력 관리해서 상호작용 쉽게 찾고 이해.

스프링 AMQP는 트랜잭션 지원한다.
@Transactional -> 예외 발생시 이벤트 전송X

-- Mockito로 이벤트 잘 전송 됐는지 테스트 해야함

게임화 마이크로 서비스

게임화 마이크로 서비스를 위해 start.spring.io에서 다시 프로젝트 생성해줌

게임화 마이크로 서비스 - 도메인

ScoreCard : 주어진 사용자가 주어진 시간에 획득한 점수의 모델
Badge : 게임에서 사용하는 모든 배지의 목록
BadgeCard : 특정 사용자가 특정 시간에 획득한 배지
LeaderBoardRow : 리더보드(다른 사용자와 종합 스코어가 표시)에서의 위치
GameStatus : 주어진 사용자의 점수와 배지, 하나의 게임 결과 또는 점수와 배지 합산에 사용될 수 있음

카드(점수와 배지)는 획득 시간 정보 포함
게임의 결과는 하나이상의 ScoreCard와 하나 이상의 BadgeCards 포함

게임화 마이크로 서비스 - 데이터

저장할 모델 : 사용자의 점수와 얻은 배지
하나의 행에 점수 누적 대신 카드 저장, 사용자 총 점수 계산 시 카드 이용해 집계

게임화 마이크로 서비스 - 컨트롤러, REST API

로컬 머신 실행 시 충돌 피하기 위해서 HTTP port 8080 ->8081로 수정

8. RabbitMQ로 이벤트 받기

7번에서는 이벤트 발행을 했고,
이번에는 이벤트 구독하는 쪽 (게임화 서비스)

RabbitMQ 설정

Queue를 exchange에 binding 하는 개념

구독자는 메시지를 소비하는 큐를 생성한다.
이 메시지는 라우팅 키를 이용해 익스체인지에 발행된다.
이는 토픽을 유연하게 교환하는데 도움이 된다.
exchange통해 전송되는 모든 message는 routing key로 tag 붙이고, 소비자는 queue를 exchange에 binding할 때 routing key 또는 pattern을 통해 queue로 전송되는 message를 선택한다.

package microservices.book.gamification.configuration;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer;
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;

/**
 * 이벤트를 사용하기 위한 RabbitMQ 설정
 */
@Configuration
public class RabbitMQConfiguration implements RabbitListenerConfigurer {

  @Bean
  public TopicExchange multiplicationExchange(@Value("${multiplication.exchange}") final String exchangeName) {
    return new TopicExchange(exchangeName);
  }

  @Bean
  public Queue gamificationMultiplicationQueue(@Value("${multiplication.queue}") final String queueName) {
    return new Queue(queueName, true);
  }

  @Bean
  Binding binding(final Queue queue, final TopicExchange exchange,
                  @Value("${multiplication.anything.routing-key}") final String routingKey) {
    return BindingBuilder.bind(queue).to(exchange).with(routingKey);
  }

  @Bean
  public MappingJackson2MessageConverter consumerJackson2MessageConverter() {
    return new MappingJackson2MessageConverter();
  }

  @Bean
  public DefaultMessageHandlerMethodFactory messageHandlerMethodFactory() {
    DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
    factory.setMessageConverter(consumerJackson2MessageConverter());
    return factory;
  }

  @Override
  public void configureRabbitListeners(final RabbitListenerEndpointRegistrar registrar) {
    registrar.setMessageHandlerMethodFactory(messageHandlerMethodFactory());
  }
}
  • 새로운 큐 gamificationMultiplicationQueue() 와 TopicExchange(multiplication) 를 함께 바인딩해서 연결한다. (( binding메서드가 익스체인지와 큐를 매개변수로 받음 )))
  • 큐 생성시 두 번째 매개변수를 true로 설정해 내구성 있게 만든다. (브로커 중지 상태에도 대기중인 이벤트 처리 가능! )
  • 프로퍼티 multiplication.exchange는 곱셈 서비스에서 정의된 것과 같아야함! (multiplication_exchange).multiplication 으로 시작하는 라우팅 키 찾기위해 multiplication.* 패턴 사용
    큐 이름(multiplication.queue)는 선호나는 모든 규칙 사용가능. 이 모든 값은 application.properties에 정의
multiplication.exchange=multiplication_exchange
multiplication.solved.key=multiplication.solved
multiplication.queue=gamification_multiplication_queue
multiplication.anything.routing-key=multiplication.*

이벤트 핸들러

@RabbitListener 로 이벤트 소비하는 메서드 만든다.
브로커가 보낸 메시지를 우리가 정의한 큐를 통해 받는 복잡한 과정 처리한다.

9. 마이크로서비스 간 데이터 요청하기

반응형 패턴과 REST 결합

마이크로서비스가 서로 데이터를 요청하는 것이다.
게임화 서비스 설계 변경해서 행운의 숫자라는 배지 -> 42숫자 포함한 곱셈 답안 제출 경우에만 얻을 수 있음
게임화 서비스는 곱셈 사용 인수를 모른다.

마이크로서비스 간 데이터 공유하기 위해 반응형 패턴 대신 요청/응답 패턴 사용할 수도 있음.(가장 일반적 구현체는 REST API) 이벤트는 이미 발생했기 때문


위 그림처럼
곱셈 마이크로서비스에서 답안 ID로 답안 정보 제공하는 새로운 엔드포인트 노출해야. 그리고 게임화 마이크로서비스에서 인수 조회하는 REST 클라이언트 만들어서 그 중 하나가 행운 숫자면 배지 부여하는 로직 작성

도메인 격리 상태로 유지

게임화 마이크로서비스도 답안 처리하니까 두 마이크로서비스모두 답안 관련 비즈니스 개념 이해 해야함
게임화 마이크로서비스에서도 MultiplicationResultAttempt를 모델링해야함 !

일반적 빠질 수 있는 함정
곱셈화 마이크로서비스에서 도메인 패키지를 게임화 마이크로서비스와 공유할 수있는 별도의 라이브러리로 뽑아서 MultiplicationResultAttempt에 접근하는 것 ====> 매우 안좋음
다른 마이크로서비스의 로직이 이 도메인에서 사용하면 해당 도메인은? 통제불능 상태!!! ==>상호 의존성 생겼기 때문
반드시 도메인 개체의 소유권은 하나의 마이크로서비스만 가지고 있어야함!

좀 더 나은 대안은 단순히 모델 복수해서 공유하는 것 -> 데이터 전달 객체(DTO, Data transfer object)에 기반
-> 시간 걸리고 좋지 않은 방법 따르면 의존성 생김

소비자가 가공하지 않은 응답을 보내도 상관없다면 (json등) 마이크로서비스를 가능한 격리시키기 위한 가장 좋은 방법 : 아무것도 공유하지 않는 것
필드 두개만 필요하다면 해당 필드만 역직렬화 하고 나머지는 무시
--> 마이크로서비스는 특정 필드 변경 경우에만 영향 받음

아직

마이크로서비스 완성했지만 아직 세 번째 사용자 스토리 완성 못함. UI 접속하는 리더 보드 제공하고 기술적인 측면에서 애플리케이션 사이의 강한 연결을 개선해야한다.

profile
HelloWorld! 같은 실수를 반복하지 말기위해 적어두자..

0개의 댓글