스프링에서 MSA 모식도
각각의 경로에 대해 비즈니스 로직을 각각의 스프링 부트 서버로 띄울 것이다.
특정 주소로 오면 경로를 따라 게이트 웨이가 특정 컨트롤러를 가진 서버에 데이터를 전달한다.
여러개의 비즈니스 로직에 대한 url 요청을 받는 것이 아닌 한개의 url 요청을 게이트 웨이가 받아서 해당 요청을 나누어 주는 게이트웨이가 있는 것이다. 이렇게 게이트웨이와 스프링부트 어플리케이션 만으로도 시스템을 구축할 수 있다.
유레카 서버는 각각의 스프링부트 어플리케이션이나 게이트웨이가 활성화된 상태로 떠 있는지 어떤 정보를 가지고 있는지 모니터링 할 수 있는 서버다. 등록되어 있는 모든 서버 정보들이 뜬다. 뜨게 하기 위해선 원하는 서버를 등록해주어야한다. 등록된 서비스가 유레카 클라이언트라고 불린다. 유레카 클라이언트들이 등록되면 유레카 서버에 뜨게된다. application.yml 같은 변수파일에는 직접 명시하는 것이 아닌 변수를 제공하게 할 수 있는 서버가 존재한다.이것이 스프링 클라우드에서는 스프링 컨피그 서버라 부르고 이 서버에서는 특정 경로에 데이터들을 요청하면 이 데이터들은 client들에게 전달해준다.
Spring Cloud Gateway는 스케일 아웃이 자동으로 이루어진다. 트래픽이 많이 발생하면 자동 스케일 아웃이 되는데 그때마다 새로운 서버의 ip주소를 스프링 클라우드 게이트웨이에 알려주는 역할이 스프링 클라우드 유레카 서버가 진행하게 된다.
Spring Cloud Gateway : SCG
- URL 주소에 대해서 아래 세부 경로에 따라 각각의 스프링 부트 어플리케이션에 분배하는 분배기 역할을 수행
Spring Cloud Eureka Server
- 모니터링 서버로 Eureka Client 설정을 해둔 서버를 Eureka Server에 띄워 줌
- 모니터링 기능과 함께 추가적으로 Spring Cloud Gateway에 목록을 전달하여 Gateway가 로드밸런싱 대상을 설정하도록 작업
Spring Cloud Eureka Client
- Eureka Server에 등록되는 요소로 스프링 부트 어플리케이션과 같은 여러 스프링 프레임워크 서버에 설정이 가능하다.
Spring Config Server
- 변수 값들을 제공하는 서버로 특정 경로로 접근하면 미리 사전에 설정해둔 변수 값들을 제공 받을 수 있다.
- MSA를 구축하면 각각의 스프링 부트 어플리케이션에 application.properties에 값을 명시하는 것이 아닌 config server로 부터 데이터를 받아서 사용한다.
- 자기가 변수값을 가지고 있는 것이 아닌 단순 매개체로서 앞단의 스프링 부트 어플리케이션이 시작되며 변수를 요청하면 컨피그 서버는 서빙 역할만 한다.
- 보통 깃허브 리포지토리나 데이터베이스의 특정 테이블과 같은 저장소(스프링 리포지토리)를 만들어 실제 값들이 저장된다.
Config Repository
- Config Server는 단순하게 데이터를 전달하는 매개체로 실제 데이터는 Config Server 뒷단에 깃허브 리포지토리와 같은 저장소를 물려서 사용한다.
- Config 영속성의 종류
- Git Service
- RDB
- Document NoSQL
- Redis
- File
- Vault
- 여러 영속성 도구 중 Git Service를 가장 많이 사용한다.
- Spring Config Client
- Config Server로 부터 변수 데이터를 받기 위한 Client 서버 설정
Config Server
깃허브 레파지토리 생성
repository 내부에 설정 파일을 저장할 수 있는 파일을 생성
그리고 config server가 접속할 수 있게 하기 위해 키를 생성해야한다.
비대칭 키를 생성해서 public key는 깃허브에 private key는 서버에서 데이터를 가지고 오는데 증명을 하기 위해 사용할 것이다.
C:\Users\qkrwl\.ssh
ssh-keygen -m PEM -t rsa -b 4096 -C "
public key 내용을 저장하고 setting에서 key를 등록해준다.
프로젝트 생성 및 의존성 추가
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2023.0.3")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.cloud:spring-cloud-config-server'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
어노테이션 등록
@SpringBootApplication
@EnableConfigServer
public class ConfigServerEcommerceApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerEcommerceApplication.class, args);
}
}
Config 저장소 연결
server:
port: 9000
spring:
cloud:
config:
server:
git:
uri: git@github.com:je-pa/sample.git
ignoreLocalSshSettings: true
private-key: |
-----BEGIN RSA PRIVATE KEY-----
MIIJKQI...
-----END RSA PRIVATE KEY-----
Config 서버 시큐리티 설정
config 서버가 외부로부터 설정되지 못하게 막아야함.
package com.configserverecommerce.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf((auth) -> auth.disable());
http
.authorizeHttpRequests((auth) -> auth.anyRequest().authenticated());
http
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user1 = User.builder()
.username("admin")
.password(bCryptPasswordEncoder().encode("1234"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user1);
}
}
접근 주소
- 예시: http://localhost:9000/ms1/dev
Eureka 서버 구축
모니터링 및 스프링 클라우드 게이트웨이에게 서버 주소를 전달해줄 유레카 서버를 구축하자.
의존성
- security
- eureka-server
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2023.0.3")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
어노테이션
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerEcommerceApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerEcommerceApplication.class, args);
}
}
application 설정
server.port=8761
eureka.client.register-with-eureka=false #
eureka.client.fetch-registry=false # 유레카 서버 역할을 할 어플리케이션임을 뜻함
유레카 서버에 라이브러리를 가지고 있으면 유레카 클라이언트의 역할도 할 수 있다.
유레카 서버가 유레카 클라이언트로 등록될 수 있는게 기본 값(true)이다.
즉 기본 설정은 다른 마이크로 서비스들이 찾아서 추출하는 역할의 마이크로 서비스가 된다는 뜻인데, 유레카 서버는 이와 같은 동작의 의미가 없기 때문에 클라이언트로 등록되지 않도록 false 로 변경해준다.
Eureka client
유레카 서버에 클라이언트들을 등록해주자. MSA를 구성하는 요소들 중 Eureka 서버에서 모니터링 및 관리를 원하는 요소(ex. spring application, config server)를 Eureka 클라이언트 설정을 진행해서 등록할 수 있다.
의존성
- Eureka Discovery Client
ext {
set('springCloudVersion', "2022.0.4")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
어노테이션 등록
@EnableDiscoveryClient
설정
server.port=8080
spring.application.name=ms1
eureka:
client:
register-with-eureka: true #유레카 서버에 등록할지 여부
fetch-registry: true #유레카 서버의 정보를 가져올지 여부
service-url.defaultZone: http://아이디:비밀번호@아이피:8761/eureka #유레카 서버 주소
다른 포트 띄우기
동일한 api를 별도의 다른 포트를 통해 등록시키려면 아래처럼 설정하여 실행시켜 확인해보면 된다.
Spring Cloud Gateway
스프링 클라우드 게이트웨이는 MSA 가장 앞단에서 클라이언트들로 부터 오는 요청을 받은 후 경로와 조건에 알맞은 마이크로서비스 로직에 요청을 전달하는 게이트웨이
의존성
- 게이트웨이
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2023.0.3")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
application 설정
server:
port: 8080
spring:
application:
name: scg
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: ms1
uri: lb://MS1 # Eureka에 등록된 Application instatnce 이름
predicates:
- name: Path
args:
pattern: /ms1/**
eureka:
client:
register-with-eureka: true #유레카 서버에 등록할지 여부
fetch-registry: true #유레카 서버의 정보를 가져올지 여부
service-url.defaultZone: http://admin:1234@localhost:8761/eureka #유레카 서버 주소
엑추에이터
스프링 부트 어플리케이션을 모니터링하기 위해서 사용하는 라이브러리
config repository에서 설정이 클라이언트에 자동 반영되게 하기 위해 사용해볼 것이다.
반영되게 하기 위해서 요청을 엑추에이터에게 보내야한다.
YAML에서. 지금 제가 가지고 있는 이 설정에서 액추에이터에 대한 설정을 추가해 줘야 된다.
management:
endpoints:
web:
exposure:
include: refresh
엑추에이터 관련된 설정은 management로 시작한다.
위의 include에서 스프링 부트가 가지고 있는 다양한 기능 중 어떤 것을 사용하겠다라고 지정해 줄 수 있다.
*은 다양한 기능들을 모두 사용할 수 있는 것이다. 여기서는 리프레쉬만 사용하겠다라는 설정이다.
리프레쉬가 반영될 수 있도록 플래시 스코프를 지정해 주어야 된다.
@RefreshScope
이제 컴피그 클라이언트 서버를 다시 가동하지 않고 액추에이터를 통해서 설정을 적용하려면, 요청을 하면 된다.
컴피그 클라이언트의 포트 번호와 액추에이터의 /actuator/refresh라는 URL을 POST 형태로 전송하면, 이 컴피그 클라이언트한테 '리프레시 하라'는 뜻이다.
라우팅 추가
스프링 클라우드 게이트웨이는 퍼블릭 엔드포인트에서 게이트웨이 역할을 수행한다. 서비스를 시작한 후 멈추면 서비스를 접근할 수 있는 경로가 없어지기 때문에 항상 가동 상태여야 한다.
항상 가동 상태로 유지되어야 하는 게이트웨이 특성상 서버 스크립트를 중지 후 코드를 수정하고 재배포하면 서비스의 실시간성을 보장하지 못한다.
스프링 클라우드 게이트웨이의 경우 가동 중 새로운 비즈니스 로직(경로)이 추가될 경우 해당 주소에 대한 라우팅을 즉시 추가하고 삭제할 수 있는 여러 기능을 제공한다.
게이트웨이를 멈추지 않고 라우팅을 추가하는 방법에 대해 알아보자.
엑추에이터를 통한 라우팅 추가 방법
Actuator: 스프링 어플리케이션의 기능을 엔드 포인트로 제공하는 의존성
Actuator를 활용하여 가동중인 스프링 클라우드 게이트웨이에 새로운 라우팅을 추가하고 삭제할 수 있다.
프로젝트 생성과 의존성 추가
- Gateway
- Spring Boot Actuator
Actuator 설정
- application
management.endpoint.gateway.enabled=true # 엑추에이터에서 특정 엔드포인트를 사용할 것인지
management.endpoints.web.exposure.include=gateway # 엑추에이터가 어떤 항목을 사용할 것인지 health등
라우팅 명령어
- 존재하는 라우팅 확인
GET : /actuator/gateway/routesPLAINTEXT
- 라우터 추가
POST : /actuator/gateway/routes/{id}PLAINTEXT
POST Body에 JSON 타입으로 데이터를 추가해야 한다.
- 리프래시
POST : /actuator/gateway/refreshPLAINTEXT
- 라우터 제거
DELETE : /actuator/gateway/routes/{id}PLAINTEXT
- 특정 라우터 확인
GET : /actuator/gateway/routes/{id}PLAINTEXT
- 글로벌 필터 목록
GET : /actuator/gateway/globalfiltersPLAINTEXT
- 특정 라우터 필터 목록
GET : /actuator/gateway/routefilters/{id}PLAINTEXT
라우팅 추가 실습
- 현재 라우팅 목록 확인
GET : /actuator/gateway/routesPLAINTEXT
- 라우팅 추가
POST : /actuator/gateway/routes/{이름}PLAINTEXT
- 라우팅 JSON Body
{
"predicate": "Paths: [/ms1/**]",
"filters": [],
"uri": "http://localhost:8081",
"order": 0
}
- 추가 후 refresh
POST : /actuator/gateway/refresh
참고로 /actuator를 모두 접근할 수 있으면 누구든지 해당 라우트를 제어할 수 있게 되기 때문에 security 설정이 추가적으로 필요하다.
게이트웨이 필터
요청 전달 전 게이트웨이에서 로깅, jwt 검증, 특정 ip에 대한 ben등의 작업을 한다. 이런 작업들을 하는 것이 필터다. 필터에는 글로벌 필터, 지역 필터로 두가지가 있다. 글로벌 필터의 경우 필터 등록만 하면 모든 서비스를 거치기 전에 필터를 거친 상태로 서비스에 도달하게 된다.
글로벌 필터
스프링 클라우드 게이트웨이에서 글로벌 필터는 모든 라우팅에 대해서 적용되는 필터이다. 따라서 필터만 구현하면 특별한 설정 없이 적용된다.
클라이언트는 해당 필터를 요청할 때 받을 때 2번 거친다. 클라이언트의 요청은 필터 → 마이크로서비스 → 필터 형태로 이동되며 같은 필터라도 마이크로서비스를 접근하기 이전이면 pre, 이후면 post라고 명명한다.
각각의 필터는 Order 값을 가질 수 있으며 pre 필터의 경우 Order 값이 작을수록 빠르게 동작하며, post 필터의 경우 Order 값이 작을수록 늦게 동작한다.
글로벌 필터 작성
package com.example.scg.component;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class G1Filter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("pre global filter order -1");
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
System.out.println("post global filter order -1");
}));
}
@Override
public int getOrder() {
return -1;
}
}
- Order 설정시
- 각각의 마이크로서비스에 요청을 전달하는 라우팅 테이블이 Order 0으로 설정된 경우가 많기 때문에 필터의 경우 음수 설정이 지향된다.
지역 필터
지역 필터: 특정 마이크로서비스 라우팅에 대해서만 동작을 진행하는 필터
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
public class L1Filter extends AbstractGatewayFilterFactory<L1Filter.Config> {
public L1Filter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
if (config.isPre()) {
System.out.println("pre local filter 1");
}
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
if (config.isPost()) {
System.out.println("post local filter 1");
}
}));
};
}
@NoArgsConstructor
@AllArgsConstructor
@Data
public static class Config {
private boolean pre;
private boolean post;
}
}
application 설정
지역필터를 특정 라우트에 등록시켜주어야 한다.
spring.cloud.gateway.routes[0].filters[0].name=L1Filter
spring.cloud.gateway.routes[0].filters[0].args.pre=true
spring.cloud.gateway.routes[0].filters[0].args.post=true
server:
port: 8080
spring:
cloud:
gateway:
routes:
- id: ms1
uri: http://localhost:8081
predicates:
- Path=/ms1/**
filters:
- name: L1Filter
args:
pre: true
post: true
- id: ms2
uri: http://localhost:8082
predicates:
- Path=/ms2/**
@Configuration
public class RouteConfig {
@Bean
public RouteLocator ms1Route(RouteLocatorBuilder builder) {
return builder.routes()
.route("ms1", r -> r.path("/ms1/**")
.filters(f -> f.filter(L1Filter.apply(new L1Filter.Config(true, true))))
.uri("http://localhost:8081")
)
.route("ms2", r -> r.path("/ms2/**")
.uri("http://localhost:8082")
)
.build();
}
}
유레카
'spring > spring' 카테고리의 다른 글
[Spring][RabbitMQ][Exception] Message로 인터페이스 사용하기 (0) | 2024.04.29 |
---|---|
[Spring][RabbitMQ] 설정하고 실행해보기 (0) | 2024.04.22 |
[Spring][Exception] Controller Ambiguous mapping (0) | 2024.04.15 |
[Spring][스프링 부트 핵심 가이드] 스프링 시큐리티: 서비스의 인증과 권한 부여 (0) | 2024.03.13 |
[Spring][스프링 부트 핵심 가이드] 액추에이터 활용하기 (0) | 2024.03.06 |