- 정렬 알고리즘

알아보기

 

 

 

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' 카테고리의 다른 글

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

1. Gradle

빌드 자동화 시스템

실행 가능한 jar파일을 만들어준다

Java, Android, and Kotlin, C++ 등 개발자를 위한 빌드도구

 

 

2. build.gradle

  • Gradle 기반의 빌드 스크립트
  • 소스 코드를 빌드하고 라이브러리들의 의존성을 쉽게 관리
  • groovy 혹은 kotlin 언어로 작성
  • gradle을 이용해서 프로젝트를 생성하면 아래와 같이 SourceSet을 구성해준다.
  • 그렇기 때문에 gradle로 프로젝트를 생성하지 않으면 SourceSet의 구성이 달라진다.

gradle을 이용한 프로젝트 source set

 

3. Gradle Basics

< Gradle 구성>

 - Project

  빌드하고 싶은 애플리케이션이나 라이브러리

 - Build 스크립트(build.gradle)

  프로젝트를 빌드하는 단계를 적은 파일

  프로젝트 당 build 스크립트가 한 개 이상

 - Dependency Management

  프로젝트에 필요한 외부 리소스들을 선언하고 풀어주는 역할

 - Tasks

  기본 작업 단위 - 코드 컴파일, 테스트 실행

 - Plugins

 

4. build.gradle파일 구성

plugins{} :

여기에 추가한 내용은 재사용 가능한 빌드 로직을 패키지화하여 task로 제공해 빌드시 사용할 수 있도록 도와준다


Options

group = 'com.sparta' : 아티팩트의 그룹명

version = '0.0.1=SNAPSHOT' : realse될 버전

sourceCompatiblity = '17' : java 소스를 컴파일할 때 사용되는 java 버전

configurations : 

configurations {
    compileOnly {
        extendsFrom annotationProcessor // compileOnly가 dependency의 anntoationProcessor를 상속하도록 설정
    }
    
    smokeTest.extendsFrom testImplementation // smokeTest 에서도 testImplementation을 사용할 수 있다
    
    
}

dependencies {
	annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'junit:junit:4.13'
    smokeTest 'org.apache.httpcomponents:httpclient:4.5.5'
}

repositories

 

repositories{ mavenCentral() } : 프로젝트 빌드에 필요한 의존성을 다운받을 저장소. 여기서는 maven 저장소

        mavenCentral은 maven의 중앙저장소로 gradle 내장 함수 mavenCentral()을 이용해 중앙저장소에 있는 의존성들을 가져온다. // 자신의 maven reposity를 구축하여 사설 저장소로도 쓸 수 있다.(maven,ivy를 사용)

repositories{ 
	mavenCentral() 
}

# 프로젝트 빌드에 필요한 의존성을 다운받을 저장소. 여기서는 maven 저장소
# mavenCentral은 maven의 중앙저장소로 gradle 내장 함수 mavenCentral()을 이용해 
# 중앙저장소에 있는 의존성들을 가져온다. 자신의 maven reposity를 구축하여 
# 사설 저장소로도 쓸 수 있다.(maven,ivy를 사용)

 


dependecies : 

        Dependency Configuration

apiElements와 runtimeElements는 구성 요소가 라이브러리를 컴파일하거나 실행할 때 사용

- apiElements

  • 해당 라이브러리 컴파일
  • 해당 라이브러리를 컴파일하는데 필요한 모든 요소를 검색   

 - runtimeElements

  • 해당 라이브러리를 실행
  • 해당 라이브러리를 컴파일하는데 필요한 모든 요소를 검색   

compileClasspath, runtimeClasspath : 구성 요소 자체를 실행하는 데 필요한 모든 요소를 검색할 때 사용한다.

  • compileClasspath : 소스를 컴파일할 때 사용할 classPath
  • runtimeClasspath : 소스를 실행시 사용할 classPath

api, implementation, compileOnly, runtimeOnly : 사용자가 의존성을 선언할 때 사용 / 라이브러리마다 필요한 시점이 다르기 때문에 거기에 맞게 사용

  • api
    • 컴파일 타임에 필요한 라이브러리
    • 의존 라이브러리가 수정되는 경우 본 모듈을 의존하는 모든 모듈들을 재빌드
      ex) A <- B <- C 의 구조에서 C에서는 A 접근이 가능하며 A가 수정되면 B, C가 재빌드된다
  • implementation
    • 내부적으로만 사용되고 사용자에게는 의존성을 노출시키지 않게 선언??
    • 런타임 시점에 노출 / 의존 라이브러리를 수정해도 본 모듈까지만 재빌드
      ex) A <- B <- C 의 구조에서 C에서는 A에 접근할 수 없고, A 수정 시 B까지만 재빌드한다
  • compileOnly
    • 컴파일 시점에 꼭 필요한 라이브러리
      ex) Lombok
    • 컴파일 시에만 빌드하고 빌드 결과물에는 포함하지 않는다 -> 컴파일하면서 코드로 풀어짐
  • runtimeOnly
    • 컴파일 시점에는 필요 없지만 런타임 시점에 필요한 라이브러리
      ex) Loggin 관련 라이브러리, DB 관련 라이브러리
  • annotationProcessor : annotation processor를 명시하기 위해 사용
dependencies {
    // JWT
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

    //json
    implementation 'org.json:json:20230227'

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.mysql:mysql-connector-j'
    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'
    
    compile project(':sub') 
    #==> 프로젝트 의존성을 사용하면 해당 프로젝트에서 다른 프로젝트의 의존성을 가져올 수 있다
    
    runtime files('common.jar', 'system.jar') 
    #==> 
    #일반 jar파일의 경로를 적어주어 의존성을 추가할 수 있다. 
    #이렇게 하면 maven 중앙저장소나 사설저장소에 넣지 않고도 의존성을 추가 가능
}

 


tasks.named() :

tasks.named('test') {
	useJUnitPlatform() // JUnit플랫폼을 이용하여 'test'라는 이름의 task를 추가한다
}

 


sourceSets{} :

    mapper.xml 파일을 resuorces 폴더에 넣고 싶지 않을 때 사용
   빌드 시 XML파일을 ClassPath에 포함시켜준

sourceSets {
    main {
        resources { // 리소스 파일을 찾는다.
            srcDirs = ["src/main/java", "src/main/resources"] // 이 경로에서
            includes = ["**/*.xml", "**/application.properties"] // 이 유형의 파일을
        }
    }
}

resources 하위는 package경로가 아닌 directory 경로로 접근하게 된다.

점(.)으로 패키지 간 구분이 안된다.

 

 

 

참조

 

gradle 파일 이해하기

개요 지금까지 build.gradle 파일을 사용해서 java와 spring 파일들을 빌드했지만, 구조를 정확히 알지 못하는 느낌을 받았다. 그래서 gradle 파일이 어떤식으로 구성되는지 공부해 보려고 한다. settings.g

hyunwook.dev

 

+ Recent posts