[SpringBoot] Monolithic -> MSA 구조 전환 (MSA_1)

Tessssssssy·2024년 8월 17일
post-thumbnail

도입 계기

약 3개월 동안 진행했던 캠핑온탑  기능 보강 리팩토링 프로젝트가 끝났다.

그동안 아래의 여러 기능들을 구현하여 성공적으로 적용했다.


최종 코드 병합 후 수정된 버전으로 배포를 완료했다.

그동안 갈고 닦은 실력을 오롯이 발휘하기 위해 낮과 밤을 가리지 않고 최선을 다했다.
그 결과, 리팩토링을 하기 전 계획했던 모든 기능들을 목표로 설정한 8월 내에 완료할 수 있었다.

내가 동생이지만 깍듯하게 팀장 대우를 해주고 군말없이 끝까지 함께해준 팀원 형님에게 무한한 감사를 보낸다.


지금 코드는 모놀리식으로 구성되어 있는데, 기능 보강 작업이 끝났기 때문에
이제 Micro Service로 전환하는 작업에 돌입하려고 한다.

현재 상황에서 내가 운영하고 있는 서비스의 도메인에 많은 사람들이 접속하고 있지 않기 때문에
사실 굳이 MSA로 전환할 필요는 없다.

하지만 이미 많은 회사들이 MSA 아키텍처 기반의 개발 환경을 갖추고 있고,
MSA 개발 경험을 갖춘 개발자에 대한 수요가 높아지고 있다.

게다가 이전에 MSA로 전환하는 연습을 할 때,
이메일 인증, 로그인 정도만 micro service로 전환하는 정도에서 중단했기에 내심 아쉬움도 있었다.

따라서 MSA로 리팩토링하는 작업을 하여 이에 대한 개발 경험과 노하우를 쌓아서,
입사 후 곧바로 적응할 수 있도록 준비를 해야겠다고 판단했다.

다만, MSA로 전환하면 나는 AWS의 프리티어 계정을 사용하고 있기에 리소스에 한계가 있어
모든 micro service들을 다른 서버에 배포할 수 없다는 맹점이 있다.

여러 개의 계정을 만들어서 할 수는 있지만, 현실적으로 유지보수를 하기 어렵다.


따라서 대안이 있는지 찾아보면서, 우선 로컬 환경에서 아래의 과정들을 진행하려고 한다.

  • 기존의 monolithic에 있는 기능들을 하나씩 micro service로 분리
  • Vue로 구성되어 있는 프론트엔드의 API 호출 경로 변경
  • DB 이중화를 통한 읽기, 쓰기 서버 분리
  • EurekaAPI Gateway를 최대한 사용하기 위한 방식 연구 및 도입
  • 이 과정에서 사용될 Kafka, OpenFeign 등을 이용한 메세지 발행구독 방식 학습

아래 이론과 실습 내용은 부트캠프 수강 당시 실습한 내용을 첨부한 것이므로
캠핑온탑의 MSA 전환 화면은 맨 아래 성공 화면에서 보이도록 하겠다.


여담이지만 MSA 구조로 각 서버들이 이중화, 삼중화가 되어 있으면서
데이터베이스도 모두 이중화, 삼중화 등이 되어 있고
CI/CD Flow도 갖추어져 있어 자동 무중단 배포가 될 수 있는 환경에서 원없이 개발을 해보고 싶다.



MSA

  • Micro Service Architecture
  • 전부 도메인 별로 아예 프로젝트를 나눠서, 서버를 나눠서 구현하면
    여러 기능들 중 하나의 서버가 중단된다고 하더라도 전체 서비스중단되는 일을 막을 수 있다.

Monolithic vs MSA

  • Monolithic
    • 서비스의 서버가 하나로 되어 있으면 기능 하나에 문제가 있을 때,
      전체 서비스중단될 수 있다는 큰 문제가 있다.

    • 하나의 서버로 되어 있으면 배포 서버를 두 곳으로 늘린다고 할 때
      트래픽이 많지 않은 다른 기능도 모두 포함되어 있으니까 전체 서비스를 duplicate 해야한다.

  • MSA
    • 장애가 발생했을 때 처리하기 용이하다.

    • 부하 분산을 특정 기능만 늘릴 수 있다.

    • 한 쪽에서는 프로젝트를 Java, 한 쪽에서는 Python
      다른 프로젝트, 언어로 개발해도 상관없다.

    • 각각의 프로젝트가 쪼개져 있을 때
      전체 시스템이 하나로 되어 있을 때보다 인력도 적고 프로젝트를 진행하기에 용이하다.

    • 개발 속도, 생산성 모두 높다.

MSA 단점

  • 서버많이 필요하다.

  • 개발 속도가 빠른 것이 무조건 좋은 건 아니다
    → 변경이 많이 이루어진다.
    → 배포를 많이 해야 한다.
    → 시스템을 통합할 때 문제가 발생할 수 있다.

  • 이전에는 위 문제가 발생했을 때 적당한 해결 방법이 없었는데
    이제는 Cloud가 등장하면서 많이 해결되었고 CI/CD 방식을 사용한 자동화 방식도 도입되었기 때문에
    MSA 방식이 현재 시점에서는 가장 좋은 Architecture이다.

  • 스타트업의 경우 기능이 많지 않은데 굳이 MSA 방식으로 할 필요는 없다.
    → 어느 정도 기반 시스템이 구축되고 트래픽이 늘어나면
        그 때 MSA 방식으로 바꾸는 방식으로 프로젝트를 진행한다.

  • 서비스가 운영 중이라는 상황이라고 가정하고
    코드가 변경될 때마다 서버를 중단시키지 않고 배포할 수 있으니까 이런 방식으로 실습을 진행한다.
    → 항상 누군가가 접속을 할 수 있도록 해야 한다는 점을 가정하고 진행한다.

  • 예전에 주로 구현되던 방식


  • ex.) NETFLIX
    • NETFLIX OSS라는 것을 만들어서 자체적으로 MSA 방식으로 구성한다.
      (Open Source software)

    • ex.) Eureka, Ribbon, …

    • Eureka
      • 결제 서버, 회원 서버, 회원에서도 회원 가입 서버, 로그인 서버를 따로 구현할 수 있다.

      • 너무 서버가 많아지다 보니까 일일이 수동으로 어떤 com이 어떤 IP를 갖고 있고
        어떤 서비스를 제공하고 있는지 개발자가 알 수 없다.
        • Eureka만 실행되고 있으면 Eureka 서버에 모든 서버 정보를 등록해서
          편하게 사용할 수 있다.

    • Hystrix
      • 장애와 관련

      • 특정 서비스에서 장애가 발생할 수 있는데
        장애가 다른 기능으로 전파되면 안된다. <중요>

      • Circuit Breaker (차단기)
        • circuit breaker 기능을 하는 프로그램


  • 이제 Spring이 위 open source를 보고 Spring Cloud를 만들어서
    Eureka, Hystrix, Ribbon 등을 Netfilx oss와 함께 사용할 수 있고,

    Spring cloud에서는 Config, Stream, Sleuth 등을 추가적으로 개발해서 제공하기에
    우리는 MSA 방식으로 개발을 더 편하게 할 수 있다.

  • 우리는 대부분 Spring Cloud에서 제공하는 기능들을 이용해서 구현한다.

  • ZuulSpring Cloud API Gateway로 대체되었다.
    (API Gateway에 각종 URL을 등록해놓고 gateway에서 어떤 서버로 가라고 알려 준다.)


  • MSA
    • 최근 들어서 도입된 기술
    • 정답은 없는데 API Gateway를 사용하는 곳도 있고 사용하지 않는 곳도 있다.
      그래서 API Gatewayshut down되면 전체 서비스가 마비되는 것이기 때문에
      API Gateway이중화, 삼중화해서 사용하는 경우도 있다.
      (중요하게 생각해야 함.)


  • AxonFramework - Event Handling Layer
  • Kafka

  • CI/CD와 서버가 많이 필요하면 클라우드가 단점을 보완해준다.

  • 각각의 서비스 단위로 쪼개서 개발하는 것 = MSA

  • 빠르게 개발할 수 있다는 건 요구 사항에 대한 대처를 빠르게 할 수 있다는 의미
    → 이에 따라 개발하는 Architecture, Java 파일도 변경되어야 한다.


  • 지금은 controller, Service, Repository 구성으로 되어 있는데 (monolithic 구조이니까)
    아래 언급할 상황을 적용할 수 있지만 어느 정도의 한계가 있다.

    하지만 MSA에서는 구조상 아래의 장점을 극대화할 수 있다.
    • 3 layer가 종속되어 있는데 Java에서 종속성을 느슨하게 만들어주는 문법 예제이다.

         List<String> a = new ArrayList<>():

    • ArrayList 객체를 만들어서 할당해 주었는데
      원래는 ArrayListList에 종속되어 있어야 한다.

      하지만 List에는 ArrayList 뿐만 아니라 다른 List들도 객체를 만들어서 할당할 수 있다.
      ⇒ 종속성이 느슨한 것이다.


  • 그래서 MSA에서는 serviceinterface로 만들고 serviceImpl로 구현하는 방식으로 하고
    controller에서 interface의 의존성을 주입 받아서 사용하는 방식으로 사용한다.

  • 하나의 interface를 구현한 여러 impl 클래스 파일이 있고
    그 interface를 의존성 주입받아서 사용하면, import하면
    controller의 코드 변경을 하지 않아도 impl만 변경해서 사용하면 된다.
    ⇒ 종속성이 느슨한 구조

  • 위 구조를 극한으로 끌어 올린 구조
    Hexagonal Architecture

Web AdapterController
Use caseService
Persistent AdapterRepository
packageAdapter, Application, Domain으로 나눌 것.


  • 외부와 내부를 연결하는 부분은 무조건 입력 포트interface를 통해야 한다.

  • client에게 요청을 받고 db에 저장하고 client에게 반환하는 것 뿐만 아니라
    micro service들 간에 통신이 필요하다.

    그래서 외부 시스템 어댑터 - 입력 포트 - 유즈 케이스 - 출력 포트 - 외부 시스템 어댑터
    의 구조가 있는 것이다.

  • port어댑터를 연결해서 사용하는 구조
    • controller라고 이름 불러서 부르는 것과 같다.

  • Port, Adapter가장 중요한 개념이다.



실습

  • 회원 기능을 Hexagonal Architecture로 구현해보자.
  • 아직 MSA 아님!!

  • Port
    • 입력 포트
    • 출력 포트

  • 웹 어댑터에서 입력 포트로 들어오는 것도 있고
  • 외부 시스템 어댑터에서 입력 포트로 들어오는 것도 있다.

    • 웹 어댑터/adapter/in/web
    • 입력 포트/application/port
    • 유즈 케이스/application/service
    • 출력 포트/application/port
    • 영속성 어댑터/adapter/out/persistence (repository)

  • 웹 어댑터에서 들어와서 입력 포트유즈 케이스출력 포트출력 어댑터

  • 변경 요구 사항이 들어왔을 때 가장 많이 변경하는 것이 유즈 케이스
  • 웹 어댑터입력 포트를 가져와서 사용하는 것이기 때문에 변경될 일이 없다.
  • 유즈 케이스에 입력 받은 데이터가 들어오기 전에 검증을 한다.
    검증 dto를 만든다.

  • 개발 순서는 주로 Domain부터 만들고 시작한다.


정리

  • 포스트맨 회원 가입 요청
    • /adapter/in/web/RegisterMemberController 실행
    • /application/port/in/RegisterMemberUseCase의 구현체인
      /application/service/RegisterMemberService 실행
    • /application/port/out/RegisterMemberPort의 구현체인
      /adapter/persistence/MemberPersistenceAdapter 실행
    • SpringJpaEntity 이용해 저장

저장하고 영속성 어댑터에서 출력 포트를 거쳐서 유즈 케이스를 거쳐 엔티티 갔다가 다시
입력 포트로 와서 웹 어댑터를 통해 client로 최종 반환한다.


나중에는 application layer만 변경하고 맨 앞, 뒤 layer는 코드를 변경하지 않아도 된다.


Bean으로 등록해야 하니까 @Componet로 등록했는데
@Persistence라는 annotation을 만들어서 달아주자.


Newmodule (프로젝트 안에 또 다른 project 생성)

아직 monolithic이고 MSA이 방식 아니다.


common에 사용할 주요 module들을 추가하고 annotation을 변경한다.
(controller, service)


@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface WebAdapter {
    @AliasFor(annotation = Component.class)
    String value() default "";
}


그리고 기존 src는 삭제하고 안에 main, test를 새롭게 module 만들어서 그 안의 src에 복사한다.


**New modules**로 해야 한다!!(Spring Initializer x)


기존에는 /abc라면 결제 기능인데 /def로 새롭게 따로 만들어서 구현한 후
프론트엔드에서 url 바꿔주고 잘 되면 기존 monolithic에서 작동하던 결제 코드를 삭제한다.


기존 프로젝트에서 하나씩 떼서 개발해자.

cf.) Stored Procedure 스토어드 프로시저

  • MSA에는 분산 트랜잭션 처리를 해야 하는데 이 부분이 어렵다.

  • DB에 저장하고 다른 곳에도 똑같이 저장한다.
    Elastic Search로 검색 속도 향상 및 DB 부하 분산

  • AWS Dynamo DB
  • Batch 처리

  • Kafka 서버로 메세지를 보내고 받을 것.
  • 우리는 Transaction 처리를 Axon Framework로 처리할 것.

  • CQRS(Command and Query Responsibility Segregation)
    • 도메인을 가장 중점적으로 생각해서 Hexagonal Architecture로 만들고
      Kafka 등으로 어떻게 주고 받을 수 있게 할 것인지 고민하는 과정
    • 둘을 철저하게 분리

  • DDD

  • 조회(Query), 명령(Command) 단으로 분리해서
    만약 가게 정보가 변경되면 이벤트를 쏘고 조회(Query)에서 그 eventconsume해서
    조회 단의 데이터들을 갱신하는 방식이다.

  • 조회는 자기 db를 조회하는 것이다.
    명령의 db에서 가져와서 조회 단에서 client로 전달하는 것이 아니다.
    <비동기 방식>



MSA에서 가장 중요한 건 데이터 동기화

Kafka를 통해 message queue를 통해서 진행한다.

  • 데브원영 Kafka 책 추천.
  • message queue 역할을 하는 대표적인 시스템이 Kafka

  • monolithic에서 MSA로 넘어갈 때 어떤 message queue를 사용할까?
  • RabbitMQ, Redis queue 등도 있다.

  • message queue에 쌓아 놓으면 서버가 뻗어도 데이터를 가져오는 기능이 Kafka가 좋다.

  • 그럼 Message queue 자체가 뻗으면…?
    RabbitMQ는 사라지는데 Kafka는 디스크 기반이라 사라지지 않는다.(하지만 메모리에서 동작)

    RabbitMQ메모리 방식이라 속도는 빠르지만 뻗으면 사라진다. (안전 X)
    • RabbitMQ는 주로 캐시 서버를 사용한다.

  • 각 서비스에 맞게 결정해야 한다.

  • MSA는 각 기능끼리 message를 주고 받아야 하는데 그럼 이 message를 주고 받는
    서버가 매우 중요하고 이 서버도 아주 많아야 한다.

  • AWS Kafka / Confluent Kafka


API Gateway

Spring Cloud Gateway

  • gateway라는 이름의 module 추가하고 pom.xmlspring cloud dependency 추가
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        <version>3.1.3</version>
        <exclusions>
            <exclusion>
                <groupId>javax.servlet</groupId>
                <artifactId>servlet-api</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
        <version>3.1.3</version>
    </dependency>
</dependencies> 
  • Spring Cloud의 version을 잘 맞춰서 적용해야 한다.

  • GatewayApplication class 파일 추가

@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
} 
  • Spring Cloud Gateway의 실행 프로그램이 되는 것.

  • 각 프로젝트들이 서버에서 실행되는데 8080으로 실행될 테니까
    포트 번호들을 모두 바꿔줘야 한다.


server:
  port: 9999 

  • Adapter annotation 수정

  • pom.xmldependency 메인 pom.xml
    commonpom.xml, gatewaypom.xml 변경
    • micro service에서 사용하는 라이브러리들만 알맞게 추가해주는 것이 좋다.

  • ex.)
    • 전체
      • lombok
      • spring-boot-starter-testcommon
      • spring-boot-starter-validation
    • member-service
      • spring-boot-starter-web
      • spring-boot-starter-data-jpa
      • mysql-connector-j
      • spring-boot-starter-validation
      • common

server:
  port: 9999

  application:
    name: GATEWAY-SERVICE

  cloud:
    gateway:
      routes:
        - id: main-service
          uri: lb://MAIN-SERVICE
          predicates:
            - Path=/main/** 
  • gateway - application.yml

  • Gateway가 실행되는 모습이다.


세션 기반 인증 로그인 보다는 토큰 기반 인증 사용

  • 하는데 MSA에서는 어떻게 구현할 것인가
  • 정답은 없다.

<방법 1>

  • 인증 및 인가 MSA를 만들고 (회원 가입, 로그인)
    • 우리는 토큰 기반 인증 로그인이니까 토큰 발급해주면 된다.
    • 그럼 얘는 로그인해야만 사용할 수 있는 서비스
    • 그리고 토큰 달고 서비스에 들어왔는데
    • 다른 서비스에서는 이 토큰이 맞는지 어떻게 확인할까?
      • 그럼 인증 및 인가 서비스에 맞는지 물어보고 response 받아야 함.
      • 그럼 인증 및 인가 서버에 부담이 많아질 수 있다.

<방법 2>

  • 토큰 확인 하는 로직을 인증 및 인가 서버에 넣고 다른 서버에도 넣어준다.
    • 이 경우 문제점은 변경 시 다 수정해주어야 한다는 점이다.
      → 모듈로 빼서 개발하면 사실 큰 문제는 아니다.
      • Spring Security에서는 Filter를 만들어서 토큰을 검증한다.

  • 우리는 API Gateway에 토큰을 확인하는 모듈을 만들어서 개발한다.

port9999로 해서 gateway에 요청했고 gateway - application.yml 설정에 따라
8080/member/signup으로 요청이 가서 최종 회원 가입이 된 결과를 확인했다.



리눅스 가상 머신 3대 준비

  • server12
  • server12_1
  • server12_2


Apache Kafka

  • 원래 Kafka 서버는 2대만 있으면 된다.

  • Zookeeper 서버
    • Kafka 서버가 죽었는지 살았는지 관리하는 서버

  • Kafka 브로커 서버
    • messaging queue 서버의 메인 역할
    • zookeeper가 정상적으로 관리를 해야 잘 작동됨. (zookeeper에 종속되어 있다.)

  • Producer 서버
    • 나중에 Spring에서 Producer 개발해야 함.
      message 보내는 역할

  • Consumer 서버
    • 나중에 Spring에서 Consumer 개발해야 함.
      message 받는 역할

  • 3대만 준비하라고 한 이유는 zookeeperapache kafka에서 사용하지 않을 거라고 해서.
    ⇒ 그래서 일단 Zookeeper 서버와 Kafka 브로커 서버를 1개의 서버에서 실행할 것이다.

  • HDFS(hadoop), HBase, mahout, …
    → 빅데이터 저장하는 서버 프로그램

  • 서버 3대에 모두 Kafka 설치
    wget https://downloads.apache.org/kafka/3.6.1/kafka_2.13-3.6.1.tgz
    tar -xvzf kafka_2.13-3.6.1.tgz
    cd kafka_2.13-3.6.1

  • java11 설치

  • 실무에서는

    • Kafka 최소 3
    • zookeeper 최소 3
  • 우리는 서버 1대에 한 것이다.


./bin/zookeeper-server-start.sh ./config/zookeeper.properties

./bin/kafka-server-start.sh ./config/server.properties

zookeeper, kafka 실행 완료.




왜 서버들은 3대로 구성할까?

  • IDC 센터를 보면 서버 장비들이 막 들어가 있는데 서버를 넣어 놓는 큰 칸을 rack이라고 한다.
    같은 rack2대를 놓고 다른 rack1대를 놓는다.
    같은 rack에 넣어 놓으면 서버가 뻗었을 때, 다 뻗어 버리기 때문에
    1대는 다른 rack에 넣고 살려 둔다.

  • 이 중 셋 다 똑같은 역할을 맡아서 동작하는 것이 아니라 이 중에서 대표가 있고 상의를 해서 결정한다.
    이 서버 3대 간에도 투표를 해서 결정하는데 짝수면 결정이 나지 않으므로
    cluster 구성을 할 때, 대부분 3대, 5대, … 이런 식으로 서버를 늘리는 것이다.

  • 서버 1대는 브로커/zookeeper 서버
  • 서버 1대는 Producer
  • 서버 1대는 Consumer

<!-- producer -->

./bin/kafka-console-producer.sh --topic test --bootstrap-server SERVER12_IP:9092
<!-- consumer -->

./bin/kafka-console-consumer.sh --topic test --bootstrap-server SERVER12_IP:9092
vi config/server.properties

advertised.listeners=PLAINTEXT://SERVER12_IP:9092

서버에서 변경


zookeeper, kafka 정상 실행 확인 완료


producerconsumer message 전송 성공




실행 화면

  • 디렉토리 구조 모습

  • Eureka 서버, main-service(기존 monolithic) 등 실행 화면


이제 Monolithic이었던 프로젝트를 MSA 구조가 될 수 있도록 준비만 마쳤다.


단순히 Monolithic프로젝트를 하나의 서비스로 전환하고,
Eureka 서버와 API Gateway 등만 추가한 것이다.


아직 설치한 Kafka를 사용하지도, OpenFeign으로 메세지를 주고 받는 작업도 하지 않았다.


앞으로 유저 이메일 인증, 로그인 기능부터 하나씩 뜯어서
개별적인 Micro Service로 전환하는 작업을 할 것이다.






profile
Web Developer

0개의 댓글