MSA 1

j0yy00n0·2025년 4월 26일

2025.03.31

MSA

MSA(Micro Service Architeture)

  • 특정 기술이 아닌, 소프트웨어 아키텍처 설계 방식
  • 하나의 커다란 애플리케이션을 작고 독립적인 Service 단위로 나누어 개발하고 운영
  • 각 서비스는 고유한 기능을 담당하며, 독립적으로 배포 및 운영 가능
  • 서비스 간에는 API를 통해 통신하여 전체 시스템을 구성

MSA 장점

  • 확장성, 유지보수 용이성, 독립적인 배포 가능성
  • 서비스가 독립적이기 때문에 Update, Error 발생 시 다른 서비스에 커다란 영향이 없다

MSA 단점

  • 분산 시스템의 복잡성을 증대
  • 서비스 간의 통신 및 데이터 관리에 어려움
  • 작은 팀이나 프로젝트 초기 단계에서는 모놀로식 아키텍처가 더욱 효율적

MSA vs Monolithic

차이점

모놀로식 아키텍처

  • 모든 기능과 주요 모듈이 하나의 코드베이스 == Application 에 통합 된 구조
  • 사용자 인터페이스(UI), 비즈니스 로직(Service Layer), 데이터 엑세스 로직(DAO)이 모두 하나의 Application 으로 구성
  • 배포, 테스트, 확장도 하나의 단위로 진행됨

마이크로 서비스 아키텍처

  • 각 기능이 독립 된 서비스로 나뉘어 있다.
  • 서비스는 서로 API 통신
  • 각각의 서비스가 독립적인 Application 단위로 구성

기술 스택과 유연성

모놀로식

  • 하나의 기술 스택에 국한되는 경우가 많다
  • 기술 선택의 유연성을 제한
  • 새로운 기술 도입 시 전체 App 에 영향

마이크로 서비스 아키텍처

  • 각 서비스가 독립적으로 개발이 되기 때문에, 필요에 따라 적합한 기술 스택을 선택
  • 최신 기술을 신속하게 도입할 수 있는 장점

확장성과 성능

모놀로식 아키텍처

  • 전체 어플리케이션을 확장해야 하므로 자원 낭비가 발생 가능
    - 필요한 로직이 아니더라도 같이 확장을 해야한다.
  • 특정 기능에 대한 수요 증가 시 전체 시스템의 자원을 모두 늘려야 하는 단점

마이크로 서비스 아키텍처

  • 필요한 서비스만 개별적으로 확장할 수 있어 자원 활용을 극대화
  • 트래픽이 높은 서비스에 매우 유리
  • 스케일 아웃 가능
    - 트래픽이 집중되는 서비스만 자원을 확장 시킨다.

장애 격리 및 회복성

모놀로식 아키텍처

  • 하나의 컴포넌트에 장애 발생 시 전체 어플리케이션에 영향
  • 서비스의 신뢰성을 저하시키는 요인이 될 수 있음

마이크로 서비스 아키텍처

  • 서비스 간의 의존성이 낮기 때문에 전체 시스템의 영향을 최소화 가능
  • 각 서비스는 독립적으로 회복 할 수 있어 전체 시스템의 가용성을 높일 수 있다

관리와 모니터링

모놀로식 아키텍처

  • 단일 시스템에서 모든 Log와 성능 데이터를 중앙집중적으로 추적할 수 있기 때문에 모니터링이 비교적 간단
  • 배포 주기가 길어질 수 밖에 없다.

마이크로 서비스 아키텍처

  • 여러 개의 독립적인 서비스로 구성되어 있기 때문에 서비스 간의 상태와 성능을 모니터링 하는 것에 난이도가 높아진다.
  • 통합 모니터링 도구와 관리 시스템(분산 트레이싱 시스템)이 필수이며, 이러한 시스템은 복잡성을 증대시킬 수 있다.
  • 배포 주기가 모놀로식 보다 짧아질 수 있다.

MSA 도입 시점

  • Application 이 성장하고 , 요구사항이 복잡해질 때
  • 트래픽의 증대와 Application 의 배포 주기를 줄여야 할 때

Spring Cloud

Spring Cloud 개념

  • MSA를 구축하고 운영하는 데 필요한 다양한 공통 기능을 제공하는 Spring 기반 프레임워크
  • Spring Cloud 는 여러 개별 서비스가 서로 원활하게 통신하고 관리될 수 있도록 돕는 도구와 패턴을 제공

Eureka

  • Netflix 가 개발한 서비스 등록 및 발견 서버
  • 각 서비스는 Eureka 서버에 자신의 정보를 등록
  • 다른 서비스는 이를 통해 필요하 서비스를 찾아 호출
  • 클라이언트 측 로드 밸런싱과 장애 감지 기능을 제공

Spring Cloud Architeture

API Gateway

  • Client 가 서비스를 요청할 때, 직접 개별 서비스에 접근하는 것이 아닌 API Gateway 를 통해 요청 전달
  • MSA 환경에서 단일 진입점(Single Entry Point) 역할
  • 핵심 역할 : 라우팅(요청을 적절한 서비스 인스턴스로 전달)
  • 추가적 역할 : 인증, 권한 부여, 로깅 등의 공통 기능 수행
    -> API Gateway에서 한 번 처리하고 나머지 서비스는 비즈니스 로직에 집중 가능

Service Discovery

서버는 각각의 서비스 인스턴스를 클라이언트로 인식하고, 이를 관리하기 위해 Service Discovery 기능 사용

Eureka

  • Service Discovery 를 담당하는 요소
  • 각각의 서비스 인스턴스의 위치(주소)를 추적 및 관리하는 역할

Eureka Client

  • 각각의 서비스 인스턴스
  • 시작 시 Eureka Server(중앙 서버)에 자신을 등록
  • 주기적으로 상태를 Health Check -> 자신의 상태를 서버에 전달

Eureka Server

  • Service Discovery 중앙 서버 역할
  • 각 서비스 인스턴스들이 자신의 상태와 위치를 등록하는 중앙 관리 서버
  • 클라이언트 요청 시, 서비스 인스턴스 목록을 제공하여 라우팅 지원

Load Balancer

  • 클라이언트 요청시 트래픽을 어느 서비스 인스턴스로 요청을 보낼지 결정하는 역할
  • 서비스 간의 트래픽 분산을 담당
  • 각 인스턴스의 상태(Health Status) 를 주기적으로 Check
  • 문제가 있는 Service 에는 트래픽을 보내지 않으며 서비스 안정성을 높인다
  • Scale out 시 새로운 인스턴스에도 자동으로 트래픽을 분산하여 확장성에 유리
  • 라운드 로빈(Round Robin) - default :
    - 요청이 여러개 들어오면 순서대로 인스턴스에 분산
  • 최소 연결(Least Connections) :
    - 현재 가장 적은 연결 수를 가진 인스턴스로 요청을 보냄
    • 인스턴스마다 처리 속도가 다를 때 유리
  • 가중 라운드 로빈(Weighted Round Robin) :
    - 서버의 성능까지 체크, 가중치(성능 비율)에 비례하여 나눠서 트래픽 할당 설정

Service Instance

  • 서비스들은 각각의 Port 에서 구동이 되며 실제 비즈니스 로직을 수행
  • 각 인스턴스는 Client 의 요청에 따라서 API Gateway 와 Load Balancer 에 의해 서비스 된다

전체 통합 정리

  • Client Request → API Gateway
  • API Gateway → Eureka Server 에 해당 서비스 인스턴스 정보 조회
  • Load Balancer 에게 정보 전달 → 적절한 Eureka Client 요청 전달
  • Service Instance 는 요청 처리 후 API Gateway 를 통해 Client 에게 Response

MSA 프로젝트 생성 - IntellJ

Eureka-Server


  • 의존성 추가
dependencies {
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
  • application.yml 서버에 대한 포트 설정
# Eureka Server 역할을 하는 서버를 통상적으로 사용하는 포트 = 8761
server:
  port: 8761

# eureka 설정
eureka:
  client:
    # 서버는 클라이언트가 아니기 때문에 서버에 등록하지 않게 설정
    register-with-eureka: false
    # 다른 Eureka 서버로부터 Service Registry를 가져오지 않음 (자체가 Registry이기 때문)
    # 다른 서비스를 주기적으로 갱신해서 인지 할 필요가 없음
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka
  • Application @EnableEurekaServer 등록
@SpringBootApplication
<!-- 해당 애플리케이션을 Eureka Server로 활성화 -->
@EnableEurekaServer
public class [EurekaServerApplication] {

    public static void main(String[] args) {
        SpringApplication.run([EurekaServerApplication].class, args);
    }

}

  • 실행 시키고 http://localhost:8761에 접속하면 Eureka 대시보드(UI)가 열린다.
    - 이 뷰는 Service Registry에 등록된 클라이언트들(서비스 인스턴스)을 확인하고 모니터링할 수 있는 사이트이다.
  • 서비스 인스턴스를 등록을 하면 이 부분에 현재 사용 가능한 인스턴스 목록을 보여준다.

jar 파일로 배포하는 방법

인텔리제이에서 계속 프로젝트를 실행시키면 개발 환경이 무겁고 리소스가 많이 소모
-> 프로젝트를 jar 파일로 빌드(배포)해서
운영 환경에서는 jar 파일만 실행해서 필요한 기능을 가져다 씀

jar 파일 (Java Archive) :

  • 프로젝트 전체를 하나의 실행 가능한 파일로 압축해 놓은 것
    -> 프로젝트를 하나의 스냅샷처럼 응축한 형태
  • Spring Boot 프로젝트의 jar는 내장 Tomcat 서버도 포함
    -> 독립 실행 가능 (서버 따로 설치할 필요 없음)

빌드 방식 (Gradle 기준) :

  • bootJar:
    - Spring Boot 전용 빌드 방식
    - 실행 가능한 jar 파일을 생성 (내장 Tomcat 포함)
    • Spring Boot 애플리케이션 배포 시엔 보통 bootJar 사용
  • build (jar):
    - 일반적인 라이브러리 형태의 jar 파일을 생성
    • 실행보다는 다른 프로젝트에서 참조할 때 사


Gradle의 빌드 작업(Tasks)에서 원하는 빌드 방식을 더블 클릭해서 실행
-> build/libs 디렉토리에 [프로젝트명]-[버전]-SNAPSHOT.jar 파일이 생성


window의 경우 백그라운드에서 실행하는 방법

  • 로컬 디렉토리를 타고 들어가서 .jar 파일이 있는 곳에서 cmd 창을 연다.
  • 그 폴더에서 Shift + 우클릭 → PowerShell(또는 CMD) 창 열기

    cmd에서 java -jar라고 입력하고 tab을 누르게 되면 자동으로 경로를 작성해 준다.

    enter를 누르게 되면 cmd 창에서 어플리케이션이 실행된다.

Eureka-Client


  • 의존성 추가
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
  • application.yml 클라이언트에 대한 포트 설정
# 클라이언트 포트 설정
server:
  port: 8001

# 어플리케이션 이름 설정
# Eureka에 등록될 이름
spring:
  application:
    name: eureka-client

eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      # Eureka Server 주소
      defaultZone: http://localhost:8761/eureka
  • Application @EnableDiscoveryClient 등록
@SpringBootApplication
<!-- Eureka Client(또는 Consul 등 Service Discovery Client)로 활성화 -->
@EnableDiscoveryClient
public class [EurekaClientApplication] {

    public static void main(String[] args) {
        SpringApplication.run([EurekaClientApplication].class, args);
    }
}
  • 통신이 잘 되는지 테스트를 위한 Controller 생성
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hi(){
        return "안녕?";
    }
}


어플리케이션 실행 시켜보면 설정해놓은 Eureka 대시보드에 설정해 놓은 Eureka Client 이름이 정상적으로 등록된 것을 확인할 수 있다.

Status 항목에서 링크를 눌러서 controller에 저장해 놓은 엔드포인트 url 주소를 입력하게 되면 정상적으로 동작하는지 테스트할 수 있다.


Service

Eureka Client: First Service 등록
여러 개의 서비스를 등록해서 확인한다.

First Service

  • 의존성 추가
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
  • application.yml 클라이언트에 대한 포트 설정
server:
  port: 8111

spring:
  application:
    name: 1st-service

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka
  • Application @EnableDiscoveryClient 등록 (First Service)
@SpringBootApplication
@EnableDiscoveryClient
public class [FirstServiceApplication] {

    public static void main(String[] args) {
        SpringApplication.run([FirstServiceApplication].class, args);
    }
}


어플리케이션 실행 시켜보면 설정해놓은 Eureka 대시보드에 설정해 놓은 Eureka Client(Service) 이름이 정상적으로 등록된 것을 확인할 수 있다.

  • 통신이 잘 되는지 테스트를 위한 Controller 생성
@RestController
public class FirstController {

    /* Environment를 통해 현재 애플리케이션의 설정 값을 가져올 수 있음 (포트 번호 등) */
    private Environment environment;

    public FirstController(Environment environment) {
        this.environment = environment;
    }

    @GetMapping("/health")
    public String health() {
        /* 현재 실행 중인 로컬 서버의 포트 번호를 반환 */
        return "나는 잘 살아있어 내 포트 = " + environment.getProperty("local.server.port");
    }

}


Status 항목에서 링크를 눌러서 controller에 저장해 놓은 엔드포인트 url 주소를 입력하게 되면 정상적으로 동작하는지 테스트할 수 있다.


Local 환경에서 Scale Out (확장 테스트)

IntelliJ 구성 편집 → 구성 복제 (Configuration 복사)

복제한 구성에서 포트 번호 변경, 그 뒤 실행을 눌러준다.

두 개의 인스턴스가 생긴 것을 확인할 수 있다.

Eureka 대시보드에서도 두 개의 인스턴스가 등록되 것을 확인

가상의 포트번호를 랜덤하게 받기

  • application.yml 클라이언트에 대한 포트 설정 -1
# 랜덤 포트 할당
server:
  port: 0

spring:
  application:
    name: 1st-service

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka


port: 0 으로 포트 설정하면 Spring boot는 5만 ~ 6만번 대의 포트를 랜덤으로 할당한다.

  • application.yml 클라이언트에 대한 포트 설정 -2
server:
  port: 0

spring:
  application:
    name: 1st-service

eureka:
  instance:
  	# instance-id를 "서비스 이름:랜덤 값" 형태로 설정
    instance-id: ${spring.application.name}:${spring.instance.instance-id:${random.value}}
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka

${spring.instance.instance-id} 값이 존재하면 → 존재값을 사용
값이 없으면 → ${random.value}로 랜덤한 값 생성

복제한 인스턴스에 설정한 포트번호를 지우고 실행을 시킨다.


Second Service

  • 의존성 추가
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
  • application.yml 클라이언트에 대한 포트 설정
server:
  port: 0

spring:
  application:
    name: 2nd-service

eureka:
  instance:
    # instance-id를 "서비스 이름:랜덤 값" 형태로 설정
    instance-id: ${spring.application.name}:${spring.instance.instance-id:${random.value}}
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka
  • Application @EnableDiscoveryClient 등록 (Second Service)
@SpringBootApplication
@EnableDiscoveryClient
public class [SecondServiceApplication] {

    public static void main(String[] args) {
        SpringApplication.run([SecondServiceApplication].class, args);
    }
}
  • 통신이 잘 되는지 테스트를 위한 Controller 생성
@RestController
public class SecondController {

    private final Environment environment;

    public SecondController(Environment environment){
        this.environment = environment;
    }

    @GetMapping("/health")
    public String health(){
        return "나는 잘 살아있어 내 포트는 " + environment.getProperty("local.server.port");
    }
}


API Gateway & Load Balancer

API Gateway와 Load Balancer를 Client 형태로 구성하고, 단일 진입점을 설정하여 클라이언트 요청을 처리한다.


Gateway는 예전 기술로 동기처리에 특화, Servlet 대상으로 움직인다.
Reactive Gateway 비동기 관련 특화, webflux 기술 사용

  • 의존성 확인
dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
  • application.yml 클라이언트에 대한 포트 설정
server:
  # 단일 진입점으로 만들 것이라 포트를 고정한다.
  port: 8000

spring:
  application:
    name: gateway-server
  # api gateway 서버는 client 들을 라우팅 핵심
  # 핵심 기술인 라우팅 추가
  cloud:
    gateway:
      routes:
        - id: 1st-service
          # lb: load balance의 약자
          # gateway는 load balancer에게 인스턴스를 요청
          # 인스턴스의 설정한 이름을 작성한다.
          uri: lb://1ST-SERVICE
          # 요청경로 설정, predicates : id로 찾아 갈 수 있게 설정한다.
          predicates:
            - Path=/first-service/**
            
eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8761/eureka
      
  • Application @EnableDiscoveryClient 등록 (Second Service)
@SpringBootApplication
@EnableDiscoveryClient
public class [GatewayOrLoadBalancerApplication] {

    public static void main(String[] args) {
        SpringApplication.run([GatewayOrLoadBalancerApplication].class, args);
    }
}


Gateway 서버가 8000 포트로 고정된 모습을 확인할 수 있다.

  • application.yml 클라이언트에 대한 포트 설정 - filter 설정, Second-service 생성
server:
  # 단일 진입점으로 만들 것이라 포트를 고정한다.
  port: 8000

spring:
  application:
    name: gateway-server
  # api gateway 서버는 client 들을 라우팅 핵심
  # 핵심 기술인 라우팅 추가
  cloud:
    gateway:
      routes:
        - id: 1st-service
          # lb: load balance의 약자
          # gateway는 load balancer에게 인스턴스를 요청
          # 인스턴스의 설정한 이름을 작성한다.
          uri: lb://1ST-SERVICE
          # 요청경로 설정, predicates : id로 찾아 갈 수 있게 설정한다.
          predicates:
            - Path=/first-service/**
          filters:
            - RewritePath=/first-service/(?<segment>.*), /$\{segment}

        - id: 2nd-service
          uri: lb://2ND-SERVICE
          predicates:
            - Path=/second-service/**
          filters:
            - RewritePath=/second-service/(?<segment>.*), /$\{segment}

eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8761/eureka


진입점은 포트번호 하나로 설정되며 id 값으로 어떤 service로 이동할지 결정한다.

[서비스의 YML]
spring.application.name: 1st-service


[Eureka 등록 이름]
1st-service (Eureka 서버에 등록)


[Gateway YML - uri]
uri: lb://1ST-SERVICE ◀️ 대소문자를 구분하지 않는다.

  • (Gateway YML - id는 단순히 Gateway 안에서 라우팅 규칙 구분용)

참조

프로젝트 진행 시 모놀로식으로 진행
-> 모니터링 후 MSA로 변환

  • 프로메테우스, 그라파나, Jmeter

NETFLIX에는 Ribbon, Zuul 존재

  • 과거에는 MSA를 구축할 때 Netflix OSS에서 제공하는 Ribbon, Zuul 같은 라이브러리를 사용해서 직접 구성해야 했음
  • Spring 버전이 올라가고 Spring Cloud가 등장하면서 Ribbon, Zuul을 Spring Cloud 모듈 안에 통합하여 더 쉽게 사용할 수 있게 됨
  • Ribbon -> Load Balancer 대체
  • Zuul -> API Gateway 대체

API Gateway의 라우팅과 Load Balancer의 차이

  • API Gateway: 어떤 서비스로 갈지 결정
  • Load Balancer: 그 서비스 안에서 어떤 인스턴스로 갈지 결정
profile
잔디 속 새싹 하나

0개의 댓글