내일배움캠프

[내일배움캠프] 서비스 디스커버리, 로드 밸런싱, 서킷 브레이커, API 게이트웨이

munsik22 2026. 4. 13. 15:25

📚 목차

    🧩 서비스 디스커버리란?

    서비스 디스커버리

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

    Eureka

    • MSA에서 각 서비스의 위치를 동적으로 관리하는 넷플릭스의 서비스 디스커버리 서버
      • 모든 서비스 인스턴스의 위치를 저장하는 중앙 저장소 역할
      • 서비스 인스턴스의 상태를 주기적으로 확인하여 가용성을 보장
      • 여러 인스턴스를 지원하여 고가용성을 유지할 수 있음
    • Eureka 서버: 서비스 레지스트리를 구성하는 중앙 서버
    • Eureka 클라이언트 설정: 각 서비스는 Eureka 서버에 자신을 등록해야 함
    • 서비스 등록: 각 서비스 애플리케이션은 Eureka 서버에 자신의 위치를 등록함
    • 서비스 디스커버리: 클라이언트 애플리케이션은 Eureka 서버에서 필요한 서비스의 위치를 조회함
    • 헬스 체크
      • Eureka 서버가 주기적으로 서비스 인스턴스의 상태를 확인하여 가용성을 유지
      • 엔드포인트: /actuator/health
    • 장애 처리
      • 서비스 장애 시 Eureka 서버는 해당 인스턴스를 레지스트리에서 제거하여 다른 서비스의 접근을 차단

    Eureka의 고가용성 구성

    • 클러스터 구성
      • Eureka 서버의 고가용성을 위해 여러 인스턴스를 구성할 수 있음
      • 다중 인스턴스로 구성하여 고가용성을 유지하며, 각 인스턴스는 서로를 피어로 등록하여 상호 백업함
      • Eureka 서버를 다중 인스턴스로 구성할 때 각 서버의 피어 설정을 통해 서로를 인식하고 백업할 수 있음

    Eureka 실습하기

    • 프로젝트 생성하기

    • 프로젝트 열기: Gradle 프로젝트 열기로 3개의 프로젝트를 하나의 창에서 관리할 수 있다.

    • [server] EurekaApplication
    @EnableEurekaServer // ← 추가
    @SpringBootApplication
    public class EurekaApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(EurekaApplication.class, args);
        }
    
    }
    • [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/
    • [client.first] application.properties
    spring.application.name=first
    
    server.port=19091
    
    eureka.client.service-url.defaultZone=http://localhost:19090/eureka/
    • [client.second] application.properties
    spring.application.name=second
    
    server.port=19092
    
    eureka.client.service-url.defaultZone=http://localhost:19090/eureka/

    🧩 로드밸런싱이란?

    로드 밸런싱

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

    FeignClient

    • Spring Cloud에서 제공하는 HTTP 클라이언트로, 선언적으로 RESTful 웹 서비스를 호출할 수 있음
    • Eureka와 같은 서비스 디스커버리와 연동하여 동적으로 서비스 인스턴스를 조회하고 로드 밸런싱을 수행함
    • 특징
      • 선언적 HTTP 클라이언트: 인터페이스와 어노테이션을 사용하여 REST API를 호출할 수 있음
      • Eureka 연동: Eureka와 통합하여 서비스 인스턴스 목록을 동적으로 조회하고 로드 밸런싱을 수행
      • 자동 로드 밸런싱: Ribbon이 통합되어 있어 자동으로 로드 밸런싱을 수행

    Ribbon

    • 넷플릭스가 개발한 클라이언트 사이드 로드 밸런서로, 마이크로서비스 아키텍처에서 서비스 인스턴스 간의 부하를 분산시킴
    • 다양한 로드 밸런싱 알고리즘을 지원하며, Eureka와 같은 서비스 디스커버리와 연동하여 사용함
    • 특징
      • 서버 리스트 제공자: Eureka 등으로부터 서비스 인스턴스 리스트를 제공받아 로드 밸런싱에 사용
      • 로드 밸런싱 알고리즘: 라운드 로빈, 가중치 기반 등 다양한 로드 밸런싱 알고리즘 지원
      • Failover: 요청 실패 시 다른 인스턴스로 자동 전환

    FeignClient와 Ribbon 동작 원리

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

    로드 밸런싱 알고리즘

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

    로드 밸런싱 실습하기

    • 프로젝트 생성하기

    같은 방식으로 order 프로젝트도 생성한다.

    • [product] ProductApplication
    @EnableFeignClient // ← 추가
    @SpringBootApplication
    public class ProductApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(ProductApplication.class, args);
    	}
    
    }
    • [product] ProductController
    @RestController
    public class ProductController {
    
        @Value("${server.port}")
        private String port;
    
        @GetMapping("/product/{id}")
        public String getProduct(@PathVariable("id") String id) {
            return "Product " + id + "from port : " + port;
        }
    
    }
    • [product] application.yml (application.properties는 삭제)
    spring:
      application:
        name: product-service
    server:
      port: 19092
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/
    • 여러 포트에서 같은 애플리케이션 실행하는 방법

    • 포트 번호를 통해 로드밸런싱이 되고 있다는 것을 확인할 수 있다.

    • [order] OrderApplication: @EnableFeignClient 추가
    • [order] OrderController
    @RestController
    @RequiredArgsConstructor
    public class OrderController {
    
        private final OrderService orderService;
    
        @GetMapping("/order/{orderId}")
        public String getOrder(@PathVariable String orderId) {
            return orderService.getOrder(orderId);
        }
    
    }
    • [order] OrderService
    @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...";
        }
    }
    • [order] ProductClient
    @FeignClient(name = "product-service")
    public interface ProductClient {
        @GetMapping("/product/{id}")
        String getProduct(@PathVariable("id") String id);
    }
    • [order] application.yml
    spring:
      application:
        name: order-service
    server:
      port: 19091
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/

    호출할 때 마다 바뀐다.

    🧩 서킷 브레이커란?

    서킷 브레이커

    • 마이크로서비스 간의 호출 실패를 감지하고 시스템의 전체적인 안정성을 유지하는 패턴
    • 외부 서비스 호출 실패 시 빠른 실패를 통해 장애를 격리하고, 시스템의 다른 부분에 영향을 주지 않도록 함
    • 상태 변화: Closed → Open → Half-Open
      1. 클로즈드(Closed):
        • 모든 요청을 통과시킴 (기본 상태)
        • 이 상태에서 호출이 실패하면 실패 카운터가 증가함
        • 실패율이 설정된 임계값을 초과하면 서킷 브레이커가 오픈 상태로 전환됨
      2. 오픈(Open):
        • 모든 요청을 즉시 실패로 처리함
        • 이 상태에서 요청이 실패하지 않고 바로 에러 응답을 반환함
        • 설정된 대기 시간이 지난 후 하프-오픈 상태로 전환됨
      3. 하프-오픈(Half-Open):
        • 제한된 수의 요청을 허용하여 시스템이 정상 상태로 복구되었는지 확인함
        • 요청이 성공하면 서킷 브레이커는 클로즈드 상태로 전환됨
        • 요청이 다시 실패하면 서킷 브레이커는 다시 오픈 상태로 전환됨

    Resilience4j

    • 서비스 간의 호출 실패를 감지하고 시스템의 안정성을 유지하는 서킷 브레이커 라이브러리
    • 다양한 서킷 브레이커 기능을 제공하며, 장애 격리 및 빠른 실패를 통해 복원력을 높임
    • 특징
      • 서킷 브레이커 상태: 클로즈드, 오픈, 하프-오픈 상태를 통해 호출 실패를 관리함
      • Fallback: 호출 실패 시 대체 로직을 제공하여 시스템 안정성 확보
        • 시스템의 안정성을 높이고, 장애가 발생해도 사용자에게 일정한 응답을 제공 가능함
        • 장애가 다른 서비스에 전파되는 것을 방지함
      • 모니터링: 서킷 브레이커 상태를 모니터링하고 관리할 수 있는 다양한 도구 제공

    Resilience4j 실습하기

    • 프로젝트 생성하기

    • build.gradle
      • 강의 자료에는 spring-boot-starter-aop라고 명시되었지만 의존성 명칭이 변경되었다. (참고)
    implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
    implementation 'org.springframework.boot:spring-boot-starter-aspectj'
    • Product
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Product {
    
        private String id;
        private String title;
    
    }
    • ProductController
    @RestController
    @RequiredArgsConstructor
    public class ProductController {
    
        private final ProductService productService;
    
    
        @GetMapping("/product/{id}")
        public Product getProduct(@PathVariable("id") String id) {
            return productService.getProductDetails(id);
        }
    }
    • 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"
            );
        }
    
    }
    • application.yml
      • slidingWindowType: COUNT_BASED → 최근 N번 호출 저장 / TIME_BASED → 최근 N초 호출 저장
      • slowCallDurationThreshold: 60000ms(60초) 걸리면 느린 호출로 간주함
      • slowCallRateThreshold: 느린 호출의 비율이 임계값(100%)을 초과하면 서킷 브레이커가 동작함
      • failureRateThreshold: 실패율이 임계값(50%)을 초과하면 서킷 브레이커가 동작함
      • waitDurationInOpenState: Open 상태에서 Half-Open 상태로 전환되기 전에 대기하는 시간
    spring:
      application:
        name: sample
    
    server:
      port: 19090
    
    resilience4j:
      circuitbreaker:
        configs:
          default:
            registerHealthIndicator: true
            slidingWindowType: COUNT_BASED 
            slidingWindowSize: 5
            minimumNumberOfCalls: 5
            slowCallRateThreshold: 100
            slowCallDurationThreshold: 60000
            failureRateThreshold: 50
            permittedNumberOfCallsInHalfOpenState: 3
            waitDurationInOpenState: 20s
    
    management:
      endpoints:
        web:
          exposure:
            include: prometheus
      prometheus:
        metrics:
          export:
            enabled: true

    • 로그 보기

    이벤트 설명 로그 출력
    상태 전환 (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: ...
    • 프로메테우스 보기

    프로메테우스에서 서킷브레이커의 정보를 수집할 수 있다.

    🧩 API 게이트웨이란?

    API 게이트웨이

    • 클라이언트의 요청을 받아 백엔드 서비스로 라우팅하고, 다양한 부가 기능을 제공하는 중간 서버
    • 클라이언트와 서비스 간의 단일 진입점 역할을 하며, 보안, 로깅, 모니터링, 요청 필터링 등을 처리함
    • 주요 기능
      • 라우팅: 클라이언트 요청을 적절한 서비스로 전달
      • 인증 및 권한 부여: 요청의 인증 및 권한을 검증
      • 로드 밸런싱: 여러 서비스 인스턴스 간의 부하 분산
      • 모니터링 및 로깅: 요청 및 응답을 로깅하고 모니터링
      • 요청 및 응답 변환: 요청과 응답을 변환하거나 필터링

    Spring Cloud Gateway

    • 클라이언트 요청을 적절한 서비스로 라우팅하고 다양한 필터링 기능을 제공함
    • Spring Cloud Netflix 패키지의 일부로 MSA에서 널리 사용됨
    • 주요 특징
      • 동적 라우팅: 요청의 URL 패턴에 따라 동적으로 라우팅
      • 필터링: 요청 전후에 다양한 작업을 수행할 수 있는 필터 체인 제공
      • 모니터링: 요청 로그 및 메트릭을 통해 서비스 상태 모니터링
      • 보안: 요청의 인증 및 권한 검증

    Spring Cloud Gateway 필터링

    • 필터 종류
      • Global Filter: 모든 요청에 대해 작동하는 필터
      • Gateway Filter: 특정 라우트에만 적용되는 필터
    • 필터 주요 객체
      1. Mono
        • 리액티브 프로그래밍에서 0 또는 1개의 데이터를 비동기적으로 처리함
        • Mono<Void>는 아무 데이터도 반환하지 않음을 의미힘
      2. ServerWebExchange
        • HTTP 요청과 응답을 캡슐화한 객체
        • exchange.getRequest()로 HTTP 요청을 가져오고 exchange.getResponse()로 HTTP 응답을 가져옴
      3. GatewayFilterChain
        • 여러 필터를 체인처럼 연결함
        • chain.filter(exchange): 다음 필터로 요청을 전달함
    • 필터 시점별 종류
      1. Pre 필터
        • 요청이 처리되기 전에 실행됨
        • 요청을 가로채고 필요한 작업을 수행한 다음 체인의 다음 필터로 요청을 전달함
        • 추가적인 비동기 작업을 수행할 필요가 없기 때문에 then 메서드를 사용할 필요가 없음
      2. Post 필터
        • 요청이 처리된 후 응답이 반환되기 전에 실행됨
        • 체인의 다음 필터가 완료된 후에 실행되어야 하는 추가적인 작업을 수행해야 함
        • chain.filter(exchange)를 호출하여 다음 필터를 실행한 후, then 메서드를 사용하여 응답이 완료된 후에 실행할 작업을 정의함

    Spring Cloud Gateway 실습하기

    • [order] build.gradle: implementation 'org.springframework.boot:spring-boot-starter-actuator' 추가
    • [order] OrderController
    @RestController
    @RequestMapping("/order")
    public class OrderController {
    
        @GetMapping
        public String getOrder() {
            return "Order details";
        }
    }
    • [order] application.yml
    server:
      port: 19092
    
    spring:
      application:
        name: order-service
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/
    • [product] build.gradle: implementation 'org.springframework.boot:spring-boot-starter-actuator' 추가
    • [product] application.yml
    server:
      port: 19093
    
    spring:
      application:
        name: product-service
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka
    • [gateway] 프로젝트 생성

    • [gateway] CustomPreFilter 생성
    @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());
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            return Ordered.HIGHEST_PRECEDENCE;
        }
    }
    • [gateway] CustomPostFilter 생성
    @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());
            }));
        }
    
        @Override
        public int getOrder() {
            return Ordered.LOWEST_PRECEDENCE;
        }
    }
    • [gateway] application.yml
      • /order/** 경로로 들어오는 요청을 order-service라는 이름으로 로드 밸런싱된 서비스로 라우팅
      • /product/** 경로로 들어오는 요청을 product-service라는 이름으로 로드 밸런싱된 서비스로 라우팅
    server:
      port: 19091
    
    spring:
      main:
        web-application-type: reactive
      application:
        name: gateway-service
      cloud:
        gateway:
          routes:
            - id: order-service
              uri: lb://order-service
              predicates:
                - Path=/order/**
            - id: product-service
              uri: lb://product-service
              predicates:
                - Path=/product/**
          discovery:
            locator:
              enabled: true
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/

    [19090] 인스턴스 확인
    [19091] order 서비스 호출 (실패🤯)

    • [gateway] application.yml 수정
      • Spring Cloud Gateway의 버전이 강의자료와 달라지면서 yml 설정도 아래처럼 변경해야 한다. (참고)
      • serverroutes 사이에 webflux 필드를 추가한다.
    server:
      port: 19091
    
    spring:
      application:
        name: gateway-service
      cloud:
        gateway:
          server:
            webflux: # ← 추가
              routes:
                - id: order-service
                  uri: lb://order-service
                  predicates:
                    - Path=/order/**
                - id: product-service
                  uri: lb://product-service
                  predicates:
                    - Path=/product/**
              discovery:
                locator:
                  enabled: true
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/

    • [gateway] build.gradle 수정
      • spring-boot-starter-web 의존성을 끄지 않으면 위 사진처럼 에러가 발생하며 실행에 실패한다.
    dependencies {
    	implementation 'org.springframework.boot:spring-boot-starter-actuator'
    //	implementation 'org.springframework.boot:spring-boot-starter-webmvc' // ← 삭제
    	implementation 'org.springframework.cloud:spring-cloud-starter-gateway-server-webflux'
    	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    	compileOnly 'org.projectlombok:lombok'
    	annotationProcessor 'org.projectlombok:lombok'
    	testImplementation 'org.springframework.boot:spring-boot-starter-actuator-test'
    //	testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' // ← 삭제
    	testImplementation 'io.projectreactor:reactor-test'
    	testCompileOnly 'org.projectlombok:lombok'
    	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    	testAnnotationProcessor 'org.projectlombok:lombok'
    }

    [19091] order 서비스 호출
    [19091] product 서비스 호출
    게이트웨이 로그에서 필터가 동작하는 것을 확인할 수 있다.