Updated:

MSA

MSA(Microservice Architecture)는 하나의 애플리케이션을 여러 개의 독립적인 서비스로 분리하여 개발, 배포, 유지보수를 용이하게 하는 소프트웨어 아키텍처 스타일로, 각 서비스는 특정 비즈니스 기능을 수행하며 서로 독립적으로 배포되고 확장될 수 있다. 서비스 간의 통신은 주로 HTTP/HTTPS, 메시지 큐 등을 통해 이루어진다.  주요 특징은 다음과 같다.

  • 독립적인 배포 가능성: 각 서비스는 독립적으로 배포할 수 있으며, 다른 서비스에 영향을 주지 않고 업데이트할 수 있음
  • 작인 팀 구성: 각 서비스는 작은 팀이 독립적으로 개발하고 관리할 수 있음
  • 기술 스택의 다양성: 각 서비스는 적절한 기술 스택을 자유롭게 선택할 수 있음

모놀리틱 아키텍처는 다음과 같이 하나의 큰 코드베이스로 구성된 애플리케이션으로, 모든 기능이 하나의 애플리케이션 내에 포함된다.

  • 장점: 간단한 배포, 단일 데이터베이스
  • 단점: 확장성 부족, 긴 개발 주기, 유연성 부족

 반면, MSA는 여러 개의 독립적인 서비스로 구성된 애플리케이션으로, 각 서비스는 특정 비즈니스 기능을 수행한다.

  • 장점: 확장성, 독립적 배포, 유연성(서비스별로 적합한 기술 스택을 선택 가능), 작은 팀 구성 가능
  • 단점: 복잡성 증가, 운영비용 증가, 데이터 일관성 유지 어려움, 네트워크 지연

Spring Cloud

 Spring Cloud는 마이크로서비스 개발을 위해 다양한 도구와 서비스를 제공하는 스프링 프레임워크의 확장으로, 마이크로서비스 아키텍처를 쉽게 구현하고 운영할 수 있도록 도와준다. 주요 기능은 다음과 같다.

  • 서비스 등록 및 디스커버리: Eureka, Consul, Zookeeper
  • 로드 밸런싱: Ribbon, Spring Cloud LoadBalancer
  • 서킷 브레이커: Hystrix, Resilience4j
  • API 게이트웨이: Zuul, Spring Cloud Gateway
  • 구성 관리: Spring Cloud Config
  • 분산 추적: Spring Cloud Sleuth, Zipkin
  • 메시징: Spring Cloud Stream

Spring Cloud 주요 모듈

  • 서비스 등록 및 디스커버리
    • Eureka: 마이크로서비스 아키텍처에서 각 서비스의 위치를 동적으로 관리
      • 서비스 레지스트리: 모든 서비스 인스턴스의 위치를 저장하는 중앙 저장소
      • 헬스 체크(Health check): 서비스 인스턴스의 상태를 주기적으로 확인하여 가용성을 보장
  • 로드 밸런싱
    • Ribbon: 서비스 인스턴스 간의 부하를 분산
      • 서버 리스트 제공자: Eureka로부터 서비스 인스턴스 리스트를 제공받아 로드밸런싱에 사용
      • 로드 밸런싱 알고리즘: 라운드 로빈, 가중치 기반 등 다양한 로드 밸런싱 알고리즘 지원
      • Failover: 요청 실패 시 다른 인스턴스로 자동 전환
  • 서킷 브레이커
    • Hystrix: 서비스 간의 호출 실패를 감지하고 시스템의 전체적인 안정성을 유지
      • 서킷 브레이커 상태: 클로즈드, 오픈, 하프-오픈 상태를 통해 호출 실패를 관리
      • Failback: 호출 실패 시 대체 로직을 제공하여 시스템 안정성 확보
      • 모니터링: Hystrix Dashboard를 통해 서킷 브레이커 상태 모니터링
    • Resilience4j: 자바 기반의 경량 서킷 브레이커 라이브러리
      • 서킷 브레이커: 호출 실패를 감지하고 서킷을 열어 추가적인 호출을 차단하여 시스템의 부하를 줄임
      • Failback: 호출 실패 시 대체 로직을 실행하여 시스템의 안정성을 유지
      • 타임아웃 설정: 호출의 응답 시간을 설정하여 느린 서비스 호출에 대응할 수 있음
      • 재시도: 재시도 기능을 지원하여 일시적인 네트워크 문제 등에 대응할 수 있음

Spring Cloud 구성 요소의 활용

  • API 게이트웨이
    • Zuul: 모든 서비스 요청을 중앙에서 관리
      • 라우팅: 요청 URL에 따라 적절한 서비스로 요청 전달
      • 필터: 요청 전후에 다양한 작업을 수행할 수 있는 필터 체인 제공
      • 모니터링: 요청 로그 및 메트릭을 통해 서비스 상태 모니터링 할 수 있음
    • Cloud Gateway: 스프링 클라우드에서 제공하는 API 게이트웨이로, 마이크로서비스 아키텍처에서 필수적인 역할
      • 루팅 및 필터링: 요청을 받아 특정 서비스로 라우팅하고 필요한 인증 및 권한 부여를 수행
      • 보안: 외부 요청으로부터 애플리케이션을 보호하고, 보안 정책을 적용함
      • 효율성: 마이크로서비스 아키텍처에서 필요한 요청 처리 및 분산 환경의 관리를 효율적으로 수행
  • 구성 관리
    • Spring Cloud Config: 분산된 환경에서 중앙 집중식 설정 관리를 제공
      • Config 서버: 중앙에서 설정 파일을 관리하고 각 서비스에 제공
      • Config 클라이언트: Config 서버에서 설정을 받아서 사용하는 서비스
      • 설정갱신: 설정 변경 시 서비스 재시작 없이 실시간으로 반영

서비스 디스커버리

서비스 디스커버리는 마이크로서비스 아키텍처에서 각 서비스의 위치를 동적으로 관리하고 찾아주는 기능으로, 각 서비스는 등록 서버에 자신의 위치를 등록하고, 다른 서비스는 이를 조회하여 통신한다. 주요 기능으로는 서비스 등록, 서비스 조회, 헬스 체크 등이 있다.

Eureka

 Eureka는 넷플릭스가 개발한 서비스 디스커버리 서버로, 마이크로서비스 아키텍처에서 각 서비스의 위치를 동적으로 관리한다. 모든 서비스 인스턴스의 위치를 저장하는 중앙 저장소 역할을 하며, 서비스 인스턴스의 상태를 주기적으로 확인하여 가용성을 보장하고, 여러 인스턴스를 지원하여 고가용성을 유지할 수 있다.  Eureka 서버는 서비스 레지스트리를 구성하는 중앙 서버로 다음과 같이 설정하면 된다. - 해당 설정을 통해 Eureka 서버를 구성하고, 클라이언트가 등록할 수 있도록 준비한다.

server:
  port: 8761

eureka:
  client:
    register-with-eureka: false  # 다른 Eureka 서버에 이 서버를 등록하지 않음
    fetch-registry: false  # 다른 Eureka 서버의 레지스트리를 가져오지 않음
  server:
    enable-self-preservation: false  # 자기 보호 모드 비활성화

 각 서비스는 Eureka 서버에 자신을 등록해야 한다. spring-cloud-starter-netflix-eureka-client 의존성을 사용하고, 애플리케이션 이름만 설정파일에 있으면 Eureka에 등록된다. 다음은 클라이언트 설정 파일 예시이다.

spring:
  application:
    name: my-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/  # 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-expiration-duration-in-seconds: 90  # 리스 만료 기간

 클라이언트 애플리케이션은 Eureka 서버에서 필요한 서비스의 위치를 조회한다. RestTemplate를 사용하는 경우 Spring Boot 애플리케이션에서 @LoadBalanced 애노테이션을 사용하여 RestTemplate에 로드 밸런싱 기능을 추가한다.

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

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
@RestController
public class MyRestTemplateController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/get-data-rest")
    public String getDataWithRestTemplate() {
        String serviceUrl = "http://my-service/api/data";
        return restTemplate.getForObject(serviceUrl, String.class);
    }
}

 FeignClient를 사용하는 경우는 Spring Boot 애플리케이션에서 FeignClient를 사용하여 간편하게 서비스 호출을 수행한다.

@SpringBootApplication
@EnableFeignClients
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}
@FeignClient(name = "my-service") // http://my-service/api/data
public interface MyServiceClient {

    @GetMapping("/api/data")
    String getData();
}
@RestController
public class MyFeignClientController {

    @Autowired
    private MyServiceClient myServiceClient;

    @GetMapping("/get-data-feign")
    public String getDataWithFeignClient() {
        return myServiceClient.getData();
    }
}

헬스 체크 및 장애 처리

  • 헬스 체크: Eureka 서버가 주기적으로 서비스 인스턴스의 상태를 확인하여 가용성을 유지한다. 기본 헬스 체크 엔드포인트 /actuator/health 를 사용
  • 장애 처리: 서비스 장애 시 Eureka 서버는 해당 인스턴스를 레지스트리에서 제거하여 다른 서비스의 접근을 차단

Eureka의 고가용성 구성

 Eureka 서버의 고가용성을 위해 여러 인스턴스를 구성할 수 있다. 다중 인스턴스로 구성하여 고가용성을 유지하며, 각 인스턴스는 서로를 피어로 등록하여 상호 백업한다. 다음과 같이 설정하면된다.

eureka:
  client:
    service-url:
      defaultZone: http://eureka-peer1:8761/eureka/,http://eureka-peer2:8761/eureka/

 Eureka 서버를 다중 인스턴스로 구성할 때 각 서버의 피어 설정을 통해 서로를 인식하고 백업할 수 있다.

실습

 먼저 Eureka Server Dependencies를 가진 server 프로젝트와 Eureka Discovery Client, Spring Web Dependencies를 가진 프로젝트를 생성한다.

  • Server
    • application.properties
      spring.application.name=server
      
      server.port=19090
      
      eureka.client.register-with-eureka=false
      
      eureka.client.fetch-registry=false
      
      eureka.instance.hostname=localhost
      
      eureka.client.service-url.defaultZone=http://localhost:19090/eureka/
      
    • ServerApplication
      @SpringBootApplication
      @EnableEurekaServer
      public class ServerApplication {
      
          public static void main(String[] args) {
              SpringApplication.run(ServerApplication.class, args);
          }
      }
      
  • Client
    • application.properties
      spring.application.name=first
      
      server.port=19091
      
      eureka.client.service-url.defaultZone=http://localhost:19090/eureka/
      
      spring.application.name=second
      
      server.port=19092
      
      eureka.client.service-url.defaultZone=http://localhost:19090/eureka/
      

 3개의 프로젝트를 실행해준 뒤, http://localhost:19090/에 접속하면 다음과 같이 Application에 FIRST와 SECOND가 들어가있는 것을 볼 수 있다.

로드밸런싱

 로드 밸런싱은 네트워크 트래픽을 여러 서버로 분산시켜 서버의 부하를 줄이고 시스템의 성능과 가용성을 높이는 기술로, 서버 간 트래픽을 고르게 분배하여 특정 서버에 부하가 집중되는 것을 방지한다. 로드 밸런싱의 종류로는 클라이언트 사이드 로드 밸런싱, 서버 사이드 로드 밸런싱이 있다.
클라리언트 사이트 로드 밸런싱은 클라이언트가 직접 여러 서버 중 하나를 선택하여 요청을 보내는 방식으로, 클라이언트는 서버의 목록을 가지고 있으며 이를 바탕으로 로드 밸런싱을 수행한다.

FeignClient & Ribbon

  • FeignClient: Spring Cloud에서 제공하는 HTTP 클라이언트로, 선언적으로 RESTful 웹 서비스를 호출할 수 있음
    • Eureka와 같은 서비스 디스커버리와 연동하여 동적으로 서비스 인스턴스를 조회하고 로드 밸런싱을 수행
    • 선언적 HTTP 클라이언트: 인터페이스와 어노테이션을 사용하여 REST API를 호출할 수 있음
    • Eureka 연동: Eureka와 통합하여 서비스 인스턴스 목록을 동적으로 조회하고 로드 밸런싱을 수행
    • 자동 로드 밸런싱: Ribbon이 통합되어 있어 자동으로 로드 밸런싱을 수행
  • Ribbon: 넷플릭스가 개발한 클라이언트 사이드 로드 밸런서로, 마이크로서비스 아키텍처에서 서비스 인스턴스 간의 부하를 분산
    • 다양한 로드 밸런싱 알고리즘을 지원하며, Eureka와 같은 서비스 디스커버리와 연동하여 사용
    • 서버 리스트 제공자: Eureka 등으로부터 서비스 인스턴스 리스트를 제공받아 로드 밸런싱에 사용
    • 로드 밸런싱 알고리즘: 라운드 로빈, 가중치 기반 등 다양한 로드 밸런싱 알고리즘 지원
    • Failover: 요청 실패 시 다른 인스턴스로 자동 전환

 FeifnClient와 Ribbon을 사용하려면 의존성을 추가해야 한다.

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}
@SpringBootApplication
@EnableFeignClients
public class MyServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyServiceApplication.class, args);
    }
}

 FeignClient 인터페이스를 작성하여 서비스 호출을 수행한다.

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

    @GetMapping("/endpoint")
    String getResponse(@RequestParam(name = "param") String param);
}

로드 밸런싱 알고리즘

  • 라운드 로빈: 각 서버에 순차적으로 요청을 분배하는 방식
    • 간단하고 공평하게 트래픽을 분산
  • 가중치 기반 로드 밸런싱: 각 서버에 가중치를 부여하고, 가중치에 비례하여 요청을 분배하는 방식
    • 서버의 성능이나 네트워크 상태에 따라 가중치를 조절
  • 최소 연결: 현재 연결된 클라이언트 수가 가장 적은 서버로 요청을 보내는 방식
  • 응답 시간 기반: 서버의 응답 시간을 기준으로 가장 빠른 서버로 요청을 보내는 방식

FeignClient와 Eureka 연동

  • Eureka 설정: Eureka와 FeignClient를 함께 사용하면 동적으로 서비스 인스턴스를 조회하여 로드 밸런싱을 수행
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka
    
  • FeignClient와 Ribbon 설정: FeignClient에서 제공하는 서비스 인스턴스를 사용하여 로드 밸런싱을 수행
    my-service:
      ribbon:
        eureka:
          enabled: true
    

 FeignClient와 Ribbon 동작 원리는 다음과 같다.

  1. 서비스 이름: @FeignClient(name = "my-service") 어노테이션은 Eureka에 등록된 서비스 이름을 참조
  2. 서비스 인스턴스 조회: Eureka 서버에서 my-service라는 이름으로 등록된 서비스 인스턴스 목록을 조회
  3. 로드 밸런싱: 조회된 서비스 인스턴스 목록 중 하나를 선택하여 요청을 보냅니다. 이는 기본적으로 Ribbon을 사용하여 로드 밸런싱을 수행
  4. 요청 분배: 여러 서비스 인스턴스가 있을 경우, Round Robin 또는 다른 설정된 로드 밸런싱 알고리즘을 사용하여 요청을 분배

 FeignClient 인터페이스를 작성하여 서비스 호출을 수행한다.

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

    @GetMapping("/endpoint")
    String getResponse(@RequestParam(name = "param") String param);
}

 FeignClient를 사용하여 다른 서비스 호출을 수행한다.

@RestController
public class MyController {

    @Autowired
    private MyServiceClient myServiceClient;

    @GetMapping("/call-service")
    public String callService(@RequestParam String param) {
        return myServiceClient.getResponse(param);
    }
}

실습

 Eureka 서버 하나에 주문 인스턴스 1개와 같은 기능의 포트만 다른 인스턴스 3개를 연결해본다.
 먼저, Eureka Discovery Client, Spring Web, Lombok, OpenFeign Dependencies를 가진 product 프로젝트와 order 프로젝트를 생성한다.
 Product 프로젝트부터 코드를 작성한다.

@SpringBootApplication
@EnableFeignClients
public class ProductApplication {

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

}
@RestController
public class ProductController {

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/product/{id}")
    public String getProduct(@PathVariable("id") String id) {
        return "Product " + id + "info!!!!! From port : " + serverPort;
    }
    
}
spring:
  application:
    name: product-service
server:
  port: 19092
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

 19092, 19093, 19094 포트에 같은 애플리케이션을 실행해야 한다. Intellij에서는 이를 하기 위해 실행 > 구성 편집 에서 ProductApplication의 이름을 ProductApplication:19092 로 변경하고, 우측 상단의 복사 버튼을 클릭하여 ProductApplication을 두개 더 생성하고 19093, 19094로 이름을 변경한다. 그리고 VM 옵션 추가를 하여 -Dserver.port=19093, -Dserver.port=19094를 입력한다.

 이후, server를 실행하고 product:19092를 실행하면 eureka 서버에서 다음과 같이 볼 수 있다.

 또한 http://localhost:19092/product/1 에 접속하면 다음과 같이 출력됨을 볼 수 있다(19093, 19094 도 동일).

 이번엔 Order 프로젝트를 작성한다.

@SpringBootApplication
@EnableFeignClients
public class OrderApplication {

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

}
@RestController
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;
    
    @GetMapping("/order/{orderId}")
    public String order(@PathVariable("orderId") String orderId) {
        return orderService.getOrder(orderId);
    }
}
@FeignClient(name = "product-service")
public interface ProductClient {
    @GetMapping("/product/{id}")
    String getProduct(@PathVariable("id") String id);
}
@Service
@RequiredArgsConstructor
public class OrderService {

    private final ProductClient productClient;

    public String getProductInfo(String productId) {
        return productClient.getProduct(productId);
    }

    public String getOrder(String orderId) {
        if(orderId.equals("1") ){
            String productId = "2";
            String productInfo = getProductInfo(productId);
            return "Your order is " + orderId + " and " + productInfo;

        }
        return "Not exist order...";
    }
}
spring:
  application:
    name: order-service
server:
  port: 19091
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

 localhost:19090 에 접속하면 order service가 정상적으로 등록된 것을 확인할 수 있다. 또한 http://localhost:19091/order/1 에 접속하면 Your order is 1 and Product 2info!!!!! From port : 19092, Your order is 1 and Product 2info!!!!! From port : 19094, Your order is 1 and Product 2info!!!!! From port : 19093가 돌아가면서 출력됨을 확인할 수 있다. 이런 방식이 라운드로빈이다.

서킷브레이커

 서킷 브레이커는 마이크로서비스 간의 호출 실패를 감지하고 시스템의 전체적인 안정성을 유지하는 패턴으로, 외부 서비스 호출 실패 시 빠른 실패를 통해 장애를 격리하고, 시스템의 다른 부분에 영향을 주지 않도록 한다. 클로즈드 > 오픈 > 하프-오픈으로 상태가 변환한다.

Resilience4j

 Resilience4j는 서킷 브레이커 라이브러리로, 서비스 간의 호출 실패를 감지하고 시스템의 안정성을 유지한다. 다양한 서킷 브레이커 기능을 제공하며, 장애 격리 및 빠른 실패를 통해 복원력을 높인다.
 Resilience4j의 주요 특징은 다음과 같다.

  • 서킷 브레이커 상태: 클로즈드, 오픈, 하프-오픈 상태를 통해 호출 실패를 관리
    • 클로즈드(Closed):
      • 기본 상태로, 모든 요청을 통과시킵니다.
      • 이 상태에서 호출이 실패하면 실패 카운터가 증가합니다.
      • 실패율이 설정된 임계값(예: 50%)을 초과하면 서킷 브레이커가 오픈 상태로 전환됩니다.
      • 예시: 최근 5번의 호출 중 3번이 실패하여 실패율이 60%에 도달하면 오픈 상태로 전환됩니다.
    • 오픈(Open):
      • 서킷 브레이커가 오픈 상태로 전환되면 모든 요청을 즉시 실패로 처리합니다.
      • 이 상태에서 요청이 실패하지 않고 바로 에러 응답을 반환합니다.
      • 설정된 대기 시간이 지난 후, 서킷 브레이커는 하프-오픈 상태로 전환됩니다.
      • 예시: 서킷 브레이커가 오픈 상태로 전환되고 20초 동안 모든 요청이 차단됩니다.
    • 하프-오픈(Half-Open):
      • 오픈 상태에서 대기 시간이 지나면 서킷 브레이커는 하프-오픈 상태로 전환됩니다.
      • 하프-오픈 상태에서는 제한된 수의 요청을 허용하여 시스템이 정상 상태로 복구되었는지 확인합니다.
      • 요청이 성공하면 서킷 브레이커는 클로즈드 상태로 전환됩니다.
      • 요청이 다시 실패하면 서킷 브레이커는 다시 오픈 상태로 전환됩니다.
      • 예시: 하프-오픈 상태에서 3개의 요청을 허용하고, 모두 성공하면 클로즈드 상태로 전환됩니다. 만약 하나라도 실패하면 다시 오픈 상태로 전환됩니다.
  • Fallback: 호출 실패 시 대체 로직을 제공하여 시스템 안정성 확보
  • 모니터링: 서킷 브레이커 상태를 모니터링하고 관리할 수 있는 다양한 도구 제공

Resilience4j를 사용하려면 Spring Boot 애플리케이션에 의존성을 추가해야한다.

dependencies {
    implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
	  implementation 'org.springframework.boot:spring-boot-starter-aop'
}

 Resilience4j의 설정은 application.yml 파일에서 설정할 수 있습니다.

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초로 설정

Fallback 메서드는 외부 서비스 호출이 실패했을 때 대체 로직을 제공하는 메서드이다.

@Service
public class MyService {

    @CircuitBreaker(name = "myService", fallbackMethod = "fallbackMethod")
    public String myMethod() {
        // 외부 서비스 호출
        return externalService.call();
    }

    public String fallbackMethod(Throwable t) {
        return "Fallback response";
    }
}

 Fallback은 시스템의 안정성을 높이고, 장애가 발생해도 사용자에게 일정한 응답을 제공할 수 있다. 또한 장애가 다른 서비스에 전파되는 것을 방지한다.
 Resilience4j Dashboard를 사용하여 서킷 브레이커의 상태를 모니터링할 수 있다.

dependencies {
    implementation 'io.github.resilience4j:resilience4j-micrometer'
    implementation 'io.micrometer:micrometer-registry-prometheus'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

 설정은 application.yml 파일에서 설정할 수 있다.

management:
  endpoints:
    web:
      exposure:
        include: prometheus
  prometheus:
    metrics:
      export:
        enabled: true

 http://${hostname}:${port}/actuator/prometheus 에 접속하여 서킷브레이커 항목을 확인 가능하다.
 Prometheus와 Grafana를 사용하여 Resilience4j 서킷 브레이커의 상태를 실시간으로 모니터링할 수 있다. Prometheus를 통해 수집된 메트릭을 Grafana 대시보드에서 시각화할 수 있다.

Resilience4j와 Spring Cloud 연동

 Resilience4j는 Spring Cloud Netflix 패키지의 일부로, Eureka와 Ribbon 등 다른 Spring Cloud 구성 요소와 쉽게 통합할 수 있다. Spring Cloud의 서비스 디스커버리와 로드 밸런싱을 활용하여 더욱 안정적인 마이크로서비스 아키텍처를 구축할 수 있다.

spring:
  application:
    name: my-service
  cloud:
    circuitbreaker:
      resilience4j:
        enabled: true

실습

 이번 실습에서는 상품을 조회하는 것을 가정해 상품 아이디 111을 호출하면 에러를 발생시켜 fallbackMethod를 실행하는 것을 확인한다. 또한 이벤트리스너를 사용하여 서킷브레이커의 상태를 조회해본다.
 먼저, Lombok, Spring Web, Spring Boot Actuator, Prometheus Dependencies를 가진 프로젝트를 생성한다. 이후 build.gradle에 들어가 dependencies를 추가한다.

implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter-aop'
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {

    private String id;
    private String title;
}
@Service
@RequiredArgsConstructor
public class ProductService {

    @CircuitBreaker(name = "productService", fallbackMethod = "fallbackGetProductDetails")
    public Product getProductDetails(String productId) {

        if ("111".equals(productId)) {
            throw new RuntimeException("Empty response body");
        }

        return new Product(productId, "sample Product");
    }

    public Product fallbackGetProductDetails(String productId, Throwable t) {
        return new Product(productId, "Fallback Product");
    }
}
@RestController
@RequiredArgsConstructor
public class ProductController {

    private final ProductService productService;

    @GetMapping("/product/{id}")
    public Product getProduct(@PathVariable("id") String id) {
        return productService.getProductDetails(id);
    }
}
spring:
  application:
    name: sample

server:
  port: 19090

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초로 설정

management:
  endpoints:
    web:
      exposure:
        include: prometheus
  prometheus:
    metrics:
      export:
        enabled: true

 프로젝트를 실행하면 productId가 111일 때, Fallback Product가 출력됨을 확인할 수 있다.
 이제 ProductService를 다음과 같이 수정해서 로그를 확인해본다.

@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"
        );
    }
}

 이후 http://localhost:19090/actuator/prometheus 에 접속하면 인스턴스의 상태 값들을 볼 수 있다.

Api GW

 API 게이트웨이는 클라이언트의 요청을 받아 백엔드 서비스로 라우팅하고, 다양한 부가 기능을 제공하는 중간 서버이다. 클라이언트와 서비스 간의 단일 진입점 역할을 하며, 보안, 로깅, 모니터링, 요청 필터링 등을 처리한다.
 API 게이트웨이의 주요 기능은 다음과 같다.

  • 라우팅: 클라이언트 요청을 적절한 서비스로 전달
  • 인증 및 권한 부여: 요청의 인증 및 권한을 검증
  • 로드 밸런싱: 여러 서비스 인스턴스 간의 부하 분산
  • 모니터링 및 로깅: 요청 및 응답을 로깅하고 모니터링
  • 요청 및 응답 변환: 요청과 응답을 변환하거나 필터링

Spring Cloud Gateway

 pring Cloud Gateway는 Spring 프로젝트의 일환으로 개발된 API 게이트웨이로, 클라이언트 요청을 적절한 서비스로 라우팅하고 다양한 필터링 기능을 제공한다. Spring Cloud Netflix 패키지의 일부로, 마이크로서비스 아키텍처에서 널리 사용된다.
 Spring Cloud Gateway의 주요 특징은 다음과 같다.

  • 동적 라우팅: 요청의 URL 패턴에 따라 동적으로 라우팅
  • 필터링: 요청 전후에 다양한 작업을 수행할 수 있는 필터 체인 제공
  • 모니터링: 요청 로그 및 메트릭을 통해 서비스 상태 모니터링
  • 보안: 요청의 인증 및 권한 검증

 Spring Cloud Gateway를 사용하려면 Spring Boot 애플리케이션에 의존성을 추가해야 한다.

dependencies {
    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 파일에서 라우팅 설정을 정의할 수 있다.

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
      routes:
        - id: users-service  # 라우트 식별자
          uri: lb://users-service # 'users-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/users/** # /users/** 경로로 들어오는 요청을 이 라우트로 처리
        - id: orders-service  # 라우트 식별자
          uri: lb://orders-service  # 'orders-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/orders/** #/orders/** 경로로 들어오는 요청을 이 라우트로 처리

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

 Spring Cloud Gateway 필터 종류는 다음과 같다.

  • Global Filter: 모든 요청에 대해 작동하는 필터
  • Gateway Filter: 특정 라우트에만 적용되는 필터

 필터를 구현하려면 GlobalFilter 또는 GatewayFilter 인터페이스를 구현하고, filter 메서드를 오버라이드해야 한다.
 필터의 주요 객체로는 Mono, ServerWebExchange, GatewayFilterChain이 있다.

  • Mono: 리액티브 프로그래밍에서 0 또는 1개의 데이터를 비동기적으로 처리
    • Mono<Void>는 아무 데이터도 반환하지 않음을 의미
  • ServerWebExchange: HTTP 요청과 응답을 캡슐화한 객체
    • exchange.getRequest()로 HTTP 요청을 가져옴
    • exchange.getResponse()로 HTTP 응답을 가져옴
  • GatewayFilterChain: 여러 필터를 체인처럼 연결함
    • chain.filter(exchange)는 다음 필터로 요청을 전달함

 필터 시점별 종류로는 다음이 있다.

  • Pre 필터
    • 요청이 처리되기 전에 실행
    • 요청을 가로채고 필요한 작업을 수행한 다음, 체인의 다음 필터로 요청을 전달
    • 추가적인 비동기 작업을 수행할 필요가 없기 때문에 then 메서드를 사용할 필요가 없음
      @Component
      public class PreFilter implements GlobalFilter, Ordered {
      
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 요청 로깅
            System.out.println("Request: " + exchange.getRequest().getPath());
            return chain.filter(exchange);
        }
      
        @Override
        public int getOrder() {  // 필터의 순서를 지정합니다.
            return -1;  // 필터 순서를 가장 높은 우선 순위로 설정합니다.
        }
      }
      
  • Post 필터
    • 요청이 처리된 후, 응답이 반환되기 전에 실행
    • 체인의 다음 필터가 완료된 후에 실행되어야 하는 추가적인 작업을 수행해야 함
    • chain.filter(exchange)를 호출하여 다음 필터를 실행한 후, then 메서드를 사용하여 응답이 완료된 후에 실행할 작업을 정의
      @Component
      public class PostFilter implements GlobalFilter, Ordered {
      
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                // 응답 로깅
                System.out.println("Response Status: " + exchange.getResponse().getStatusCode());
            }));
        }
      
        @Override
        public int getOrder() {
            return -1;
        }
      }
      

 Spring Cloud Gateway는 Spring Cloud Netflix 패키지의 일부로, Eureka와 쉽게 통합할 수 있다. Eureka를 통해 동적으로 서비스 인스턴스를 조회하여 로드 밸런싱과 라우팅을 수행할 수 있다.

실습

 기존 eureka 프로젝트를 이어서 진행한다. order 프로젝트는 19092로, product 프로젝트틑 19093으로 해준다. 이후 OderController와 ProductController를 수정한다.

@RestController
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    @GetMapping("/order")
    public String order() {
        return "Order detail";
    }
}
@RestController
@RequiredArgsConstructor
public class ProductController {

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("/product")
    public String getProduct() {
        return "Product info!!!!! From port : " + serverPort;
    }
}

 이번엔 Lombok, Reactive Gateway, Spring Boot Actuator, Eureka Discovery Client, Spring Web dependencies를 가진 GateWay를 생성한다.

@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 request = exchange.getRequest();
        logger.info("Pre Filter: Request URI: " + request.getURI());
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
@Component
public class CustomPostFilter implements GlobalFilter, Ordered {

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

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();
            logger.info("Post Filter : Response status code is " + response.getStatusCode());

        }));
    }

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

}
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을 지정

 http://localhost:19091/order, http://localhost:19091/product 를 실행하면 정상적으로 실행됨을 알 수 있다.

보안구성

 마이크로서비스 아키텍처에서는 각 서비스가 독립적으로 배포되고 통신하기 때문에 보안이 매우 중요하므로 데이터 보호, 인증 및 권한 부여, 통신 암호화 등을 통해 시스템의 보안성을 확보해야 한다.

OAuth2

 OAuth2는 토큰 기반의 인증 및 권한 부여 프로토콜이다. 클라이언트 애플리케이션이 리소스 소유자의 권한을 얻어 보호된 리소스에 접근할 수 있도록 한다. OAuth2는 네 가지 역할(리소스 소유자, 클라이언트, 리소스 서버, 인증 서버)을 정의한다. 다음은 OAuth2의 주요 개념이다.

  • Authorization Code Grant: 인증 코드를 사용하여 액세스 토큰을 얻는 방식
  • Implicit Grant: 클라이언트 애플리케이션에서 직접 액세스 토큰을 얻는 방식
  • Resource Owner Password Credentials Grant: 사용자 이름과 비밀번호를 사용하여 액세스 토큰을 얻는 방식
  • Client Credentials Grant: 클라이언트 애플리케이션이 자신의 자격 증명을 사용하여 액세스 토큰을 얻는 방식

JWT

 JWT(JSON Web Token)는 JSON 형식의 자가 포함된 토큰으로, 클레임(claim)을 포함하여 사용자에 대한 정보를 전달한다. JWT는 세 부분(헤더, 페이로드, 서명)으로 구성된다. JWT는 암호화를 통해 데이터의 무결성과 출처를 보장한다. JWT의 주요 특징은 다음과 같다.

  • 자가 포함: 토큰 자체에 모든 정보를 포함하고 있어 별도의 상태 저장이 필요 없음
  • 간결성: 짧고 간결한 문자열로, URL, 헤더 등에 쉽게 포함될 수 있음
  • 서명 및 암호화: 데이터의 무결성과 인증을 보장

실습

 클라우드 게이트웨이의 Pre 필터에서 JWT 인증을 진행해본다. 기존에 진행한 gateway 프로젝트를 이어서 진행한다.
 먼저, Spring Web, Spring Boot Actuator, Eureka Discovery Client, Lombok, Spring Security dependencies를 가진 auth 프로젝트를 생성하고 build.gradle에 implementation 'io.jsonwebtoken:jjwt:0.12.6' 를 추가한다.

spring:
  application:
    name: auth-service

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

service:
  jwt:
    access-expiration: 3600000
    secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"

server:
  port: 19095
@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();
    }
}
@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;

    }
}
@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();
    }
}

 이 프로젝트를 실행하고 localhost:19090 에 접속하면 다음과 같이 정상적으로 실행됨을 볼 수 있다.

 이번에는 Gateway 코드에 JWT 인증 및 auth-service 라우팅 정보를 추가한다. 먼저, implementation 'io.jsonwebtoken:jjwt:0.12.6' dependency를 추가하고 yml에 다음을 추가한다.

service:
  jwt:
    secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
@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;
        }
    }
}

 이제 Gateway 서버를 실행하고 http://localhost:19091/auth/signIn?user_id=aaa 를 실행하면 19091 포트에서도 정상적으로 실행됨을 확인할 수 있다. 또한 ProductApplication을 실행하면 http://localhost:19091/product 에서 정상적으로 실행된다. 하지만 http://localhost:19093/product 에서 접속이 가능한데, 이는 방화벽같은 처리를 해서 접근을 못하게 막아줘야 한다.

Config

 Spring Cloud Config는 분산 시스템 환경에서 중앙 집중식 구성 관리를 제공하는 프레임워크로, 애플리케이션의 설정을 중앙에서 관리하고, 변경 사항을 실시간으로 반영할 수 있다. Git, 파일 시스템, JDBC 등 다양한 저장소를 지원한다. 주요 기능은 다음과 같다.

  • 중앙 집중식 구성 관리: 모든 마이크로서비스의 설정을 중앙에서 관리합니다.
  • 환경별 구성: 개발, 테스트, 운영 등 환경별로 구성을 분리하여 관리할 수 있습니다.
  • 실시간 구성 변경: 설정 변경 시 애플리케이션을 재시작하지 않고도 실시간으로 반영할 수 있습니다.

Spring Cloud Config

 Config 서버는 설정 파일을 저장하고 제공하는 역할을 한다. 다음과 같은 의존성을 추가해야 한다.

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-config-server'
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

 Spring Boot 애플리케이션에서 Config서버를 설정해야 한다.

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

 application.yml 파일에서 Config 서버의 설정을 정의한다.

server:
  port: 8888

spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/my-config-repo/config-repo
          clone-on-start: true

 Config 클라이언트는 Config 서버에서 설정을 받아오는 역할을 한다. 먼저, build.gradle 에 의존성을 추가해야한다.

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-config'
}

 클라이언트의 application.yml 파일에서 Config 서버의 설정을 정의한다.

spring:
  application:
    name: my-config-client
  cloud:
    config:
      discovery:
        enabled: true
        service-id: config-server
     
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

구성 관리

 Config 서버는 환경별로 다른 설정 파일을 제공할 수 있다. 예를 들어, application-dev.yml, application-prod.yml 파일을 Git 저장소에 저장하여 환경별 설정을 관리한다. 이는 Spring Boot 애플리케이션에서 프로필을 사용하여 환경을 구분할 수 있다.

spring:
  profiles:
    active: dev

 실시간 구성 변경을 반영하는 방법에는 여러 가지가 있다. Spring Cloud Bus를 사용하는 방법, 수동으로 /actuator/refresh 엔드포인트를 호출하는 방법, Spring Boot DevTools를 사용하는 방법, 그리고 Git 저장소를 사용하는 방법이 있다. Spring Cloud Bus는 메시징 시스템을 통해 실시간으로 설정 변경 사항을 전파하는 데 매우 유용하며, Git 저장소를 사용하면 설정 파일의 버전 관리를 쉽게 할 수 있다. Spring Boot DevTools는 주로 개발 환경에서 유용하게 사용된다.

  • Spring Cloud Bus
    • 설정 변경 사항을 실시간으로 클라이언트 애플리케이션에 반영할 수 있음
    • 이를 위해서는 메시징 시스템(RabbitMQ 또는 Kafka 등)을 사용하여 변경 사항을 전파해야 함
  • 수동 구성 갱신
    • /actuator/refresh 엔드포인트 사용
      • Spring Cloud Bus를 사용하지 않는 경우, 클라이언트 애플리케이션에서 수동으로 설정을 갱신할 수 있음
      • Config 서버에서 설정 파일을 변경한뒤, 클라이언트 애플리케이션의 /actuator/refresh 엔드포인트를 POST 요청으로 호출하여 변경된 설정을 반영함
      • 간단하지만, 각 클라이언트 애플리케이션에서 수동으로 엔드포인트를 호출해야 함
    • Spring Boot DevTools 사용
      • 개발 환경에서 파일 변경을 자동으로 감지하고 애플리케이션을 재시작할 수 있음
      • classpath 내의 파일 변경도 포함
  • Git 저장소 사용
    • 설정 파일의 변경 사항을 쉽게 반영하고, 여러 서비스 간에 일관된 구성을 유지하는 데 유용

실습

 Config 서버를 생성하고 product 애플리케이션이 local에서 동작할 때 포트 정보 빛 메시지를 Config 서버에서 가져온다. Config 서버의 메시지를 변경하여 product 애플리케이션의 message가 갱신되는 모습을 확인한다. Gateway 에서 학습한 모든 프로젝트를 가져와 실습한다.
 실습에서는 네이티브 모드를 사용한다. 로컬 환경에서는 설정 변경 후 반영하려면 애플리케이션을 재시작해야 하기 때문이다.
Config Server, Eureka Discovery Client, Spring Web, Spring Boot Actuator, Lombok dependencies를 가진 config 프로젝트를 생성한다.

@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {

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

}
server:
  port: 18080

spring:
  profiles:
    active: native
  application:
    name: config-server
  cloud:
    config:
      server:
        native:
          search-locations: classpath:/config-repo  # 리소스 폴더의 디렉토리 경로



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

 resources 안에 config-repo 폴더를 생성한 후 product-service.yml, product-service-local.yml 파일을 생성한다.

server:
  port: 19093

message: "product-service message"
server:
  port: 19083

message: "product-service-local message"

 Product-service 프로젝트 build.gradle에 config client 를 추가한다. 또한 yml과 ProductController를 다음과 같이 설정한다.

server:
  port: 0  # 임시 포트, 이후 Config 서버 설정으로 덮어씌움

spring:
  profiles:
    active: local
  application:
    name: product-service
  config:
    import: "configserver:"
  cloud:
    config:
      discovery:
        enabled: true
        service-id: config-server

management:
  endpoints:
    web:
      exposure:
        include: refresh

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

message: "default message"
@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 ;
    }
}

 이제 프로젝트를 실행해보면, product 프로젝트를 0번 포트로 지정했는데 19083 포트로 실행됨을 확인할 수 있다. 또한 http://localhost:19083/product 로 Get 요청을 보내면 product-service-local yml 파일을 보여주는 것을 확인할 수 있다.
 product-service의 profiles: active: local을 삭제하고 재시작하면, 19093번 포트로 실행됨을 확인할 수 있다.
 다시 local로 설정을 해준 뒤, product-service-local.yml의 message를 product-service-local message update로 수정해준 뒤, http://localhost:19083/actuator/refresh 로 post 요청을 보내면 메시지가 업데이트 됨을 확인할 수 있다. 다시 http://localhost:19083/product 를 호출하면 메시지가 변경됨을 확인할 수 있다.

분산추적

 분산 추적은 분산 시스템에서 서비스 간의 요청 흐름을 추적하고 모니터링하는 방법으로 각 서비스의 호출 관계와 성능을 시각화하여 문제를 진단하고 해결할 수 있도록 돕는다. 주요 개념은 다음과 같다.

  • 트레이스(Trace) : 트레이스는 하나의 요청이 시작부터 끝까지 각 서비스를 거치는 전체 흐름을 나타냄
    • 하나의 트레이스는 여러 개의 스팬으로 구성
    • 분산 시스템에서 클라이언트의 요청이 여러 서비스로 전달될 때, 각 서비스 호출이 트레이스의 일부로 기록됨
    • 트레이스 ID는 각 스팬에 공통으로 부여되며, 이를 통해 전체 요청 흐름을 추적할 수 있음
  • 스팬(Span) : 스팬은 분산 추적에서 가장 작은 단위로, 특정 서비스 내에서의 개별 작업 또는 요청을 나타냄
    • 각 스팬은 시작 시간과 종료 시간을 기록하여 작업의 지속 시간을 나타냄
    • 스팬은 고유한 스팬 ID를 가지며, 이는 트레이스 ID와 함께 특정 작업을 식별하는 데 사용
    • 스팬은 부모-자식 관계를 가질 수 있으며, 이를 통해 호출 계층 구조를 표현합
    • 스팬에는 메타데이터(태그, 로그, 이벤트 등)를 추가하여 상세한 정보를 기록할 수 있음
  • 컨텍스트(Context)
    • 컨텍스트는 요청이 서비스 간에 전달될 때 함께 전파되어, 각 서비스가 요청의 전체 흐름에 대한 정보를 가질 수 있게 함
    • 컨텍스트는 트레이스 ID, 스팬 ID, 부모 스팬 ID 등의 정보를 포함하여 각 서비스가 요청의 출처와 경로를 추적할 수 있도록 도움
    • 서비스 호출 간에 컨텍스트를 유지함으로써, 분산 시스템 전체에서 일관된 추적이 가능함

 MSA에서는 여러 서비스가 협력하여 하나의 요청을 처리한다. 서비스 간의 복잡한 호출 관계로 인해 문제 발생 시 원인을 파악하기 어려울 수 있다. 분산 추적을 통해 각 서비스의 호출 흐름을 명확히 파악하고, 성능 병목이나 오류를 빠르게 진단할 수 있다.

Micrometer

Micrometer는 Spring 기반 애플리케이션에서 메트릭을 수집하고 모니터링하기 위한 라이브러리이다. 각 서비스의 성능 지표를 수집하고, Prometheus, Grafana 등과 연동하여 시각화할 수 있다. 분산 추적을 위한 기능도 제공하여 서비스 간의 호출 흐름을 추적할 수 있다. 주요 특징은 다음과 같다.

  • 다양한 메트릭 수집: 애플리케이션의 다양한 성능 지표를 수집할 수 있음
  • 유연한 연동: Prometheus, Grafana 등 다양한 모니터링 도구와 연동할 수 있음
  • 추적 기능: 서비스 간의 호출 흐름을 추적하여 성능 병목을 진단할 수 있음

Zipkin

Zipkin은 트레이스 데이터를 수집하고 시각화하는 분산 추적 시스템이다. 각 서비스의 트레이스와 스팬 데이터를 저장하고, 이를 통해 호출 흐름을 시각화한다. 주요 특징은 다음과 같다.

  • 데이터 수집 및 저장: 각 서비스에서 전송된 트레이스 데이터를 수집하고 저장
  • 시각화: 트레이스 데이터를 시각화하여 서비스 간의 호출 관계를 명확히 파악할 수 있음
  • 검색 및 필터링: 특정 트레이스나 스팬을 검색하고 필터링하여 문제를 진단할 수 있음

 Zipkin 서버를 Docker를 사용하여 실행할 수 있다(docker run -d -p 9411:9411 openzipkin/zipkin). Zipkin 대시보드에 접속(http://localhost:9411)하여 트레이스 데이터를 시각화한다.
 분산 추적은 다음과 같은 상황에서 사용할 수 있다.

  • 서비스 호출 흐름 추적
    • 예제 서비스 간의 호출 흐름을 추적하고, Zipkin 대시보드에서 시각화
    • 각 서비스 호출 시 트레이스와 스팬이 생성되고, Zipkin 서버로 전송
  • 성능 병목 진단
    • Zipkin 대시보드를 통해 성능 병목이 발생하는 부분을 식별
    • 각 스팬의 소요 시간과 호출 관계를 분석하여 성능 문제를 진단하고 해결

실습

 로드밸런싱에서 사용한 프로젝트를 사용한다. product-service는 19092 포트, order-service는 19091 포트를 사용한다.
 product-service에 다음 dependencies를 추가하고 yml 파일을 수정한다.

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.github.openfeign:feign-micrometer'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
spring:
  application:
    name: product-service
server:
  port: 19092
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

management:
  zipkin:
    tracing:
      endpoint: "http://localhost:9411/api/v2/spans"
  tracing:
    sampling:
      probability: 1.0

 order-service도 수정한다.

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.github.openfeign:feign-micrometer'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
spring:
  application:
    name: order-service
server:
  port: 19091
eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

management:
  zipkin:
    tracing:
      endpoint: "http://localhost:9411/api/v2/spans"
  tracing:
    sampling:
      probability: 1.0

 Docker를 이용해 Zipkin을 실행하면 http://localhost:9411/zipkin/ 로 접속 후에 RUN QUERY를 클릭하면 리스트가 나오며 Spans 3인 항목의 SHOW를 클릭하면 Order-service가 Product-service를 호출하는 과정이 트래킹 되는 것을 확인할 수 있다.

이벤트 드리븐

 이벤트 드리븐 아키텍처는 시스템에서 발생하는 이벤트(상태 변화나 행동)를 기반으로 동작하는 소프트웨어 설계 스타일이다. 이벤트는 비동기적으로 처리되며, 서비스 간의 느슨한 결합을 통해 독립적으로 동작할 수 있게 한다. 주요 개념은 다음과 같다.

  • 이벤트: 시스템 내에서 발생하는 상태 변화나 행동을 나타내는 메시지
  • 이벤트 소스: 이벤트를 생성하여 이벤트 버스에 전달하는 역할
  • 이벤트 핸들러: 이벤트를 수신하여 처리하는 역할
  • 이벤트 버스: 이벤트 소스와 이벤트 핸들러 간의 메시지 전달을 중개

 이벤트 드리븐 아키텍처의 장점은 다음과 같다.

  • 느슨한 결합
    • 서비스 간의 강한 종속성을 제거하여 독립적인 개발과 배포가 가능
    • 이벤트 기반 통신을 통해 서비스 간의 결합도 낮춤
  • 확장성
    • 수평 확장이 용이하여 대규모 시스템에서 유용
    • 이벤트 프로듀서와 컨슈머를 독립적으로 확장 가능
  • 비동기 처리
    • 이벤트를 비동기적으로 처리하여 시스템의 응답성 향상
    • 요청과 응답을 비동기적으로 처리하여 성능 최적화

 이벤트 드리븐 아키텍처의 단점은 다음과 같다.

  • 복잡성 증가
    • 이벤트 기반 통신으로 인해 시스템의 복잡성 증가 가능
    • 이벤트 흐름과 상태 관리를 체계적으로 설계 필요
  • 장애 전파
    • 이벤트 실패 시 다른 서비스로 장애가 전파될 수 있음
    • 이벤트 재처리 및 장애 복구 메커니즘 구현 필요

Spring Cloud Stream

 Spring Cloud Stream은 이벤트 드리븐 마이크로서비스를 구축하기 위한 프레임워크이다. Kafka, RabbitMQ 등의 메시지 브로커와 통합하여 이벤트 스트리밍을 처리한다. 프로듀서와 컨슈머 간의 통신을 추상화하여 간편하게 이벤트 기반 애플리케이션을 개발할 수 있다. 주요 특징은 다음과 같다.

  • 바인더 추상화: 메시지 브로커와의 통합을 위한 추상화 레이어를 제공
  • 프로듀서/컨슈머 모델: 이벤트를 생성하고 처리하는 프로듀서와 컨슈머 모델을 지원
  • 유연한 설정: 다양한 설정 옵션을 통해 손쉽게 커스터마이징할 수 있음

Kubernetes

 쿠버네티스(Kubernetes)는 컨테이너화된 애플리케이션의 배포, 확장, 운영을 자동화하는 오픈소스 플랫폼이다. 컨테이너 오케스트레이션 도구로, 다수의 컨테이너를 효율적으로 관리할 수 있다. 주요 개념은 다음과 같다.

  • 컨테이너: 애플리케이션과 그 종속성을 함께 패키징한 가상화된 환경
  • Pod: 쿠버네티스에서 실행되는 최소 단위의 배포 객체로, 하나 이상의 컨테이너를 포함
  • 노드(Node): 쿠버네티스 클러스터에서 Pod가 실행되는 물리적 또는 가상 머신
  • 클러스터: 여러 노드로 구성된 쿠버네티스의 집합
  • 네임스페이스(Namespace): 클러스터 내에서 리소스를 논리적으로 구분하는 단위

 쿠버네티스와 Spring Cloud를 비교해본다.

  • 공통점
    • 확장성: 둘 다 마이크로서비스 아키텍처의 확장성을 지원
    • 관리성: 서비스의 배포, 관리, 확장 등을 쉽게 할 수 있도록 도와줌
    • 고가용성: 서비스의 가용성을 높이고, 장애 발생 시 자동 복구를 지원
  • 차이점
    • 초점:
      • Spring Cloud: 마이크로서비스 간의 통신, 서비스 디스커버리, 구성 관리 등 애플리케이션 레벨의 문제 해결에 초점
      • 쿠버네티스: 컨테이너 관리, 배포, 스케일링 등 인프라 레벨의 문제 해결에 초점
    • 구성 요소:
      • Spring Cloud: Eureka, Ribbon, Zuul, Config Server, Hystrix 등 다양한 마이크로서비스 패턴을 지원하는 구성 요소
      • 쿠버네티스: Pod, Deployment, Service, Ingress, ConfigMap, Secret 등 컨테이너 오케스트레이션에 필요한 구성 요소
    • 배포 방식:
      • Spring Cloud: 애플리케이션 코드와 함께 다양한 클라우드 서비스에 직접 배포
      • 쿠버네티스: 컨테이너 이미지를 기반으로 클러스터 내에서 배포 및 관리

 Spring Cloud Kubernetes는 Spring Cloud와 Kubernetes의 통합을 지원한다. 서비스 디스커버리, ConfigMap, Secrets 등을 Spring Cloud 애플리케이션에서 사용할 수 있도록 지원한다. 주요 기능은 다음과 같다.

  • 서비스 디스커버리: Kubernetes API를 통해 서비스 인스턴스를 동적으로 검색하고 로드 밸런싱을 수행
  • 구성 관리: Kubernetes ConfigMap과 Secrets을 사용하여 애플리케이션 설정을 중앙에서 관리
  • 자동화된 배포: CI/CD 파이프라인과 통합하여 애플리케이션의 자동 배포와 관리를 지원

 쿠버네티스의 장단점은 다음과 같다.

  • 장점
    • 확장성: 수평 확장을 통해 대규모 트래픽을 처리할 수 있음
    • 자동화: 배포, 스케일링, 복구 등의 작업을 자동화하여 운영 부담을 줄임
    • 유연성: 다양한 인프라 환경에서 일관된 운영이 가능
  • 단점
    • 복잡성: 초기 설정과 운영에 대한 학습 곡선이 높음
    • 운영 비용: 클러스터 운영과 모니터링에 추가적인 리소스가 필요
    • 디버깅 어려움: 분산 환경에서 문제를 추적하고 해결하는 것이 복잡할 수 있음

댓글남기기