
📚 목차
🧩 서비스 디스커버리란?
서비스 디스커버리
- 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 동작 원리
- 서비스 이름:
@FeignClient(name = "my-service")어노테이션은 Eureka에 등록된 서비스 이름을 참조함 - 서비스 인스턴스 조회: Eureka 서버에서
my-service라는 이름으로 등록된 서비스 인스턴스 목록을 조회함 - 로드 밸런싱: 조회된 서비스 인스턴스 목록 중 하나를 선택하여 요청을 보냄 (기본적으로 Ribbon을 사용)
- 요청 분배: 여러 서비스 인스턴스가 있을 경우, Round Robin 등의 로드 밸런싱 알고리즘을 사용하여 요청을 분배함
로드 밸런싱 알고리즘
- 라운드 로빈: 각 서버에 순차적으로 요청을 분배하는 방식으로, 간단하고 공평하게 트래픽을 분산시킴
- 가중치 기반 로드 밸런싱: 각 서버에 가중치를 부여하고 가중치에 비례하여 요청을 분배하는 방식으로, 서버의 성능이나 네트워크 상태에 따라 가중치를 조절함
- 최소 연결: 현재 연결된 클라이언트 수가 가장 적은 서버로 요청을 보내는 방식
- 응답 시간 기반: 서버의 응답 시간을 기준으로 가장 빠른 서버로 요청을 보내는 방식
로드 밸런싱 실습하기

- 프로젝트 생성하기


- [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
- 클로즈드(Closed):
- 모든 요청을 통과시킴 (기본 상태)
- 이 상태에서 호출이 실패하면 실패 카운터가 증가함
- 실패율이 설정된 임계값을 초과하면 서킷 브레이커가 오픈 상태로 전환됨
- 오픈(Open):
- 모든 요청을 즉시 실패로 처리함
- 이 상태에서 요청이 실패하지 않고 바로 에러 응답을 반환함
- 설정된 대기 시간이 지난 후 하프-오픈 상태로 전환됨
- 하프-오픈(Half-Open):
- 제한된 수의 요청을 허용하여 시스템이 정상 상태로 복구되었는지 확인함
- 요청이 성공하면 서킷 브레이커는 클로즈드 상태로 전환됨
- 요청이 다시 실패하면 서킷 브레이커는 다시 오픈 상태로 전환됨
- 클로즈드(Closed):
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.ymlslidingWindowType: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: 특정 라우트에만 적용되는 필터
- 필터 주요 객체
- Mono
- 리액티브 프로그래밍에서 0 또는 1개의 데이터를 비동기적으로 처리함
Mono<Void>는 아무 데이터도 반환하지 않음을 의미힘
- ServerWebExchange
- HTTP 요청과 응답을 캡슐화한 객체
exchange.getRequest()로 HTTP 요청을 가져오고exchange.getResponse()로 HTTP 응답을 가져옴
- GatewayFilterChain
- 여러 필터를 체인처럼 연결함
chain.filter(exchange): 다음 필터로 요청을 전달함
- Mono
- 필터 시점별 종류
- Pre 필터
- 요청이 처리되기 전에 실행됨
- 요청을 가로채고 필요한 작업을 수행한 다음 체인의 다음 필터로 요청을 전달함
- 추가적인 비동기 작업을 수행할 필요가 없기 때문에
then메서드를 사용할 필요가 없음
- Post 필터
- 요청이 처리된 후 응답이 반환되기 전에 실행됨
- 체인의 다음 필터가 완료된 후에 실행되어야 하는 추가적인 작업을 수행해야 함
chain.filter(exchange)를 호출하여 다음 필터를 실행한 후,then메서드를 사용하여 응답이 완료된 후에 실행할 작업을 정의함
- Pre 필터
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/



- [gateway]
application.yml수정- Spring Cloud Gateway의 버전이 강의자료와 달라지면서 yml 설정도 아래처럼 변경해야 한다. (참고)
server와routes사이에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'
}



'내일배움캠프' 카테고리의 다른 글
| [내일배움캠프] Windows에서 Docker 설치하기 (1) | 2026.04.14 |
|---|---|
| [내일배움캠프] 보안 구성, Config 서버, 분산 추적, 이벤트 드리븐 (1) | 2026.04.14 |
| [내일배움캠프] MSA와 Spring Cloud (0) | 2026.04.11 |
| [내일배움캠프] 예외 처리 (0) | 2026.04.10 |
| [내일배움캠프] Spring AOP (0) | 2026.04.10 |