
해당 포스팅은 인프런에 "Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)" 강의를 기반으로 작성됐습니다.
포스팅의 모든 사진 자료는 해당 강의 출처임을 밝힙니다.
스터디를 진행함에 따라서 각자 좋은 정보와 의견들이 오고 가는 것 같아서 너무 긍정적이고 좋습니다.
어제는 한 스터디원 분께서 회의록을 작성하자는 좋은 의견이 나와서 이를 오늘 스터디 때 적용해보기로 했습니다.
오늘은 어제에 이어서 MSA 내에서 프록시의 역할을 해주고, 마이크로 서비스로 라우팅을 담당하는 API Gateway 서버에 대해서 학습하겠습니다.
📖 학습 목표
- API Gateway에 대한 이해
- API Gateway의 발전 과정 이해
- SPring Cloud Gateway에 대한 이해
- Spring Cloud Gateway에 필터에 대한 이해

위 그림은 클라이언트 측에서 바로 Microservices를 호출하고 있습니다.
이처럼 직접 호출하게 될 경우, 문제가 발생합니다.
만약, 프로트 서버에서 직접 Microservices의 엔드 포인트를 직접 주소로 호출한 경우, Microservices 엔트 포인트 수정하면 Front Server에 작성한 엔드 포인트도 수정 후 재빌드, 배포되어야 합니다.

위처럼 프론트에서 직접 서버로 요청하는방식의 한계를 해결 하기위해 의 역할로써 'API Gateway'를 사용합니다.
사용자가 설정한 라우팅 설정에 따라서 각각의 엔드포인트로 클라이언트를 대신하여 요청을 수행하고, 응답을 클라이언트에게 돌려주는 Proxy 역할을 합니다.

MicroService에서 매번 인증,인가를 거치지 않고, API Gateway에서 일괄 처리 가능.
일괄적인 정책, 특정 MicroService에서 문제 발생 시 회로 차단, 트래픽 처리를 다시 시도.
Microservices 호출 요청, 응답 관련 경로 등을 추적 및 로그에 기록.
허용 또는 차단 IP를 확인하는 방화벽 같은 역할.
Spring Cloud 에서 MS 간의 통신을 위해 사용되는 방식으로 2가지가 있습니다.


Spring Cloud 초기에 제공하던 클라이언트 사이드 로드 밸런서입니다.
외부에 있는 Microservices를 호출하기 위해서 IP, port가 아닌 서비스명으로 호출하는 기능을 제공했다는 장점이 있습니다. + Health Check 제공.
비동기 처리를 지원하지 않는 문제가 있어 SpringBoot 2.4부터 Maintenance 된 상태입니다.

API의 단일 진입점이 되어 Routing, Monitoring, 인증 등의 기능을 수행합니다.
Zuul 1.X 버전은 동기식 API를 사용하며, 이로 인한 성능 저하 문제가 발생할 수 있습니다.
또한, Zuul은 비동기 처리를 지원하지 않는 문제가 있어 SpringBoot 2.4부터 Maintenance 된 상태입니다.
Spring Cloud Gateway는 비동기 및 논블록킹 I/O 모델을 사용하여 높은 성능과 확장성을 제공합니다.
이를 통해 이전에 Netflix Zuul에서 겪는 문제들을 해결하고 더 나은 기능을 제공합니다.
Intelli J
Java 17
Spring Boot 3.2.4 ver.
gradle(강의에서는 Maven 사용)

Gateway, Eureka Discovery Client, Lombok
⚠️ 주의
SpringBoot 3.2.4 버전 기준 Spring Cloud Routing - Gateway 의존 추가 시, "spring-cloud-starter-gateway-mvc" 라는 의존이 추가되므로, 확인 후
spring-cloud-starter-gateway로 수정 해주세요!
위의 주의사항을 간과하게 되면, 의존이 추가되지 않아 RouteLocator와 같은 클래스 사용이 불가능합니다.
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
cloud:
gateway:
routes:
- id: first-service # id지정
uri: http://127.0.0.1:8081/ #포워딩할 uri
predicates: # 조건절과 같은 의미
- Path=/first-service/** #Path 정보를 지정
- id: second-service
uri: http://127.0.0.1:8082/
predicates:
- Path=/second-service/**
💡 spring.cloud.gateway.routes 설정
- id : 게이트 웨이에서 routes를 구분하기 위한 id
- uri : 포워딩할 uri 지정
- prdicates : 요청 온 경로가 일치할 경우, uri 경로로 포워딩.
설정 후, Gateway 서버를 실행하여 테스트 결과는 다음과 같습니다.
요청 : http://localhost:8000/first-service/welcome
응답 : 404...?
404 응답 상태 코드는 서버 내에서 해당 리소스를 찾지 못할 경우 발생하는 상태 코드입니다.
why? 왜 이러는 걸까요?
이는 Spring Cloud Gateway에서 경로를 라우팅해주는 방식에 대해 이해해보면 됩니다.
Predicates로 등록된 경로로 요청이 들어오게 되면 Uri 등록된 곳으로 포워딩 됩니다.
이 때, 뒤에 Predicate로 등록된 경로도 함께 추가되어 포워딩 됩니다.
이러한 이유로 인해 first-service 또는 second-service에 Controller 측에 매핑 주소를 같도록 @RequestMapping("first-service")를 추가해줘야 합니다.

위 그림은 Spring Cloud Gateway 통해 Client에서 MS로 통신이 수행되는 그림입니다.
Spring Cloud gateway의 내부 동작을 자세하게 보면 다음과 같습니다.
Gateway Handler Mapping : 요청을 받습니다.
Predicate : 조건에 맞는 Microservice로 요청을 포워딩합니다.
Prefilter : 포워딩되는 과정에서 Microservice로 도달하기 전에 거치는 필터입니다.
Postfilter : Microservice에서 가공된 후에 거치는 필터입니다.
Routes 관련 설정하는 방법에는 2가지가 있습니다.
Property - 설정 파일을 통한 설정(application.yml)
JavaCode - @Configuration + 클래스 정의를 통한 설정
이번 시간에 저희는 두 방식 모두 테스트 해볼겁니다.
우선, 이전에 application.yml에 설정한 내용들을 주석처리 해줍니다.

그 다음 FilterConfig를 정의하여 @Configuration 어노테이션을 추가해줍니다.
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder){
return builder.routes()
.route(r -> r.path("/first-service/**")
.filters(f -> f.addRequestHeader("first-request","first-request-header")
.addResponseHeader("first-response","first-response-header"))
.uri("http://localhost:8081"))
.route(r->r.path("/second-service/**")
.filters(f->f.addRequestHeader("second-request","second-request-header")
.addResponseHeader("second-response","second-response-header"))
.uri("http://localhost:8082"))
.build();
}
}
RouteLocator를 빈으로 등록함으로써 application.yml에 설정을 등록하는 것과 동일한 설정이 가능합니다.
route 정보를 등록 시, RouteLocatiorBuilder 를 통해 RouteLocator 객체를 빌드하여 추가해줍니다.
구현 방식은 Lambda식의 함수형 객체(익명 객체)의 형태로 구현하게 됩니다.
filters의 매개변수로 Function 형태의 람다식 객체를 받습니다.
해당 람다식 내 인자 중 GatewayFilterSpec 이라는 클래스를 통해서 다음과 같은 설정이 추가될 수 있습니다.
요청 헤더 추가
요청 파라미터 추가
응답 헤더 추가
위의 설정 이외에도 여러 설정이 있으며, 해당 클래스 내부 메서드를 확인하시면 기능들 확인이 가능합니다.
포워딩할 URI 설정은 uri()를 통해서 설정해줍니다.



위처럼 요청 관련 로그가 각 서비스의 콘솔에 찍히는 것을 확인할 수 있고,
응답 헤더에 값이 추가된 것을 알 수 있습니다.
사용했던 FilterConfig 클래스의 @Configuration 과 @Bean 어노테이션을 주석처리 해줍니다.
위 2개의 어노테이션만 주석처리해도 Spring 컨테이너에서 빈으로 등록하지 않습니다.

application.yml 파일의 주석을 풀어준 뒤, 다음과 같은 설정을 추가해줍니다.
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
filters: # 추가1
- AddRequestHeader=first-request, first-request-header2
- AddResponseHeader=first-response, first-response-header2
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters: # 추가2
- AddRequestHeader=second-request, second-request-header2
- AddResponseHeader=second-response, second-
위 코드는 이전에 JavaCode에서 추가했던 " f -> f.addRequestHeader(key, value).addResponseHeader(key, value) " 메서드와 같은 기능을 합니다.
마지막으로 각 마이크로서비스에 다음과 같이 @GetMpping으로 처리된 message 라는 메서드를 정의해줍니다.

이번에는 브라우저 대신에 Postman 이라는 Api 테스트 툴을 활용하여 진행해보겠습니다.


테스트 결과는 성공적으로 text와 response 헤더에 값이 추가되는 것을 알 수 있습니다.
@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
public GlobalFilter(){
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
//Custom Pre Filter
return (exchange, chain) -> {
ServerHttpRequest request= exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Global Filter baseMessage : {}", config.getBaseMessage());
if(config.isPreLogger()){
log.info("Global Filter Start : request id -> {}",request.getId());
}
// Custom Post Filter
return chain.filter(exchange).then(Mono.fromRunnable(()->{
if(config.isPostLogger()){
log.info("Global Filter End : response code -> {}",response.getStatusCode());
}
}));
};
}
@Data
public static class Config{
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
GatewayFilter filter = new OrderedGatewayFilter((exchange, chain) -> {
ServerHttpRequest request= exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Logging Filter baseMessage : {}", config.getBaseMessage());
if(config.isPreLogger()){
log.info("Logging Filter Start : request id -> {}",request.getId());
}
return chain.filter(exchange).then(Mono.fromRunnable(()->{
if(config.isPostLogger()){
log.info("Logging Filter End : response code -> {}",response.getStatusCode());
}
}));
}, // GatewayFilter 구현체
Ordered.LOWEST_PRECEDENCE); // 순서값

이번에는 Service Discovery에 Api Gateway 서버를 연동 및 등록하도록 하겠습니다.
Spring Cloud Gateway, First-Service, Second-Service에 유레카 서버의 Client로 등록해줍니다.
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
위처럼 Eureka 서버 관련 설정을 추가해줍니다.
- id: first-service
# uri: http://localhost:8081/
uri: lb://MY-FIRST-SERVICE # 변경
predicates:
- Path=/first-service/**
filters:
- CustomFilter
Eureka Client를 Api Gatewy에 application.yml 파일에 uri를 수정해줍니다.
uri 를 기존에 first-service에 경로로 설정하던 부분을 Eureka Server에 등록된 application.name으로 변경해줍니다.
위에서 lb 는 LoadBalancer의 약어로써, Eureka Server에 등록된 서버 중 네이밍이 같은 서비스를 찾아서 포워딩 해줌을 의미합니다.

Microservice 들이 Eureka Server에 정상 등록된 것을 알 수 있습니다.


포트스맨을 사용하여 테스트한 결과 정상적으로 Api Gateway를 통해서 포워딩되는 것을 알 수 있습니다.
이전 포스팅에서 활용했던것과 같이 동일한 서버를 추가하는 방법은 다음과 같습니다.
1)
2)
cd를 통해서 해당 프로젝트 경로로 이동해줍니다.
$ mvn spring-boot:run -Dspring-bbot.run.jvmArguments'-Dserver.port=9003' (maven 방식)
-./gradlew bootRun --args='--server.port=9003'(Gradle 방식)
위처럼 사용해주면, 명령 프롬프트에서 다른 포트 번호로 실행이 가능합니다.
3)
$ mvn clean compile package(maven 방식)
$ java-jar -Dserver.port=9004 ./target/user-service-0.0.1-SNAPSHOT.jar(maven)
./gradlew clean build(Gradle 방식)
$ java -jar -Dserver.port=9002 ./build/libs/second-service-0.0.1-SNAPSHOT.jar
Gradle의 경우 빌드하면, build 디렉토리 내부에 libs라는 디렉토리에 생성됩니다.
실행 후에 Eureka Server를 보시면 다음과 같이 새로운 인스턴스가 등록되어 있습니다.

server:
port: 0
지정 후 인스턴스를 다시 기동해주면, 다음과 같이 유레카 서버에 등록됩니다.

현재 실행중인 서버 포트를 확인하기 위해서 요청 시에 port 번호를 콘솔로 찍어주도록 UserController의 check 메서드를 수정하겠습니다.
@GetMapping("check")
public String check(HttpServletRequest request){
log.info("Server port = {}",request.getServerPort()); //1
return String.format("Hi, there. This is a message from First Service on PORT %s",
environment.getProperty("local.server.port")); //2
}
port 정보를 가져오기 위해서 위와 같이 2가지 방식이 있습니다.
Environment 객체 활용
HttpServletRequest 객체 활용
postman으로 테스트한 결과는 다음과 같습니다.


요청을 보내보면, port 번호가 바뀌면서 요청이 들어가는 것을 확인할 수 있습니다.
API Gateway에서 으로 한번은 49265 , 다른 한번은 49280 으로 요청을 보내주게 됩니다.

이것으로 API Gateway 역할을 해주는 Spring Cloud Gateway에 대한 학습을 마치겠습니다.
이번 실습을 통해서 실제 어떤 방식으로 로드 밸런싱이 수행되고, 어떻게 해당 인스턴스를 찾는지에 대해서 알 수 있었던 과정이었습니다.
너무 재밌고 다음 포스팅에서는 드디어 Micro-service를 구현하게 됩니다.
다들 다음에도 기대 많이해주세요!
💪 포스팅에 잘못된 내용이 포함된 경우 댓글로 말씀해주세요! :)