FeignClient with Eureka
RECOMMEND POSTS BEFORE THIS
1. Practice
FeignClient와 Eureka를 함께 사용하는 예제입니다.
Eureka 서버와 클라이언트를 만들어 연결하는 예제는 Spring Cloud Netflix Eureka 포스트를 참고하시기 바랍니다.
1.1. Context of Practice
다음과 같은 연습 환경을 구축하였습니다.
Eureka서버가 클라이언트 서비스들을 관리합니다.서비스A인스턴스 1개,서비스B인스턴스 2개를 관리합니다.
- 클라이언트 서비스들은 서로 간의 통신을 위해
FeignClient를 사용합니다. - 사용자는
서비스A로 API 요청을 수행합니다.- 사용자 역할은
JUnit프레임워크를 사용한 테스트 코드로 대체합니다. - 100회 API 요청을 반복 수행합니다.
- 사용자 역할은
서비스A는 요청을 처리하는 과정 중간에서비스B에게 API 요청을 수행합니다.서비스B는 자신의 포트(port) 정보와 상태를 응답합니다.
서비스A는 응답 받은서비스B의 상태와 자신의 상태를 사용자에게 함께 전달합니다.
1.2. Setup Service A
다음 작업들은 서비스A 모듈에서 수행합니다.
1.2.1. application.yml
- 포트 번호는 8080으로 고정합니다.
Eureka클라이언트로써a-service-as-client이름을 사용합니다.
server:
port: 8080
spring:
application:
name: a-service-as-client
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka-service:8761/eureka/
1.2.2. HealthClient Interface
FeignClient이름을Eureka서버에 등록된 클라이언트 이름으로 지정합니다.- 해당 이름을 가진
Eureka클라이언트에게 API 요청을 수행합니다.Eureka클라이언트 이름을 사용하면 별도의 URL 정보가 필요 없습니다.
- 해당
FeignClient를 사용하면 자동으로b-service-as-client서비스에게 요청을 수행합니다.
package cloud.in.action.client;
import cloud.in.action.domain.Health;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "b-service-as-client")
public interface HealthClient {
@GetMapping(path = "/health")
Health health();
}
1.2.3. HealthController Class
- 현재 자신의 상태와 클라이언트로부터 받은
서비스B상태를 함께 전달합니다.
package cloud.in.action.controller;
import cloud.in.action.client.HealthClient;
import cloud.in.action.domain.Health;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
public class HealthController {
private final Environment environment;
private final HealthClient healthClient;
public HealthController(Environment environment, HealthClient healthClient) {
this.environment = environment;
this.healthClient = healthClient;
}
@GetMapping("/health")
public List<Health> health() {
int port = Integer.parseInt(environment.getProperty("local.server.port"));
Health healthOfServiceA = Health.builder()
.serviceName("SERVICE-A")
.port(port)
.status("OK")
.build();
Health healthOfServiceB = healthClient.health();
return Arrays.asList(healthOfServiceA, healthOfServiceB);
}
}
1.2.3. AServiceApplication Class
FeignClient를 사용하기 위해@EnableFeignClients애너테이션을 추가합니다.Eureka서버에 등록하기 위해@EnableEurekaClient애너테이션을 추가합니다.
package cloud.in.action;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@EnableEurekaClient
@SpringBootApplication
public class AServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AServiceApplication.class, args);
}
}
1.3. Setup Service B
다음 작업들은 서비스B 모듈에서 수행합니다.
1.3.1. application.yml
0으로 지정하여 임의의 포트 번호를 사용합니다.Eureka클라이언트로써b-service-as-client이름을 사용합니다.
server:
port: 0
spring:
application:
name: b-service-as-client
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka-service:8761/eureka/
1.3.2. HealthController Class
- 현재 자신의 상태를 전달합니다.
package cloud.in.action.controller;
import cloud.in.action.domain.Health;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HealthController {
private final Environment environment;
public HealthController(Environment environment) {
this.environment = environment;
}
@GetMapping("/health")
public Health health() {
int port = Integer.parseInt(environment.getProperty("local.server.port"));
return Health.builder()
.serviceName("SERVICE-B")
.port(port)
.status("OK")
.build();
}
}
1.3.3. BServiceApplication Class
Eureka서버에 등록하기 위해@EnableEurekaClient애너테이션을 추가합니다.
package cloud.in.action;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient
@SpringBootApplication
public class BServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BServiceApplication.class, args);
}
}
2. Test
도커 컴포즈(docker compose)로 테스트 환경을 구축합니다.
2.1. docker-compose.yml
version: "3.9"
services:
eureka-service:
build: ./eureka
ports:
- "8761:8761"
a-service:
build: ./a-service
ports:
- "8080:8080"
b-service:
build: ./b-service
2.2. Run Docker Compose
서비스B는 두 개의 인스턴스가 필요합니다.
--scale 옵션을 사용합니다.
$ docker-compose up -d --scale b-service=2
[+] Building 19.9s (31/31) FINISHED
=> [2021-03-08-spring-cloud-netflix-eureka-b-service internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 32B 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-eureka-service internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 32B 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-a-service internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 32B 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-b-service internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-a-service internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-eureka-service internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-b-service internal] load metadata for docker.io/library/openjdk:11-jdk-slim-buster 4.5s
=> [2021-03-08-spring-cloud-netflix-eureka-eureka-service internal] load metadata for docker.io/library/maven:3.8.6-jdk-11 4.4s
=> [2021-03-08-spring-cloud-netflix-eureka-eureka-service stage-1 1/3] FROM docker.io/library/openjdk:11-jdk-slim-buster@sha256:863ce6f3c27a0a50b458227f23beadda1e7178cda0971fa42b50b05d9a5dcf55 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-eureka-service maven_build 1/6] FROM docker.io/library/maven:3.8.6-jdk-11@sha256:805f366910aea2a91ed263654d23df58bd239f218b2f9562ff51305be81fa215 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-eureka-service internal] load build context 0.0s
=> => transferring context: 674B 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-a-service internal] load build context 0.0s
=> => transferring context: 2.52kB 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-b-service internal] load build context 0.1s
=> => transferring context: 945B 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-a-service stage-1 2/3] WORKDIR /app 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-b-service maven_build 2/6] WORKDIR /build 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-eureka-service maven_build 3/6] COPY pom.xml . 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-eureka-service maven_build 4/6] RUN mvn dependency:go-offline 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-eureka-service maven_build 5/6] COPY src ./src 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-eureka-service maven_build 6/6] RUN mvn package -Dmaven.test.skip=true 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-eureka-service stage-1 3/3] COPY --from=MAVEN_BUILD /build/target/*.jar ./app.jar 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-a-service] exporting to image 0.3s
=> => exporting layers 0.2s
=> => writing image sha256:c45f0e1fc9ae655643cd937215ccecb4e5703b984ba30860e1e236c97e2b26ad 0.0s
=> => naming to docker.io/library/2021-03-08-spring-cloud-netflix-eureka-eureka-service 0.0s
=> => writing image sha256:d222a99bf00bccbe8e45860be7c56b3de1280bba9ce2ddf2ef1ef94b27178bc0 0.0s
=> => naming to docker.io/library/2021-03-08-spring-cloud-netflix-eureka-b-service 0.0s
=> => writing image sha256:24df9e5960e463d73f59d6acba48838f5d4c3816eeaf07471b1301282b882376 0.0s
=> => naming to docker.io/library/2021-03-08-spring-cloud-netflix-eureka-a-service 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-a-service maven_build 3/6] COPY pom.xml . 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-a-service maven_build 4/6] RUN mvn dependency:go-offline 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-b-service maven_build 3/6] COPY pom.xml . 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-b-service maven_build 4/6] RUN mvn dependency:go-offline 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-b-service maven_build 5/6] COPY src ./src 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-b-service maven_build 6/6] RUN mvn package -Dmaven.test.skip=true 0.0s
=> CACHED [2021-03-08-spring-cloud-netflix-eureka-b-service stage-1 3/3] COPY --from=MAVEN_BUILD /build/target/*.jar ./app.jar 0.0s
=> [2021-03-08-spring-cloud-netflix-eureka-a-service maven_build 5/6] COPY src ./src 0.1s
=> [2021-03-08-spring-cloud-netflix-eureka-a-service maven_build 6/6] RUN mvn package -Dmaven.test.skip=true 14.2s
=> [2021-03-08-spring-cloud-netflix-eureka-a-service stage-1 3/3] COPY --from=MAVEN_BUILD /build/target/*.jar ./app.jar 0.1s
[+] Running 4/4
- Container 2021-03-08-spring-cloud-netflix-eureka-b-service-2 Started 1.6s
- Container 2021-03-08-spring-cloud-netflix-eureka-eureka-service-1 Started 0.9s
- Container 2021-03-08-spring-cloud-netflix-eureka-a-service-1 Started 1.1s
- Container 2021-03-08-spring-cloud-netflix-eureka-b-service-1 Started 0.7s
2.3. JUnit Test
서비스A에게 상태 정보를 100회 요청합니다.- 응답 정보를 서비스 이름, 포트 정보로 그룹핑(grouping)합니다.
- 각 서비스로부터 몇 번 응답 받았는지 로그를 통해 확인합니다.
package cloud.in.action;
import cloud.in.action.domain.Health;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@FeignClient(name = "test-client", url = "http://localhost:8080")
interface TestClient {
@GetMapping(path = "/health")
List<Health> health();
}
@Slf4j
@SpringBootTest
class AServiceApplicationTests {
@Autowired
TestClient testClient;
@Test
void health_check() {
List<Health> healthList = new ArrayList<>();
for (int index = 0; index < 100; index++) {
healthList.addAll(testClient.health());
}
Map<String, List<Health>> result = healthList.stream()
.collect(
Collectors.groupingBy(
(health) -> String.format("%s:%s", health.getServiceName(), health.getPort())
)
);
result.entrySet()
.stream()
.forEach((entry) -> {
log.info(String.format("Response count from (%s) - %s", entry.getKey(), entry.getValue().size()));
});
}
}
Test Result
서비스A로부터 100회 응답을 받습니다.- 두 개의
서비스B로부터 각 50회씩 응답을 받습니다.- 포트 번호를 통해 서로 다른 인스턴스로부터 응답 받았음을 확인합니다.
2023-02-05 11:26:59.818 INFO 12080 --- [ main] c.in.action.AServiceApplicationTests : Response count from (SERVICE-B:43283) - 50
2023-02-05 11:26:59.818 INFO 12080 --- [ main] c.in.action.AServiceApplicationTests : Response count from (SERVICE-B:44105) - 50
2023-02-05 11:26:59.818 INFO 12080 --- [ main] c.in.action.AServiceApplicationTests : Response count from (SERVICE-A:8080) - 100
CLOSING
상태 정보에 포트 정보를 추가한 이유는 자동으로 이뤄지는 부하 분산(load balance)에 대해 이야기하고 싶었기 때문입니다.
테스트 수행 결과를 보면 서비스A는 두 개의 서비스B에게 고르게 요청을 분산한 것을 볼 수 있습니다.
FeignClient와 Eureka 컴포넌트를 함께 사용하면 자동으로 부하 분산이 이뤄집니다.
FeignClient는 내부에Ribbon이라는 클라이언트 사이드 로드 밸런서 라이브러리를 사용하고 있습니다.Ribbon라이브러리는Eureka서버를 통해 각 서비스로의 요청 횟수를 판단할 수 있습니다.
댓글남기기