Java

[MSA] e-commerce API; 구현1 - 설정(docker, config-server, eureka-server, gateway)

ride-dev 2024. 6. 18. 20:55

[MSA] e-commerce API; 프로젝트 흐름

[MSA] e-commerce API; 구현1 - 설정(config-server, eureka-server, gateway)

[MSA] e-commerce API; 구현2 - customer, product, payment, order, notification

먼저, 최상위 디렉토리를 생성합니다.

관리할 항목들에 대한 디렉토리를 생성하고, docker-compose.yml을 작성합니다.

0. docker-compose.yml

주요 항목은 4 가지 입니다.

services, network, volumes, secrets

services는 postgres, mongodb, kafka 등의 서비스를 담당합니다.

networks는 각 컨테이너의 네트워크 설정을 담당합니다.

volumes는 컨테이너 저장공간을,

sercrets는 컨테이너 설정 시 비밀번호와 같은 보안 파일을 담당합니다.

secrets에서 선언하고 services의 environment에서 사용합니다.

secrets는 _FILE변수에 사용합니다.

(구체적인 예시는 아래를 참조합니다)

각 컨테이너는 네트워크가 격리되어 있습니다.

따라서 networks 설정을 통해 하나의 네트워크로 연결하여 상호작용하도록 합니다.

 

services:
  postgres:
    container_name: ms_pg_sql
    image: postgres
    ports:
      - "5432:5432"
    secrets:
      - postgres_user
      - postgres_password
    environment:
      - POSTGRES_USER_FILE=/run/secrets/postgres_user
      - POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password
      - PGDATA=/var/lib/postgresql/data
    volumes:
      - postgres:/var/lib/postgresql/data
    networks:
      - microservices-net
    restart: unless-stopped

  mongodb:
    container_name: ms_mongo_db
    image: mongo
    ports:
      - "27017:27017"
    secrets:
      - mongo_username
      - mongo_password
    environment:
      - MONGO_INITDB_ROOT_USERNAME_FILE=/run/secrets/mongo_username
      - MONGO_INITDB_ROOT_PASSWORD_FILE=/run/secrets/mongo_password
    volumes:
      - mongo:/data

  mongo-express:
    container_name: ms_mongo_express
    image: mongo-express
    ports:
      - "8081:8081"
    secrets:
      - mongo_admin_username
      - mongo_admin_password
    environment:
      - ME_CONFIG_MONGODB_ENABLE_ADMIN=true
      - ME_CONFIG_MONGODB_ADMINUSERNAME_FILE=/run/secrets/mongo_admin_username
      - ME_CONFIG_MONGODB_ADMINPASSWORD_FILE=/run/secrets/mongo_admin_password
      - ME_CONFIG_MONGODB_SERVER=mongodb
    restart: unless-stopped

  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      - ZOOKEEPER_SERVER_ID=1
      - ZOOKEEPER_CLIENT_PORT=2181
      - ZOOKEEPER_TICK_TIME=2000
    networks:
      - microservices-net

  kafka:
    image: confluentinc/cp-kafka:latest
    container_name: ms_kafka
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1
      - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1
      - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR=1
      - KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181
      - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
    networks:
      - microservices-net

  mail-dev:
    container_name: ms_mail_dev
    image: maildev/maildev
    ports:
      - "1080:1080"
      - "1025:1025"

  zipkin:
    container_name: ms_zipkin
    image: openzipkin/zipkin
    ports:
      - "9411:9411"
    networks:
      - microservices-net

networks:
  microservices-net:
    driver: bridge

volumes:
  postgres:
  mongo:

secrets:
  postgres_user:
    file: ./secrets/postgres_user.txt
  postgres_password:
    file: ./secrets/postgres_password.txt
  mongo_username:
    file: ./secrets/mongo_username.txt
  mongo_password:
    file: ./secrets/mongo_password.txt
  mongo_admin_username:
    file: ./secrets/mongo_admin_username.txt
  mongo_admin_password:
    file: ./secrets/mongo_admin_password.txt

(이 외 부분의 최적화는 최적화 게시글에서 다루겠습니다)

1. Config Server

Config Server에서는 Spring Cloud 프로젝트의 구성관리를 담당합니다.

Eureka에 등록할 서비스의 yml 파일을 관리합니다.

필요한 의존성은 아래와 같습니다.

implementation 'org.springframework.cloud:spring-cloud-config-server'

ConfigServer의 스프링 컨테이너가 가동될 main클래스에,

@EnableConfigServer 어노테이션을 선언합니다.

package com.ride.configserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {

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

}

resources에 configurations 디렉토리를 생성하여,

공통 구성파일(application.yml), 각 서비스별 구성 파일을 작성하고 관리합니다.

config-server의 application.yml은 아래와 같습니다.

server:
  port: 8888
spring:
  profiles:
    active: native
  application:
    name: config-server
  cloud:
    config:
      server:
        native:
          search-locations: classpath:/configurations

server.port 을 통해 config-server가 수신할 포트를 지정합니다.

spring.profiles.active 를 native로 설정하여,

로컬 파일 시스템에서 다른 설정 파일(각 클라이언트 별 yml, properties 파일)을 읽어올 수 있도록 합니다.

spring.cloud.config.server.native.search-locations 설정을 통해,

config-server가 configurations디렉토리에서 설정 파일을 읽어오도록 합니다.

 

configurations의 공통 구성관리 파일 application.yml은 아래와 같습니다.

eureka:
  instance:
    hostname: localhost
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
spring:
  cloud:
    config:
      override-system-properties: false

management:
  tracing:
    sampling:
      probability: 1.0

eureka.instance.hostname 을 통해 Eureka 서버에 등록하고 관리할 hostname을 설정합니다.

Eureka 클라이언트(각 서비스)는 이렇게 설정된 hostname를 통해 Eureka 서버에 접근합니다.

eureka.client.service-url.defaultZone 설정을 통해,

Eureka 클라이언트(각 서비스)가 Eureka 서버에 접근하기 위해 Eureka Server 의 기본 URL을 설정하고,

클라이언트(각 서비스)가 Eureka 서버에 등록됩니다.

(Eureka Client 의존성이 있는 서비스만 등록할 수 있습니다)

등록된 서비스들은 http://localhost:8761/eureka 에서 관리됩니다.

spring.cloud.config.override-system-properties 설정을 통해,

Spring Cloud Config가 시스템 프로퍼티를 덮어쓰지 않도록 합니다.

https://cloud.spring.io/spring-cloud-static/spring-cloud-commons/2.2.2.RELEASE/reference/html/#overriding-bootstrap-properties

management.tracing.sampling.probability 는 분산 추적에 대한 설정입니다.

0.0에서 1.0까지 설정할 수 있으며, 요청 중 몇 퍼센트를 추적할지 설정합니다.

개발 환경에서는 샘플링 확률을 높게 설정하고,

운영 환경에서는 낮게 설정하여 성능에 미치는 영향을 최소화할 수 있습니다.

저는 개발 환경이기 때문에 1.0, 모든 요청을 샘플링하도록 설정했습니다.

 

추후에 다른 서비스를 구현하면,

그 서비스의 구성관리 파일을 config-server의 configurations 디렉토리에 생성합니다.

2. Eureka(Discovery) Server

Eureka Server는 Config Server의 설정 정보를 바탕으로 Eureka Client를 관리합니다.

주요 의존성은 아래와 같습니다.

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-config'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'

Config Server와 마찬가지로 main 클래스에 어노테이션을 추가하여 Eureka Server 임을 알림니다.

package com.ride.discovery;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryApplication {

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

}

Eureka Server (다른 방식으로 표현하면), discovery service 의 application.yml을 작성합니다.

spring:
  application:
    name: discovery-service
  config:
    import: optional:configserver:http://localhost:8888

spring.config.import 설정을 통해 외부 서버(config-server)로부터 설정을 가져옵니다.

optional: 을 통해 configserver에 문제가 있어도 애플리케이션이 동작되도록 합니다.

configserver 를 통해 Spring Cloud Config 서버에서 설정을 가져도록 합니다.

8888은 config-server의 port입니다.

이는 discovery의 설정파일을 config-server로부터 가져오겠다는 것을 의미합니다.

 

spring.applciation.name을 discovery-service로 설정하여 다른 서비스에서 식별할 수 있도록 합니다.

그리하여 config-server의 configurations에 생성하는 yml 파일의 이름을 discovery-service로 합니다.

아래는 config-server에 생성한 discovery-service.yml입니다.

server:
  port: 8761
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}/${server.port}/eureka/

server.port 를 통해 Eureka 애플리케이션의 port를 8761로 설정합니다.

eureka.instance.hostname 설정을 통해 로컬 호스트에서 실행되도록 합니다.

eureka.client.registerWithEureka 를 통해 Eureka에 등록할지 설정합니다.

eureka.client.fetchRegistry 를 통해, 이 애플리케이션이 Eureka 서버로부터 레지스트리를 가져올지 설정합니다.

Eureka 서버 자체의 설정에서 주로 위 두 옵션은 false로 합니다.

eureka.client.serviceUrl.defaultZone 을 통해 Eureka 클라이언트가 Eureka 서버에 접근하기 위한 URL을 설정합니다.

이제 다른 클라이언트 애플리케이션이 http://localhost:8761/eureka/ URL을 통해 Eureka 서버에 접근할 수 있습니다.

3. Gateway

gateway가 요청을 받으면, 각 서비스에 알맞게 리디렉션합니다.

각 서비스의 부하를 고려하여 로드밸런싱을 적용할 수 있습니다.

 

아래는 gateway의 의존성입니다.

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-config'
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-tracing-bridge-brave'
    implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

위부터 차례로 gateway-service가 config client, gateway, eureka client 의존성을 사용할 것임을 알 수 있습니다.

Actuator, brave 의존성은 ZIPIN(분산 추적)에 대한 것입니다.

oauth2 resource server 의존성은 KeyCloak을 사용하기 위한 의존성입니다.

나중에 따로 다루도록 하겠습니다.

 

gateway의 구성 관리파일은 아래와 같습니다.

spring:
  application:
    name: gateway-service
  config:
    import: optional:configserver:http://localhost:8888
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: "http://localhost:9098/realms/micro-service"

서비스 식별자를 지정하고, configserver로 부터 설정을 가져옵니다.

security 설정은 나중에 KeyCloak과 함께 다루겠습니다.

 

config-server에 gateway의 설정파일을 gateway-service.yml이라는 이름으로 생성합니다.

port를 8222로 설정했습니다.

이제 네트워크 내부의 다른 서비스 또한 gateway의 8222포트로 접근할 수 있습니다.

server:
  port: 8222
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: customer-service
          uri: lb:http://CUSTOMER-SERVICE # eureka service
          predicates:
            - Path=/api/v1/customers/**
        - id: order-service
          uri: lb:http://ORDER-SERVICE # eureka service
          predicates:
            - Path=/api/v1/orders/**
        - id: order-lines-service
          uri: lb:http://ORDER-SERVICE # eureka service
          predicates:
            - Path=/api/v1/order-lines/**
        - id: product-service
          uri: lb:http://PRODUCT-SERVICE # eureka service
          predicates:
            - Path=/api/v1/products/**
        - id: payment-service
          uri: lb:http://PAYMENT-SERVICE # eureka service
          predicates:
            - Path=/api/v1/payments/**

spring.cloud.gateway.discovery.locator.enabled 설정을 통해,

gateway가  Eureka 서비스 레지스트리에서 경로를 동적으로 찾도록 합니다.

만약 false로 한다면, 아래에 routes에서 선언할 uri를 명시적으로 설정합니다.

spring.cloud.gateway.routes 에서 개별 서비스에 대한 라우팅 규칙을 정의합니다.

id 는 각 route의 식별자입니다.

uri 는 요청을 전달할 대상 서비스의 URI입니다.

lb를 통해 로드 밸런싱을 가능하게 합니다.

predicates 특정 조건에 따라 route를 적용할지를 결정하는 조건자(Path, Method, Header 등)를 정의합니다.

 

lb를 통해 로드 밸런싱을 가능하게 했습니다.

각 프로젝트별로 다른 포트(8070, 8071, 8072)를 사용하되

spring.application.name을 동일하게 한다면(order-service),

gateway는 라운드 로빈 형식으로 요청을 로드 밸런싱(부하 분산)합니다.

(트래픽을 균등하게 배포합니다)

(최적화 게시글에서 따로 다루겠습니다)

728x90