4.1 클라이언트 사이드 로드 밸런싱 개요

4.1.1 로드 밸런싱이란?

  • 로드 밸런싱은 네트워크 트래픽을 여러 서버로 분산시켜 서버의 부하를 줄이고, 시스템의 성능과 가용성을 높이는 기술
  • 서버 간 트래픽을 고르게 분배하여 특정 서버에 부하가 집중되는 것을 방지
  • 종류: 클라이언트 사이드 로드 밸런싱, 서버 사이드 로드 밸런싱

4.1.2 클라이언트 사이드 로드 밸런싱이란?

  • 클라이언트 사이드 로드 밸런싱은 클라이언트가 직접 여러 서버 중 하나를 선택하여 요청을 보내는 방식
  • 클라이언트는 서버의 목록을 가지고 있으며, 이를 바탕으로 로드 밸런싱을 수행

 

4.5 로드 밸런싱 알고리즘

4.5.1 라운드 로빈

  • 라운드 로빈: 각 서버에 순차적으로 요청을 분배하는 방식
  • 간단하고 공평하게 트래픽을 분산

4.5.2 가중치 기반 로드 밸런싱

  • 가중치 기반 로드 밸런싱: 각 서버에 가중치를 부여하고, 가중치에 비례하여 요청을 분배하는 방식
  • 서버의 성능이나 네트워크 상태에 따라 가중치를 조절

4.5.3 기타 알고리즘

  • 최소 연결: 현재 연결된 클라이언트 수가 가장 적은 서버로 요청을 보내는 방식
  • 응답 시간 기반: 서버의 응답 시간을 기준으로 가장 빠른 서버로 요청을 보내는 방식

'Java Spring 군 > MSA' 카테고리의 다른 글

MSA 적용하기  (1) 2024.11.27

https://spring.io/microservices

 

Spring | Microservices

Microservices with Spring BootWith Spring Boot, your microservices can start small and iterate fast. That’s why it has become the de facto standard for Java™ microservices. Quickstart your project with Spring Initializr and then package as a JAR. With

spring.io

https://spring.io/cloud

 

Spring | Cloud

We found that the performance of [Spring Cloud Gateway] was very appealing. Low latency, good throughput, [and] a very small percentage of timeouts in our use cases. CHRIS JACKSON, SENIOR DEVELOPER, TD AMERITRADE

spring.io

MSA를 실현시키기 위해 Spring에서 제공하는 framework?

1. Spring Cloud

  • 정의
    • 마이크로 서비스 개발을 위해 다양한 도구와 서비스를 제공하는 스프링 프레임워크의 확장
    • MSA를 쉽게 구현하고 운영할 수 있도록 도움
  • 주요 기능
    • 서비스 등록 및 디스커버리
    • 로드 밸런싱
    • 서킷 브레이커
    • API 게이트웨이
    • 구성 관리
    • 분산 추적
    • 메시징
  • Spring Cloud 주요 모듈
    • Eureka [ 서비스 등록 및 디스커버리]
      • 디스커버리 서버 - 각 서비스 위치(호스트와 포트)를 동적으로 관리
      • 서비스 레지스트리 : 모든 서비스 인스턴스의 위치를 저장하는 중앙 저장소
      • 헬스 체크(Health Check) : 서비스 인스턴스의 상태를 주기적으로 확인하여 가용성 보장
    • Ribbon [로드 밸런싱]
      • 클라이언트 사이드 로드 밸런서 - 서비스 인스턴스 간의 부하를 분산시킴
      • 서버 리스트 제공자 : Eureka로부터 서비스 인스턴스 리스트를 제공받아 로드 밸런싱에 사용
      • 로드 밸런싱 알고리즘 지원
      • 요청 실패 시 다른 인스턴스로 자동 전환시킴
    • Resilience4j [서킷 브레이커], , (Hystrix) 
      • 자바 기반의 경량 서킷 브레이커 라이브러리, 넷플릭스 Hystrix의 대안으로 개발
      • 서킷 브레이커 : 호출 실패를 감지하고 서킷을 열어 추가적인 호출을 차단하여 시스템의 부하를 줄임
      • 호출 실패 시 대체 로직을 실행 => 시스템의 안정성 유지
      • 호출에 대한 타임아웃 설정 가능
      • 재시도 기능 제공 -> 일시적인 네트워크 문제 등에 대응
  • Spring Cloud 구성 요소의 활용????
    • Cloud Gateway [API 게이트웨이], (Zuul <- spring boot2에서 사용)
      • 스프링 클라우드에서 제공하는 API게이트웨이
      • 루팅 및 필터링 : 요청을 받아 특정 서비스로 라우팅하고 필요한 인증 및 권한 부여를 수행
      • 외부 요청으로부터 애플리케이션 보호. 보안 정책을 적용
      • 필요한 요청 처리 및 분산 환경의 관리를 효율적으로 수행
    • Spring Cloud Config [구성관리]
      • 중앙 집중식 설정 관리를 제공
      • Config 서버 : 중앙에서 설정파일 관리 및 각 서비스에 제공
      • Config 클라이언트 : Config 서버에 관리를 받는 서비스
      • 설정 변경 시 서비스 재시작 없이 실시간으로 반영

2. Eureka 사용하기 [서비스 디스커버리]

  • 서비스 디스커버리
    • MSA에서 각 서비스의 위치를 동적으로 관리하고 찾아주는 기능
    • 각 서비스는 등록 서버에 자신의 위치를 등록하고, 다른 서비스는 이를 조회하여 통신
    • 서비스 등록, 서비스 조회, 헬스 체크 등의 기능이 있다
  • Eureka 사용하기

1) Eureka 서버 설정 [ 서비스 레지스트리 역할 등록 ]

build.gradle 에 아래 의존성을 추가해야 한다

implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
spring:
  application:
    name: eureka-server

server:
  port: 190090
  
eureka:
  client:// 클라이언트로써의 역할 / 기본적으로 유레카서버도 자신을 레지스트리에 등록시키기 때문에 작성해야 함
    register-with-eureka: false// 다른 eureka 서버에 이 서버를 등록하지 않음(자신이 서버여서 가져올 필요 없음) 
    					// -> 유레카 서버에 자신을 등록할지 여부 설정
    fetch-registry: false// 다른 eureka 서버의 레지스트리를 가져오지 않음(자신이 서버여서 가져올 필요 없음)
    					// eureka 서버로부터 다른 서비스 인스턴스 목록을 가져오는지 여부
    service-url:
      defaultZone: http://localhost:190090/eureka// 자신의 호스트주소 설정
  server:// 서버로써의 역할
    enable-self-preservation: false// 자기 보호모드 비활성화
  instance:
    hostname: localhost// 호스트 이름 설정
    
//logging:
//  level:
//    com.netflix.eureka: OFF
//    com.netflix.discovery: OFF
// 이 서비스가 EurekaServer임을 선언

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

@EnableEurekaServer
@SpringBootApplication
public class ServerApplication {

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

}

 

2) Eureka 클라이언트 설정 [ 서비스 등록 ]

build.gradle 에 아래 의존성을 추가해야 한다

implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'

application.yml에 아래 설정을 추가해야 한다

spring:
  application:
    name: service-name // 애플리케이션 이름이 있어야 eureka-server에서 인식하고 등록할 수 있다
server:
  port: 190091
  
// 여기까지만으로도 유레카 서버에 등록됨--------------------------------------------------

eureka:
  client:
    service-url:
      defaultZone: http://localhost:190091/eureka/// 유레카 서버 URL
    register-with-eureka: true// Eureka 서버에 등록
    fetch-registry: true// Eureka 서버로부터 레지스트리 정보 가져오기
  instance:
    hostname: localhost// 호스트 이름 설정
    prefer-ip-address: true// IP 주소 사용 선호
    lease-renewal-interval-in-seconds: 30// lease 갱신 간격
    lease-expiration-duration-in-seconds: 90// lease 만료 기간

 

3) Eureka 클라이언트 설정 [ 조회할 서비스 요청 준비 ]

Eureka 서버에서 필요한 서비스의 위치를 조회해야 한다.(RestTemplate 사용하는 방법, FeignClient를 사용하는 방법)

        1_ RestTemplate을 사용하는 경우

// @SpringBootApplication이 있는 곳에 @Bean으로 등록한 RestTemplate에 
// @LoadBalanced를 붙여 로드밸런싱 기능 추가한다.

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

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
//사용할 Controller에 서비스url을 restTemplate로 불러와서 사용한다.

@RestController
public class MyRestTemplateController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/get-data-rest")
    public String getDataWithRestTemplate() {
        String serviceUrl = "http://my-service/api/data";	//my-service 위치가 애플리케이션 이름
        return restTemplate.getForObject(serviceUrl, String.class);
    }
}

        2_ FeignClient를 사용하는 경우

// FeignClient를 사용하용하도록 표시해야 한다.

@SpringBootApplication
@EnableFeignClients
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
// FeignClient에 필요한 서비스 이름을 추가한다
// RestTemplate 방식에서 serviceUrl이 여기서 인터페이스로 만들어진다.
// 사용하는 url은 상대 서비스에 보낼 것으로 작성하면 됨

@FeignClient(name = "my-service")	// 애플리케이션 이름
public interface MyServiceClient {

    @GetMapping("/api/data")
    String getData();
}
// FeignClient를 선언하고 상대 서비스를 데이터를 조회해 온다

@RestController
public class MyFeignClientController {

    @Autowired
    private MyServiceClient myServiceClient;

    @GetMapping("/get-data-feign")
    public String getDataWithFeignClient() {
        return myServiceClient.getData();	//api가 아닌 메서드로 
    }
}

         3_ DiscoveryClient와 RestClient를 활용하는 방법

package com.example.serviceb.controller;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;

@RestController
public class ServiceBRestController {

	private final DiscoveryClient discoveryClient;
	private final RestClient restClient;

	public ServiceBRestController(DiscoveryClient discoveryClient, RestClient.Builder restClientBuilder) {
		this.discoveryClient = discoveryClient;
		restClient = restClientBuilder.build();
	}

	@GetMapping("helloEureka")
	public String helloWorld() {
		ServiceInstance serviceInstance = discoveryClient.getInstances("servicea").get(0);
        // 인스턴스명이 "servicea"인 인스턴스 중 처음 것을 가져옴
		String serviceAResponse = restClient.get()
				.uri(serviceInstance.getUri() + "/helloWorld")
				.retrieve()
				.body(String.class);
		return serviceAResponse;
	}
}

 

4) Eureka 서버에서 헬스 체크 및 장애 처리

 - 헬스 체크 : 주기적으로 서비스 인스턴스 상태를 확인하여 가용성 확인

 - 장애 처리 : 서비스 장애 시 해당 인스턴스를 레지스트리에서 제거하여 해당 서비스로의 다른 서비스 접근을 차단한다

 

5) Eureka 클라이언트 설정 [ Eureka의 고가용성 구성 ] - 클러스터 구성(역할이 같다는 것으로 등록)

// 유레카 클라이언트를 여러 유레카 서버에 등록

eureka:
  client:
    service-url:
      defaultZone: http://eureka-peer1:8761/eureka/,http://eureka-peer2:8761/eureka/
      	// 유레카 서버 URL을 여러 개로 구성

 

유레카 설정값

https://docs.spring.io/spring-cloud-netflix/reference/configprops.html

 

Configuration Properties :: Spring Cloud Netflix

If this flag is set to {@code true}, {@code ServiceInstanceListSupplier#get(Request request)} method will be implemented to call {@code delegate.get(request)} in classes assignable from {@code DelegatingServiceInstanceListSupplier} that don’t already imp

docs.spring.io

 

 

3. Ribbon 사용하기 [ 클라이언트 사이드 로드 밸런싱(FeignClient와 Ribbon) ]

 

로드 밸런싱

 

kimyhg.tistory.com

  • FeignClient
    • FeignClient는 Spring Cloud에서 제공하는 HTTP클라이언트,
      선언적으로 RESTful 웹 서비스를 호출할 수 있다.
    • Eureka와 같은 서비스 디스커버리와 연동하여
      동적으로 서비스 인스턴스를 조회하고 로드 밸런싱을 수행
    • 주요 특징
      1. 선언적 HTTP 클라이언트 : 
        인터페이스와 어노테이션을 사용하여 REST API를 호출할 수 있다.
      2. Eureka 연동 : Eureka 서버에서 인스턴스 목록을 가져와 로드 밸런싱을 수행
      3. 자동 로드 밸런싱 : Ribbon이 통합되어 있어 자동으로 로드 밸런싱을 수행
  • Ribbon
    • 클라이언트 사이드 로드 밸런서
      MSA에서 서비스 인스턴스 간의 부하를 분산시켜 줌
    • 다양한 로드 밸런싱 알고리즘 지원, 서비스 디스커버리와 연동하여 사용
    • 주요 특징
      1. 다양한 로드 밸런싱 알고리즘 지원
      2. Failover : 요청 실패 시 다른 인스턴스로 자동 전환

1) Eureka클라이언트 설정 [ FeignClient와 Ribbon 설정 ]

build.gradle 에 의존성을 추가한다.

implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'	//유레카 클라이언트
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'		//FeignClient 추가
//FeignClient 선언적 사용

@SpringBootApplication
@EnableFeignClients
public class MyServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyServiceApplication.class, args);
    }
}
// FeingClient 선언적 사용 : interface로 상대 서비스 인스턴스 선언
// Controller와 같이 선언

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = "request-service")
public interface MyServiceClient {

    @GetMapping("/endpoint")
    String getResponse(@RequestParam(name = "param") String param);
}
// FeignClient를 선언하고 상대 서비스를 데이터를 조회 구현

@RestController
@RequiredArgsConstructor
public class MyFeignClientController {

    private MyServiceClient myServiceClient;

    @GetMapping("/get-data-feign")
    public String callService(@RequestParam String param) {
        return myServiceClient.getResponse(param);
    }
}
spring:
  application:
    name: service-name // 애플리케이션 이름이 있어야 eureka-server에서 인식하고 등록할 수 있다
server:
  port: 190091

eureka:
  client:
    service-url:
      defaultZone: http://localhost:190091/eureka/// 유레카 서버 URL
    register-with-eureka: true// Eureka 서버에 등록
    fetch-registry: true// Eureka 서버로부터 레지스트리 정보 가져오기
  instance:
    hostname: localhost// 호스트 이름 설정
    prefer-ip-address: true// IP 주소 사용 선호
    lease-renewal-interval-in-seconds: 30// lease 갱신 간격
    lease-expiration-duration-in-seconds: 90// lease 만료 기간
    
request-service:
  ribbon:
    eureka:
      enabled: true//FeignClient에서 제공하는 서비스 인스턴스를 사용하여 로드 밸런싱을 수행

2) 요청받을 서비스 인스턴스 여러 개 등록(Intellij IDE 한정) - Ribbon에서 로드 밸런싱 알고리즘에 맞게 자동 호출

  • 위 사진에서 옵션 수정 클릭 - VM옵션 추가 -  "-Dserver.port=19093(실행할 포트)"  입력

 

  • FeignClient와 Ribbon 동작 원리
    1. @FeignClient(name="request-service") 어노테이션은 Eureka에 등록된 서비스 이름을 참조
    2. 서비스 인스턴스 조회
    3. 로드 밸런싱(Ribbon) : 서비스 인스턴스 선택 및 요청 분배

 

4.Resilience4j 사용하기 [ 서킷 브레이커 ]

  • 프로젝트 생성 - 생성시 dependencies에 resilience4j는 추가하지 않는다

  •  기본 설정(build.gradle)
    resilience4j를 spring starter에서 추가하지 않고 gradle dependecy에서 추가한다.
    ( spring starter에서 제공하는 dependency는 구현체가 아니어서 github의 resilience4j로 추가한다.)
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter-aop'


//Resilience4j Dashboard 사용하기
implementation 'io.github.resilience4j:resilience4j-micrometer'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

 

 

아래는 Resilience4j 를 spring boot에 적용하는 튜토리얼 페이지이다.

https://resilience4j.readme.io/docs/getting-started-3

 

Getting Started

Getting started with resilience4j-spring-boot2 or resilience4j-spring-boot3

resilience4j.readme.io

 

  • 기본 설정(application.yml
// Spring Cloud와 Resilience4j 연동 - Eureka와 Ribbon 등 다른 Spring Cloud 구성요소와 쉽게 통합
spring:
  application:
    name: my-service
  cloud:
    circuitbreaker:
      resilience4j:
        enabled: true
server:
  port: 19090


// Resilience4j 설정
resilience4j:
  circuitbreaker:
    configs:
      default:  # 기본 구성 이름
        registerHealthIndicator: true  # 애플리케이션의 헬스 체크에 서킷 브레이커 상태를 추가하여 모니터링 가능
        # 서킷 브레이커가 동작할 때 사용할 슬라이딩 윈도우의 타입을 설정
        # COUNT_BASED: 마지막 N번의 호출 결과를 기반으로 상태를 결정
        # TIME_BASED: 마지막 N초 동안의 호출 결과를 기반으로 상태를 결정
        slidingWindowType: COUNT_BASED  # 슬라이딩 윈도우의 타입을 호출 수 기반(COUNT_BASED)으로 설정
        # 슬라이딩 윈도우의 크기를 설정
        # COUNT_BASED일 경우: 최근 N번의 호출을 저장
        # TIME_BASED일 경우: 최근 N초 동안의 호출을 저장
        slidingWindowSize: 5  # 슬라이딩 윈도우의 크기를 5번의 호출로 설정
        minimumNumberOfCalls: 5  # 서킷 브레이커가 동작하기 위해 필요한 최소한의 호출 수를 5로 설정
        slowCallRateThreshold: 100  # 느린 호출의 비율이 이 임계값(100%)을 초과하면 서킷 브레이커가 동작
        slowCallDurationThreshold: 60000  # 느린 호출의 기준 시간(밀리초)으로, 60초 이상 걸리면 느린 호출로 간주
        failureRateThreshold: 50  # 실패율이 이 임계값(50%)을 초과하면 서킷 브레이커가 동작
        permittedNumberOfCallsInHalfOpenState: 3  # 서킷 브레이커가 Half-open 상태에서 허용하는 최대 호출 수를 3으로 설정
        # 서킷 브레이커가 Open 상태에서 Half-open 상태로 전환되기 전에 기다리는 시간
        waitDurationInOpenState: 20s  # Open 상태에서 Half-open 상태로 전환되기 전에 대기하는 시간을 20초로 설정
        
        
// Resilience4j Dashboard를 위한 설정 - http://${hostname}:${port}/actuator/prometheus 에 접속하여 서킷브레이커 항목을 확인 가능
management:
  endpoints:
    web:
      exposure:
        include: prometheus
  prometheus:
    metrics:
      export:
        enabled: true

 

  • Fallback 적용
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class ProductService {

    private final Logger log = LoggerFactory.getLogger(getClass());
    private final CircuitBreakerRegistry circuitBreakerRegistry;

    @PostConstruct
    public void registerEventListener() {
        circuitBreakerRegistry.circuitBreaker("productService").getEventPublisher()
            .onStateTransition(event -> log.info("#######CircuitBreaker State Transition: {}", event)) // 상태 전환 이벤트 리스너
            .onFailureRateExceeded(event -> log.info("#######CircuitBreaker Failure Rate Exceeded: {}", event)) // 실패율 초과 이벤트 리스너
            .onCallNotPermitted(event -> log.info("#######CircuitBreaker Call Not Permitted: {}", event)) // 호출 차단 이벤트 리스너
            .onError(event -> log.info("#######CircuitBreaker Error: {}", event)); // 오류 발생 이벤트 리스너
    }


    @CircuitBreaker(name = "productService", fallbackMethod = "fallbackGetProductDetails")
    public Product getProductDetails(String productId) {
        log.info("###Fetching product details for productId: {}", productId);
        if ("111".equals(productId)) {
            log.warn("###Received empty body for productId: {}", productId);
            throw new RuntimeException("Empty response body");
        }
        return new Product(
            productId,
            "Sample Product"
        );
    }

    public Product fallbackGetProductDetails(String productId, Throwable t) {
        log.error("####Fallback triggered for productId: {} due to: {}", productId, t.getMessage());
        return new Product(
            productId,
            "Fallback Product"
        );
    }

    
    // 이벤트 설명 표
    // +---------------------------+-------------------------------------------------+--------------------------------------------+
    // | 이벤트                      | 설명                                             | 로그 출력                                    |
    // +---------------------------+-------------------------------------------------+--------------------------------------------+
    // | 상태 전환 (Closed -> Open)   | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환되면 발생  | CircuitBreaker State Transition: ...       |
    // | 실패율 초과                  | 설정된 실패율 임계치를 초과하면 발생                     | CircuitBreaker Failure Rate Exceeded: ...  |
    // | 호출 차단                    | 서킷 브레이커가 오픈 상태일 때 호출이 차단되면 발생         | CircuitBreaker Call Not Permitted: ...     |
    // | 오류 발생                    | 서킷 브레이커 내부에서 호출이 실패하면 발생               | CircuitBreaker Error: ...                  |
    // +---------------------------+-------------------------------------------------+--------------------------------------------+


    // +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
    // | 이벤트                                    | 설명                                        | 로그 출력                                                         |
    // +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
    // | 메서드 호출                                | 제품 정보를 얻기 위해 메서드를 호출                | ###Fetching product details for productId: ...                  |
    // | (성공 시) 서킷 브레이커 내부에서 호출 성공        | 메서드 호출이 성공하여 정상적인 응답을 반환          |                                                                 |
    // | (실패 시) 서킷 브레이커 내부에서 호출 실패        | 메서드 호출이 실패하여 예외가 발생                 | #######CircuitBreaker Error: ...                                |
    // | (실패 시) 실패 횟수 증가                      | 서킷 브레이커가 실패 횟수를 증가시킴               |                                                                 |
    // | (실패율 초과 시) 실패율 초과                   | 설정된 실패율 임계치를 초과하면 발생               | #######CircuitBreaker Failure Rate Exceeded: ...                |
    // | (실패율 초과 시) 상태 전환 (Closed -> Open)   | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환됨   | #######CircuitBreaker State Transition: Closed -> Open at ...  |
    // | (오픈 상태 시) 호출 차단                      | 서킷 브레이커가 오픈 상태일 때 호출이 차단됨         | #######CircuitBreaker Call Not Permitted: ...                   |
    // | (오픈 상태 시) 폴백 메서드 호출                 | 메서드 호출이 차단될 경우 폴백 메서드 호출          | ####Fallback triggered for productId: ... due to: ...           |
    // +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+


   
}

 

 

5.Spring Cloud Gateway 사용하기 [ API 게이트웨이 ]

  • 초기 프로젝트 생성( Reative Gateway로 가져오기)

  • build.gradle 설정
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
  • application.yml 설정
server:
  port: 19091  # 게이트웨이 서비스가 실행될 포트 번호

spring:
  main:
    web-application-type: reactive  # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
  application:
    name: gateway-service  # 애플리케이션 이름을 'gateway-service'로 설정
  cloud:
    gateway:
      routes:  # Spring Cloud Gateway의 라우팅 설정
        - id: order-service  # 라우트 식별자
          uri: lb://order-service  # 'order-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/order/**  # /order/** 경로로 들어오는 요청을 이 라우트로 처리
        - id: product-service  # 라우트 식별자
          uri: lb://product-service  # 'product-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/product/**  # /product/** 경로로 들어오는 요청을 이 라우트로 처리
      discovery:
        locator:
          enabled: true  # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정

eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/  # Eureka 서버의 URL을 지정
  • 필터 작성
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.logging.Logger;

@Component
public class CustomPreFilter implements GlobalFilter, Ordered {

    private static final Logger logger = Logger.getLogger(CustomPreFilter.class.getName());

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest response = exchange.getRequest();
        logger.info("Pre Filter: Request URI is " + response.getURI());
        // Add any custom logic here

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.logging.Logger;

@Component
public class CustomPostFilter implements GlobalFilter, Ordered {

    private static final Logger logger = Logger.getLogger(CustomPostFilter.class.getName());

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();
            logger.info("Post Filter: Response status code is " + response.getStatusCode());
            // Add any custom logic here
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

https://spring.io/guides/gs/gateway

 

Getting Started | Building a Gateway

As a good developer, we should write some tests to make sure our Gateway is doing what we expect it should. In most cases, we want to limit our dependencies on outside resources, especially in unit tests, so we should not depend on HTTPBin. One solution to

spring.io


5-1.Zuul 사용하기 [ Spring Boot 2 - API 게이트웨이 ]

  • build.gradle 설정
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'

 

  • Spring Boot 애플리케이션 설정
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}
  • application.yml 설정
zuul:
  routes:
    users-service:
      path: /users/**
      serviceId: users-service
    orders-service:
      path: /orders/**
      serviceId: orders-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  • 필터 설정
@Component
public class PreFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        // 요청 로깅
        System.out.println(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
        return null;
    }
}

 

 

6.OAuth2 + JWT 사용하기 [ 보안 구성 ]

  • Auth 서비스 생성

  • build.gradle 설정 - jwt dependencies 추가
implementation 'io.jsonwebtoken:jjwt:0.12.6'
  • application.yml 설정
spring:
  application:
    name: auth-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

service:
  jwt:
    access-expiration: 3600000
    secret-key: 	#key값을 임의의 값에 대한 base64로 암호화한 값으로 추가

server:
  port: 19095
  • AuthConfig.java 생성
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class AuthConfig {

    // SecurityFilterChain 빈을 정의합니다. 이 메서드는 Spring Security의 보안 필터 체인을 구성합니다.
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // CSRF 보호를 비활성화합니다. CSRF 보호는 주로 브라우저 클라이언트를 대상으로 하는 공격을 방지하기 위해 사용됩니다.
            .csrf(csrf -> csrf.disable())
            // 요청에 대한 접근 권한을 설정합니다.
            .authorizeRequests(authorize -> authorize
                // /auth/signIn 경로에 대한 접근을 허용합니다. 이 경로는 인증 없이 접근할 수 있습니다.
                .requestMatchers("/auth/signIn").permitAll()
                // 그 외의 모든 요청은 인증이 필요합니다.
                .anyRequest().authenticated()
            )
            // 세션 관리 정책을 정의합니다. 여기서는 세션을 사용하지 않도록 STATELESS로 설정합니다.
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );

        // 설정된 보안 필터 체인을 반환합니다.
        return http.build();
    }
}
  • AuthoService.java 생성
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.util.Date;

@Service
public class AuthService {

    @Value("${spring.application.name}")
    private String issuer;

    @Value("${service.jwt.access-expiration}")
    private Long accessExpiration;

    private final SecretKey secretKey;

    /**
     * AuthService 생성자.
     * Base64 URL 인코딩된 비밀 키를 디코딩하여 HMAC-SHA 알고리즘에 적합한 SecretKey 객체를 생성합니다.
     *
     * @param secretKey Base64 URL 인코딩된 비밀 키
     */
    public AuthService(@Value("${service.jwt.secret-key}") String secretKey) {
        this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
    }

    /**
     * 사용자 ID를 받아 JWT 액세스 토큰을 생성합니다.
     *
     * @param user_id 사용자 ID
     * @return 생성된 JWT 액세스 토큰
     */
    public String createAccessToken(String user_id) {
        return Jwts.builder()
                // 사용자 ID를 클레임으로 설정
                .claim("user_id", user_id)
                .claim("role", "ADMIN")
                // JWT 발행자를 설정
                .issuer(issuer)
                // JWT 발행 시간을 현재 시간으로 설정
                .issuedAt(new Date(System.currentTimeMillis()))
                // JWT 만료 시간을 설정
                .expiration(new Date(System.currentTimeMillis() + accessExpiration))
                // SecretKey를 사용하여 HMAC-SHA512 알고리즘으로 서명
                .signWith(secretKey, io.jsonwebtoken.SignatureAlgorithm.HS512)
                // JWT 문자열로 컴팩트하게 변환
                .compact();
    }
}
  • AuthController.java 생성
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    /**
     * 사용자 ID를 받아 JWT 액세스 토큰을 생성하여 응답합니다.
     *
     * @param user_id 사용자 ID
     * @return JWT 액세스 토큰을 포함한 AuthResponse 객체를 반환합니다.
     */
    @GetMapping("/auth/signIn")
    public ResponseEntity<?> createAuthenticationToken(@RequestParam String user_id){
        return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
    }

    /**
     * JWT 액세스 토큰을 포함하는 응답 객체입니다.
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class AuthResponse {
        private String access_token;

    }
}

6-1.Gateway 추가 [ 보안 구성 ] - 직접 접근하는 곳인 게이트웨이에서 Filter로 추가

  • build.gradle 설정 추가
implementation 'io.jsonwebtoken:jjwt:0.12.6'
  • application.yml 설정 추가
server:
  port: 19091  # 게이트웨이 서비스가 실행될 포트 번호

spring:
  main:
    web-application-type: reactive  # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
  application:
    name: gateway-service  # 애플리케이션 이름을 'gateway-service'로 설정
  cloud:
    gateway:
      routes:  # Spring Cloud Gateway의 라우팅 설정
        - id: auth-service  # 라우트 식별자
          uri: lb://auth-service  # 'auth-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/auth/signIn  # /auth/signIn 경로로 들어오는 요청을 이 라우트로 처리
      discovery:
        locator:
          enabled: true  # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
          
service:
  jwt:
    secret-key: 	#key값을 임의의 값에 대한 base64로 암호화한 값으로 추가
  • 인증 Filter 추가
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.crypto.SecretKey;
@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {

    @Value("${service.jwt.secret-key}")
    private String secretKey;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();
        if (path.equals("/auth/signIn")) {
            return chain.filter(exchange);  // /signIn 경로는 필터를 적용하지 않음
        }

        String token = extractToken(exchange);

        if (token == null || !validateToken(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);
    }

    private String extractToken(ServerWebExchange exchange) {
        String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        return null;
    }

    private boolean validateToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
            Jws<Claims> claimsJws = Jwts.parser()
                    .verifyWith(key)
                    .build().parseSignedClaims(token);
            log.info("#####payload :: " + claimsJws.getPayload().toString());

            // 추가적인 검증 로직 (예: 토큰 만료 여부 확인 등)을 여기에 추가할 수 있습니다.
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

 

7. Spring Cloud Config 사용하기 [ Config 서버 구성]

6-1) Config 서비스 구성

  • 서비스 생성

  • build.gradle 의존성 추가
implementation 'org.springframework.cloud:spring-cloud-config-server'
implementation 'org.springframework.boot:spring-boot-starter-web'
  • 시작점에 환경서버 기능 어노테이션 추가
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
  • Config 서버 application.yml 설정
server:
  port: 18080

spring:
  profiles:
    active: native
  application:
    name: config-server
  cloud:
    config:
      server:
        #git:
        #  uri: https://github.com/my-config-repo/config-repo
        #  clone-on-start: true
        native:
          search-locations: classpath:/config-repo  # 리소스 폴더의 디렉토리 경로
          
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/
  • resources 안에 config-repo 폴더 생성
  • 아래 두 파일을 생성

 - product-service.yml

server:
  port: 19093

message: "product-service message"

 - product-service-local.yml

server:
  port: 19083

message: "product-service-local message"

 

7-2) 설정받을 서비스 설정

  • build.gradle 설정 추가
implementation 'org.springframework.cloud:spring-cloud-starter-config'
  • application.yml 설정 추가
spring:
  application:
    name: my-config-client
  profiles:
    active: dev
  config:
    import: "configserver:"
  cloud:
    config:
      discovery:
        enabled: true
        service-id: config-server
     
management:
  endpoints:
    web:
      exposure:
        include: refresh

message: "default message"

eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/
  • Test하기
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * @RefreshScope 애노테이션은 Spring 애플리케이션의 빈이 설정 변경을 반영할 수 있도록 하는 역할을 합니다.
 * 기본적으로 Spring 애플리케이션의 빈은 애플리케이션이 시작될 때 초기화되고, 설정 값이 변경되더라도 해당 빈은 갱신되지 않습니다.
 * 이 애노테이션을 사용하면 /actuator/refresh 엔드포인트를 호출하여 설정 변경 사항을 동적으로 반영할 수 있습니다.
 */
@RefreshScope
@RestController
@RequestMapping("/product")
public class ProductController {

    @Value("${server.port}") // 애플리케이션이 실행 중인 포트를 주입받습니다.
    private String serverPort;

    @Value("${message}")
    private String message;

    @GetMapping
    public String getProduct() {
        return "Product detail from PORT : " + serverPort + " and message : " + this.message ;
    }
}

 

7-3) 설정 반영하기 [ 수동으로 /actuator/refresh 엔드포인트를 호출하는 방법 ]

  • Config 서버에서 변경할 클라이언트 설정 파일 변경
  • 클라이언트 애플리케이션으로 /actuator/refresh/ 을 요청

 

8. Spring Cloud Config 사용하기 [ Config 서버 구성]

'Java Spring 군 > MSA' 카테고리의 다른 글

로드 밸런싱 - 수정중  (1) 2024.11.28

+ Recent posts