본문 바로가기
푸닥거리

Spring Cloud를 활용한 MSA 설치 및 구성-2

by ┌(  ̄∇ ̄)┘™ 2022. 7. 24.
728x90
spring.application.name=Service04
server.port=8084

 

이 글은 Spring Cloud로 마이크로서비스 아키텍처(MSA)를 직접 구성한 실습 기록의 2부다. Eureka 서비스 디스커버리, API Gateway 라우팅, 로드밸런서 연동을 코드로 따라간다.

1. MSA vs SOA, 그리고 모니터링 도구

Spring Cloud를 활용한 MSA 설치 및 구성-2

 

 

 

마이크로서비스 기반 아키텍처(MSA) VS 서비스 지향 아키텍처(SOA)

 

* Microservices3Monitoring 도구
• Hystrix 대시보드
• 유레카(Eureka) 관리자 대시보드
• 스프링 부트 관리자 대시보드

 

2. API Gateway 라우팅 설정

Spring Cloud Gateway로 경로(Path) 기반 라우팅을 구성합니다. /hello/**는 8086 서비스로, /goodbye/**는 8087 서비스로 전달됩니다.

server:
  port: 9000

spring:
  cloud:
    gateway:
      routes:
      - id: helloModule
        uri: http://localhost:8086/
        predicates:
        - Path=/hello/**
      - id: goodbyeModule
        uri: http://localhost:8087/
        predicates:
        - Path=/goodbye/**

 

 

 

http://localhost:9000/hello
-> http://localhost:8086/hello

http://localhost:9000/goodbye
-> http://localhost:8087/goodbye

Gateway Server http://localhost:9000/hello or goodbye

Service #1 http://localhost:8086
Service #1 http://localhost:8087


ApiGateway 9001 - gateway, eurekaclient 
Loadbalancer 9000 - loadbalancer, eurekaclient
EurekaServer 8090 - eurekaserver
Service01 8081 - eurekaclient, spring mvc
Service02 8082 - eurekaclient, spring mvc

 

 

 

4. 서비스 모델과 컨트롤러

각 마이크로서비스가 반환할 Member 모델과 REST 컨트롤러입니다.

package com.test.model;

public class Member {
private String id;
private String name;
}

 

package com.test.model;

public class Member {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private String name;
}

 

 

 

 

 

package com.test.controllers;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.test.model.Member;

@RestController
public class MemberController {
	@RequestMapping(value="/member", method=RequestMethod.GET)
	public Member getInfo() {
		Member m = new Member();
		m.setId("hong");
		m.setName("홍길동");
		return m;
	}
}

 

 

5. pom.xml — 의존성 구성

Spring Boot 2.7 + Spring Cloud 2021.0.3 기반입니다. Eureka 클라이언트/설정 서버 의존성은 필요에 따라 주석을 해제합니다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.2</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.test</groupId>
	<artifactId>Service01</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>Service01</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
		<spring-cloud.version>2021.0.3</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- 
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-config</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		 -->
		 
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

 

 

6. Eureka 서버와 클라이언트 설정

서비스 디스커버리를 담당하는 Eureka 서버(8090)와, 여기에 자신을 등록하는 각 서비스의 설정입니다. 서버는 자기 자신을 등록/조회하지 않도록 register-with-eureka=false로 둡니다.

spring.application.name=EurekaServer
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
server.port=8090

 

 

 

package com.test;

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

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

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

}

spring.application.name=Service01
server.port=8081
eureka.client.service-url.defaultZone=http://localhost:8090/eureka

 

 

 

spring.application.name=EurekaServer
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
server.port=8090
eureka.client.service-url.defaultZone=http://localhost:8090/eureka/


http://localhost:8080/member1
http://localhost:8080/member2
http://localhost:8090
http://localhost:8081/member
http://localhost:8082/member2

 

 

 

 

7. 클라이언트 사이드 로드밸런싱

LoadBalancerClient.choose("SERVICE")로 Eureka에 등록된 인스턴스 중 하나를 골라 호출합니다. 같은 서비스명(SERVICE01/02)이 여러 개면 요청이 분산됩니다. RestTemplate으로 실제 호출까지 이어지는 과정을 단계별로 보여줍니다.

package com.test.controllers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.stereotype.Controller;

@Controller
public class ConsumerControllerClient {
	@Autowired
	private LoadBalancerClient loadBalancer;
	public void getHello() {
		ServiceInstance serviceInstance = loadBalancer.choose("SERVICE");
		System.out.println(serviceInstance.getUri());
		String baseUrl = serviceInstance.getUri().toString();
		baseUrl = baseUrl = "/member";
		// http://localhost:9000/member
		// -> http://localhost:8081/member -> SERVICE01
		// -> http://loaclhost:8082/member -> SERVICE02
	}
}

package com.test.controllers;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.stereotype.Controller;
import org.springframework.web.client.RestTemplate;

@Controller
public class ConsumerControllerClient {
	@Autowired
	private LoadBalancerClient loadBalancer;
	public void getHello() {
		ServiceInstance serviceInstance = loadBalancer.choose("SERVICE");
		System.out.println(serviceInstance.getUri());
		String baseUrl = serviceInstance.getUri().toString();
		baseUrl = baseUrl = "/member";
		// http://localhost:9000/member
		// -> http://localhost:8081/member -> SERVICE01
		// -> http://loaclhost:8082/member -> SERVICE02
		RestTemplate restTemplate = new RestTemplate();
	}
}
package com.test.controllers;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.client.RestTemplate;

@Controller
public class ConsumerControllerClient {
	@Autowired
	private LoadBalancerClient loadBalancer;
	public void getMember() {
		ServiceInstance serviceInstance = loadBalancer.choose("SERVICE");
		System.out.println(serviceInstance.getUri());
		String baseUrl = serviceInstance.getUri().toString();
		baseUrl = baseUrl = "/member";
		// http://localhost:9000/member
		// -> http://localhost:8081/member -> SERVICE01
		// -> http://loaclhost:8082/member -> SERVICE02
		RestTemplate restTemplate = new RestTemplate(); 

		ResponseEntity<String> response=null;
		try{
			response=restTemplate.exchange(baseUrl,  HttpMethod.GET, getHeaders(),String.class);
		} catch (Exception ex) {
			System.out.println(ex);
		}
		System.out.println(response.getBody());
	}

	private static HttpEntity<?> getHeaders() throws IOException {
		HttpHeaders headers = new HttpHeaders();
		headers.set("Accept", MediaType.APPLICATION_JSON_VALUE);
		return new HttpEntity<>(headers);
	}
}
package com.test;

import java.io.IOException;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.ApplicationContext;
import org.springframework.web.client.RestClientException;

import com.test.controllers.ConsumerControllerClient;

@SpringBootApplication
public class LoadbalancerApplication {

	public static void main(String[] args) throws RestClientException, IOException {

		ApplicationContext ctx = SpringApplication.run(LoadbalancerApplication.class, args);
		
		ConsumerControllerClient consumerControllerClient=ctx.getBean(ConsumerControllerClient.class);
		System.out.println(consumerControllerClient);
		for(int inx = 0; inx <= 10; inx++) {
			consumerControllerClient.getMember();
		}
	}

}

 

 

 

 

 

 

Service
/member

 

spring.application.name=LoadBalancer1
server.port=9000
eureka.client.serviceUrl.defaultZone=http://localhost:8090/eureka
package com.test.controllers;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.client.RestTemplate;

@Controller
public class ConsumerControllerClient {
	@Autowired
	private LoadBalancerClient loadBalancer;
	public void getMember() {
		ServiceInstance serviceInstance = loadBalancer.choose("SERVICE");
		System.out.println(serviceInstance.getUri());
		String baseUrl = serviceInstance.getUri().toString();
		baseUrl = baseUrl = "/member";
		// http://localhost:9000/member
		// -> http://localhost:8081/member -> SERVICE01
		// -> http://loaclhost:8082/member -> SERVICE02
		RestTemplate restTemplate = new RestTemplate(); 

		ResponseEntity<String> response=null;
		try{
			response=restTemplate.exchange(baseUrl,  HttpMethod.GET, getHeaders(),String.class);
		} catch (Exception ex) {
			System.out.println(ex);
		}
		System.out.println(response.getBody());
	}

	private static HttpEntity<?> getHeaders() throws IOException {
		HttpHeaders headers = new HttpHeaders();
		headers.set("Accept", MediaType.APPLICATION_JSON_VALUE);
		return new HttpEntity<>(headers);
	}
}
package com.test;

import java.io.IOException;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.ApplicationContext;
import org.springframework.web.client.RestClientException;

import com.test.controllers.ConsumerControllerClient;

@SpringBootApplication
public class LoadbalancerApplication {

	public static void main(String[] args) throws RestClientException, IOException {

		ApplicationContext ctx = SpringApplication.run(LoadbalancerApplication.class, args);
		
		ConsumerControllerClient consumerControllerClient=ctx.getBean(ConsumerControllerClient.class);
		System.out.println(consumerControllerClient);
		for(int inx = 0; inx <= 10; inx++) {
			consumerControllerClient.getMember();
		}
	}

}

//EurekaServer 미사용 코드 쿠현
package com.test;

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;

import java.util.Arrays;
import java.util.List;

@SpringBootApplication
public class LoadBalancerClientApplication {

	public static void main(String[] args) {
//		SpringApplication.run(LoadBalancerClientApplication.class, args);
		
		ConfigurableApplicationContext ctx = new SpringApplicationBuilder(LoadBalancerClientApplication.class)
                .web(WebApplicationType.NONE)
                .run(args);

        WebClient loadBalancedClient = ctx.getBean(WebClient.Builder.class).build();

        for(int i = 1; i <= 10; i++) {
            String response =
                loadBalancedClient.get().uri("http://example-service/hello")
                    .retrieve().toEntity(String.class)
                    .block().getBody();
            System.out.println(response);
        }
    }
}
@Configuration
class DemoServerInstanceConfiguration {
    @Bean
    ServiceInstanceListSupplier serviceInstanceListSupplier() {
        return new DemoInstanceSupplier("example-service");
    }
}

@Configuration
@LoadBalancerClient(name = "example-service", configuration = DemoServerInstanceConfiguration.class)
class WebClientConfig {
    @LoadBalanced
    @Bean
    WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}

class DemoInstanceSupplier implements ServiceInstanceListSupplier {
    private final String serviceId;

    public DemoInstanceSupplier(String serviceId) {
        this.serviceId = serviceId;
    }

    @Override
    public String getServiceId() {
        return serviceId;
    }

    @Override
    public Flux<List<ServiceInstance>> get() {
        return Flux.just(Arrays
            .asList(new DefaultServiceInstance(serviceId + "1", serviceId, "localhost", 8082, false),
                    new DefaultServiceInstance(serviceId + "2", serviceId, "localhost", 8083, false)));
    }
}

 

EurekaServer and Ribbon

 

 

 

 

spring.application.name=Service04
server.port=8084
package com.test.controllers;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.test.model.Member;

@RestController
public class MemberController {
	@RequestMapping(value="/goodbye", method=RequestMethod.GET)
	public Member getInfo() {
		Member m = new Member();
		m.setId("lee");
		m.setName("이순신");
		return m;
	}
}

 

 

 

server:
  port: 9000

spring:
  cloud:
    gateway:
      routes:
      - id: helloModule
        uri: http://localhost:8083/
        predicates:
        - Path=/hello/**
      - id: goodbye
        uri: http://localhost:8084/
        predicates:
        - Path=/goodbye/**

 

 

 

spring cloud config server github

spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/spring-cloud-samples/config-repo

https://github.com/spring-cloud/spring-cloud-config

 

GitHub - spring-cloud/spring-cloud-config: External configuration (server and client) for Spring Cloud

External configuration (server and client) for Spring Cloud - GitHub - spring-cloud/spring-cloud-config: External configuration (server and client) for Spring Cloud

github.com

 

 

server:
 port: 8888
spring:
 cloud:
  config:
   server:
    git:
     uri: https://github.com/formin/MySpringClud.git

 

server:
 port: 8089
spring:
 application:
  name: MyConfigClient
 cloud:
  config:
   name: myconfig
   profile: test
 config:
  import:
   configserver:http://localhost:8888

package com.test;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class MyConfigClient1Application {

	public static void main(String[] args) {
		SpringApplication.run(MyConfigClient1Application.class, args);
	}
	
	@Value("${message.text:default}")
	private String messageValue;
	
	@RequestMapping(value="/")
	public String getMessageValue() {
		return messageValue;
	}
}

java -jar jenkins.war

 

 

 

 

 

https://www.amazon.com/-/ko/dp/1617294543/ref=sr_1_4?crid=L81WLGU5KNN3&keywords=microservices&qid=1658649531&sprefix=microservices%2Caps%2C244&sr=8-4 

 

Amazon.com

Enter the characters you see below Sorry, we just need to make sure you're not a robot. For best results, please make sure your browser is accepting cookies.

www.amazon.com

 

 

 

https://www.amazon.com/-/ko/dp/1680506455/ref=sr_1_12?crid=L81WLGU5KNN3&keywords=microservices&qid=1658649573&sprefix=microservices%2Caps%2C244&sr=8-12 

 

Amazon.com

Enter the characters you see below Sorry, we just need to make sure you're not a robot. For best results, please make sure your browser is accepting cookies.

www.amazon.com

 

https://www.amazon.com/-/ko/dp/1801072973/ref=sr_1_1?crid=2KVRM4K5KHJSA&keywords=spring+cloud+microservice&qid=1658649630&sprefix=spring+cloud+microservic%2Caps%2C229&sr=8-1 

 

Amazon.com

Enter the characters you see below Sorry, we just need to make sure you're not a robot. For best results, please make sure your browser is accepting cookies.

www.amazon.com

 

https://www.amazon.com/-/ko/dp/1789613477/ref=sr_1_2?crid=2KVRM4K5KHJSA&keywords=spring+cloud+microservice&qid=1658649671&sprefix=spring+cloud+microservic%2Caps%2C229&sr=8-2 

 

Amazon.com

Enter the characters you see below Sorry, we just need to make sure you're not a robot. For best results, please make sure your browser is accepting cookies.

www.amazon.com

 

https://www.amazon.com/s?k=microservices&crid=L81WLGU5KNN3&sprefix=microservices%2Caps%2C244&ref=nb_sb_noss_1 

 

 

https://www.amazon.com/s?k=microservices&crid=L81WLGU5KNN3&sprefix=microservices%2Caps%2C244&ref=nb_sb_noss_1 

 

 

https://www.amazon.com/-/ko/dp/1449374646/ref=sr_1_10?crid=1G3KQESD8AX8I&keywords=cloud+native&qid=1658649736&sprefix=cloud+nativ%2Caps%2C231&sr=8-10 

 

Amazon.com

Enter the characters you see below Sorry, we just need to make sure you're not a robot. For best results, please make sure your browser is accepting cookies.

www.amazon.com

 

 

 

마치며

정리하면 이번 2부에서는 Eureka(서비스 등록/발견) → Gateway(경로 라우팅) → LoadBalancer(인스턴스 분산 호출)로 이어지는 MSA의 핵심 삼각형을 코드로 구성했습니다. 각 조각이 독립 실행되면서도 Eureka를 통해 서로를 찾는다는 점이 모놀리식과의 결정적 차이입니다. 현대적으로는 Netflix OSS(Eureka/Ribbon) 대신 Spring Cloud LoadBalancer와 Kubernetes 기반 서비스 디스커버리로 대체되는 추세지만, 여기서 다룬 개념(등록·발견·라우팅·분산)은 그대로 유효합니다.

728x90

댓글