날짜: 2026년 7월 22일
주제: Eureka Client 등록, 다중 인스턴스 실행, API Gateway, Spring MVC와 WebFlux
어제는 Cloud Native Architecture와 MSA의 전반적인 개념을 학습했다. 오늘은 해당 개념을 반복하기보다, Spring Cloud를 이용해 실제 마이크로서비스를 구성하는 과정에 집중했다.
오늘 학습한 주요 내용은 다음과 같다.
MSA에서는 각각의 서비스가 서로 다른 서버와 포트에서 실행된다. 또한 Auto Scaling이나 장애 복구로 인해 서비스 인스턴스의 주소가 계속 달라질 수 있다.
따라서 다른 서비스의 위치를 다음과 같이 코드에 직접 작성하는 것은 관리하기 어렵다.
http://localhost:8081
http://localhost:8082
http://localhost:8083
이 문제를 해결하기 위해 사용하는 것이 Service Discovery이며, 오늘은 Netflix Eureka를 이용해 이를 실습했다.
Eureka Server는 실행 중인 서비스의 이름과 위치를 저장하는 Service Registry 역할을 한다.
Eureka Client는 애플리케이션이 실행될 때 자신의 정보를 Eureka Server에 등록한다.
HELLOWORLD-SERVICE
├── localhost:8081
├── localhost:8082
└── localhost:8083
세 개의 애플리케이션이 서로 다른 포트에서 실행되더라도 같은 서비스 이름을 사용하면 Eureka에서는 동일한 서비스의 여러 인스턴스로 관리할 수 있다.
이를 통해 다른 서비스나 API Gateway는 IP 주소와 포트를 직접 관리하지 않고 서비스 이름을 기준으로 인스턴스를 찾을 수 있다.
Eureka Client로 동작하려면 다음 의존성이 필요하다.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
웹 요청을 처리하기 위한 Spring Web 의존성과 개발 편의를 위한 DevTools도 함께 사용했다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
Spring Cloud 라이브러리는 여러 개의 하위 라이브러리로 구성된다. 각각의 버전을 직접 지정하면 라이브러리 사이에 호환성 문제가 발생할 수 있다.
이를 방지하기 위해 Spring Cloud BOM을 dependencyManagement에 등록한다.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
BOM은 실제 기능을 제공하는 일반 의존성이 아니라, 관련 라이브러리들의 버전을 일관되게 관리하는 역할을 한다.
따라서 spring-cloud-dependencies는 일반 <dependencies>가 아닌 <dependencyManagement>에 작성해야 한다.
<parent>: Spring Boot 프로젝트의 기본 설정과 플러그인 관리<dependencyManagement>: 라이브러리 버전 기준 관리<dependencies>: 프로젝트에서 실제로 사용할 라이브러리 선언클라이언트가 Eureka Server에 등록되려면 애플리케이션 이름과 Eureka Server 주소가 필요하다.
spring:
application:
name: helloworld-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
여기서 spring.application.name은 Eureka에 등록되는 서비스 이름이다.
같은 애플리케이션을 여러 포트로 실행하더라도 이 이름이 같으면 동일한 서비스의 여러 인스턴스로 묶인다.
Eureka Client는 등록 이후 일정 주기로 Heartbeat를 전송한다. Eureka Server는 이를 이용해 각 인스턴스가 정상적으로 실행되고 있는지 확인한다.
오늘은 하나의 helloworld-service를 서로 다른 포트에서 여러 번 실행했다. 이는 실제 운영 환경에서 동일 서비스를 여러 인스턴스로 확장하는 상황을 간단히 실습한 것이다.
먼저 Maven으로 프로젝트를 패키징한다.
mvn clean package
그다음 생성된 JAR 파일을 원하는 포트로 실행할 수 있다.
java -Dserver.port=8082 -jar .\target\helloworld-1.0.jar
여기서 주의할 점은 -Dserver.port 옵션이 반드시 -jar보다 앞에 위치해야 한다는 것이다.
# 올바른 순서
java -Dserver.port=8082 -jar .\target\helloworld-1.0.jar
-Dserver.port=8082는 JVM 시스템 속성으로 Spring Boot의 서버 포트를 덮어쓴다.
패키징된 JAR 파일을 직접 실행하지 않고 Maven 플러그인을 이용할 수도 있다.
mvn spring-boot:run "-Dspring-boot.run.jvmArguments=-Dserver.port=8083"
PowerShell에서는 -Dspring-boot.run.jvmArguments=... 부분을 큰따옴표로 묶어 전달해야 한다.
| 구분 | java -jar | mvn spring-boot:run |
|---|---|---|
| 실행 대상 | 빌드된 JAR 파일 | 현재 Maven 프로젝트 |
| 사전 패키징 | 필요 | 필수는 아님 |
| 주요 사용 시점 | 배포 결과 확인 및 운영 | 개발 및 테스트 |
| 포트 지정 | JVM 시스템 속성 | Maven 플러그인 인수 |
이렇게 8081, 8082, 8083 포트에 같은 서비스를 실행하면 Eureka Server에는 동일한 서비스의 인스턴스가 여러 개 등록된다.
API Gateway는 외부 클라이언트의 요청을 가장 먼저 받아 적절한 마이크로서비스로 전달하는 단일 진입점이다.
클라이언트
↓
API Gateway
├── 회원 서비스
├── 주문 서비스
└── 결제 서비스
Gateway가 없다면 클라이언트가 각 서비스의 IP와 포트를 모두 알아야 한다.
회원 서비스: localhost:8081
주문 서비스: localhost:8082
결제 서비스: localhost:8083
Gateway를 사용하면 클라이언트는 Gateway의 주소만 알고 있으면 된다.
localhost:8000
클라이언트가 여러 마이크로서비스의 주소를 직접 관리할 필요가 없다. 내부 서비스의 위치가 변경되더라도 Gateway의 외부 주소가 유지된다면 클라이언트에 미치는 영향을 줄일 수 있다.
요청 경로에 따라 목적지를 결정한다.
/users/** → USER-SERVICE
/orders/** → ORDER-SERVICE
Eureka와 연동하면 고정 주소 대신 서비스 이름을 사용할 수 있다.
uri: lb://HELLOWORLD-SERVICE
lb는 Load Balancing을 의미한다.
동일한 서비스가 여러 포트에서 실행되고 있다면 요청을 여러 인스턴스에 분산할 수 있다.
HELLOWORLD-SERVICE
├── 8081
├── 8082
└── 8083
이를 통해 한 인스턴스에 요청이 집중되는 것을 막고 서비스의 처리량과 가용성을 높일 수 있다.
각 마이크로서비스에 반복적으로 작성할 수 있는 기능을 Gateway에서 처리할 수 있다.
단, 실제 권한과 중요한 비즈니스 규칙까지 모두 Gateway에만 맡기는 것은 적절하지 않을 수 있다. 각 서비스에서도 필요한 권한을 다시 확인해야 한다.
외부 사용자가 마이크로서비스에 직접 접근하지 않고 Gateway를 통해서만 요청하도록 구성할 수 있다. 내부 서비스의 주소를 숨기고 공개할 API를 통제하기 쉬워진다.
모든 요청이 Gateway를 통과하기 때문에 Gateway에 장애가 발생하면 전체 서비스 이용에 영향을 줄 수 있다. 따라서 운영 환경에서는 Gateway도 여러 인스턴스로 구성하고 장애 복구 및 모니터링 체계를 마련해야 한다.
API Gateway를 학습하면서 Spring MVC 기반과 WebFlux 기반의 요청 처리 방식도 비교했다.
| 구분 | Spring MVC | Spring WebFlux |
|---|---|---|
| 기반 | Servlet Stack | Reactive Stack |
| 처리 방식 | 동기·블로킹 중심 | 비동기·논블로킹 중심 |
| 스레드 모델 | 일반적으로 요청당 스레드 | 적은 이벤트 루프 스레드로 여러 요청 처리 |
| 반환 타입 | 객체, ResponseEntity 등 | Mono, Flux |
| 대표 서버 | Tomcat | Netty |
| 데이터 처리 | JDBC, JPA와 잘 어울림 | R2DBC 등 논블로킹 기술과 잘 어울림 |
| 적합한 서비스 | 일반적인 CRUD | Gateway, 스트리밍, 외부 API 호출 중심 서비스 |
Spring MVC는 일반적으로 하나의 요청을 하나의 스레드가 담당한다.
@GetMapping("/hello")
public String hello() {
return "Hello";
}
DB나 외부 API의 응답을 기다리는 동안 해당 스레드도 대기할 수 있다. 대신 코드 흐름이 직관적이고 JPA를 사용하는 일반적인 CRUD 서비스에 적합하다.
WebFlux는 Reactive Programming을 기반으로 비동기·논블로킹 처리를 지원한다.
@GetMapping("/hello")
public Mono<String> hello() {
return Mono.just("Hello");
}
Mono<T>: 0개 또는 1개의 결과Flux<T>: 0개 이상의 연속된 결과WebFlux는 외부 서비스의 응답을 기다리는 동안 스레드를 계속 점유하지 않고 다른 요청을 처리할 수 있다. 따라서 많은 요청을 전달해야 하는 API Gateway와 잘 어울린다.
WebFlux를 사용한다고 모든 애플리케이션의 성능이 자동으로 향상되는 것은 아니다.
WebFlux 안에서 JDBC나 JPA처럼 블로킹 방식으로 동작하는 기술을 그대로 사용하면 스레드가 결국 대기하므로 논블로킹 처리의 장점이 줄어든다. WebFlux의 효과를 충분히 얻으려면 데이터베이스와 외부 통신을 포함한 전체 호출 흐름이 논블로킹 방식으로 구성되어야 한다.
IntelliJ에서 다음과 같이 Spring Boot Parent POM을 찾지 못하는 오류가 발생했다.
spring-boot-starter-parent를 찾을 수 없습니다.
확인한 해결 방법은 다음과 같다.
-U 옵션을 이용한 의존성 강제 업데이트.m2 저장소에 남은 실패 캐시 확인mvn -U clean package
-U는 Maven이 원격 저장소의 라이브러리와 플러그인을 다시 확인하도록 하는 옵션이다.
다음과 같은 오류도 발생했다.
Unknown lifecycle phase ".run.jvmArguments=-Dserver.port=8083"
이는 Maven이 실행 옵션을 package나 install과 같은 생명주기 단계로 잘못 해석한 것이다.
PowerShell에서 전체 -D 인수를 큰따옴표로 감싸 해결했다.
mvn spring-boot:run "-Dspring-boot.run.jvmArguments=-Dserver.port=8083"
이번 오류를 통해 명령어의 내용뿐만 아니라 사용하는 터미널에 따른 인수 전달 방식도 중요하다는 점을 알게 되었다.
Eureka Client 실행
↓
Eureka Server에 서비스 이름과 위치 등록
↓
같은 서비스를 서로 다른 포트로 여러 번 실행
↓
Eureka가 여러 인스턴스의 위치를 관리
↓
API Gateway가 서비스 이름으로 인스턴스를 탐색
↓
여러 인스턴스에 요청을 분산
Eureka와 API Gateway의 역할은 다음과 같이 구분할 수 있다.
| 구성 요소 | 역할 |
|---|---|
| Eureka Server | 서비스 이름과 위치를 등록하고 제공 |
| Eureka Client | 자신의 서비스 정보를 Eureka에 등록 |
| API Gateway | 외부 요청을 받아 적절한 서비스로 전달 |
| Load Balancer | 동일 서비스의 여러 인스턴스에 요청 분산 |