고딩왕 코범석
[MSA] API Gateway 본문
Reverse Proxy
리버스 프록시는 요청을 대신해서 보내는 것이다. 클라이언트의 요청을 받아 적절한 웹 서버로 요청을 전송한다. 요청을 받은 서버는 응답을 전송할 떄, 다시 이 리버스 프록시에게 전달하여 그 응답을 클라이언트에게 제공한다.
대표적으로는 nginx가 우리가 잘 알고있는 리버스 프록시 서버이다.
Api Gateway?
- 사용자가 설정한 라우팅 설정에 따라서 각 엔드 포인트로 클라이언트 대신해서 요청을 하고 응답받으면 다시 클라이언트에게 전송하는 reverse proxy 역할
- 클라이언트가 마이크로서비스를 직접 호출하는 구조는 별로 좋지 않음
- 시스템의 내부 구조는 숨기고 외부의 요청에 대해 적절한 형태로 가공하여 응답을 클라이언트에게 전달하는 장점을 갖고 있다.
- 클라이언트와 각 마이크로서비스 사이에 위치하여 단일 진입점 형태로 개발하며, 각각의 마이크로서비스 로 요청되는 모든 정보에 대해 모든 것들을 처리한다.
- 즉, 클라이언트는 각각의 마이크로서비스들과 통신하는 것이 아닌 api gateway랑 통신하게 된다.
API Gateway가 하게 될 것
- 인증과 권한 부여
- 마이크로서비스들의 검색 통합
- 속도 제한과 부하 분산(클라이언트의 원하는 서비스를 찾아줌)
- 로깅
- 헤더, 쿼리 문자열 및 청구 변환
- IP 허용 목록에 추가
Spring cloud에서의 MSA간 통신
RestTemplate
RestTemplate restTemplate = new RestTemplate(); restTemplate.getForObject("http://localhost:8080/", User.class, 200);
Feign Client
@FeignClient("stores") // 호출하고 싶은 마이크로 서비스 명 작성 public interface StoreClient { @GetMapping("/stores") List<Store> getStores(); }
Ribbon
- Client side Load Balncer
- 클라이언트가 직접 서비스의 이름으로 호출한다.
- 비동기와 호환이 잘 되지 않는다.
- Health Check : 해당 서비스가 잘 작동하는지 확인
- 현재 부트 2.4에서는 Maintenance 상태
Nexflix Zuul
- Routing, Api Gateway 역할
- Netflix Zuul도 부트 2.4에서 Maintenance 상태
Spring Cloud Gateway
Gateway Handler Mapping으로 들어오는 요청들을 적절한 대상으로 라우팅하는 간단하고 효과적인 방법 제공
기존의 내장 서버는 톰캣이었다. 하지만 Gateway의 내장 톰캣은 Netty인 것을 알 수 있다.
Netty는 Non-blocking, Asynchronous 방식이다. 때문에 서블릿 컨테이너나 war로 빌드하면 동작하지 않는다.
Spring Cloud Gateway는 기본적으로 세 가지의 핵심 단위로 구성되어 있다.
- Route : 목적지 URI, 충족해야 될 여러 조건들(Predicates), 각종 필터들로 이루어진 요청을 라우팅할 대상들이라고 가정
- Predicate : 자바 8의 Function Predicate로 이루어져 있다. path 혹은 리퀘스트 헤더에 포함된 조건들을 의미한다. 즉, 라우팅에 필요한 조건!
- Filter : 스프링 프레임워크의 WebFilter의 인스턴스이다. 사용자가 보내는 request, 응답 받는 response를 수정하거나 정제하는 것.
Spring Cloud Gateway의 동작 방식
- 클라이언트는 스프링 클라우드 게이트웨이에 요청을 보낸다.
- Gateway Handler Mapping이 요청 정보가 게이트웨이에 설정된 Route로 전달하는 것을 결정하고, Gateway Web Handler에게 전송한다.
- Gateway Web Handler는 요청에 따른 필터체인을 통해 요청을 보낸다.
- 그림에서 필터들이 점선으로 구분된 이유는, Proxied Service에 요청을 보내기 전에 해당 게이트웨이 서버에서 필터를 통한 로직을 수행해야하기 때문이다. 위에서 말했던 API Gateway가 하게 될 것들이라 생각하면 되겠다.
- 사전에 필터들에 대한 로직들을 수행한 다음, proxied server에 원하는 정보를 얻고 사후 필터들에 대한 로직이 동작하여 클라이언트에게 응답이 간다.
Filter 설정
Java code로 설정
@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();
}
}
- 람다 표현식을 통해 라우팅 설정
- route 메서드 내에 마이크로서비스 path를 설정한다.
- filters 메서드를 통해 request, response header를 각각 추가
- 마지막에 마이크로서비스의 uri로 매핑시켜주기
application.yml로 라우팅 설정
server:
port: 8080
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
filters:
- AddRequestHeader=first-request, first-request-header-withyml
- AddResponseHeader=first-response, first-response-header-withyml
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, second-request-header-withyml
- AddResponseHeader=second-response, second-response-header-withyml
- spring.cloud.gateway.routes : 프로젝트 각각의 마이크로서비스에 대한 라우팅 설정
- id : 마이크로서비스 고유 id
- uri : 실행 중인 마이크로서비스의 uri(포트까지!)
- predicates : 위에서 설명한 조건절
- 만약 'localhost:8080/first-service/hihi' url을 요청하면 yml 파일 설정에 적힌 것 처럼 first-service path는 8081포트를 사용하는 마이크로서비스인 id : first-service로 라우팅되어 있으므로 8081/first-service/hihi로 라우팅된다.
- filters : 위의 자바 코드로 설정하는 방법처럼, 요청이나 응답에 대한 값들을 정제할 수 있다.
개인적으로는 자바코드로 설정하는 것이 훨씬 나아보인다. 컴파일로 내가 잘못 작성했을 경우 에러가 발생하기 때문에!
Custom Filter application.yml
@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
public CustomFilter() {
super(Config.class);
}
public static class Config {
// Put the configuration properties
}
@Override
public GatewayFilter apply(Config config) {
// Custom Prefilter
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Custom Pre filter : request id -> {}", request.getId());
// Custom Postfilter
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
log.info("Custom Post filter : response statusCode -> {}", response.getStatusCode());
}));
};
}
}
Global Filter와 Custom Filter의 차이
각 라우팅 정보마다 커스터마이징한 필터를 등록해주어야 한다. 앞에서 설정했던 Custom Filter가 그렇다. 하지만 Global Filter의 경우 라우팅 정보들 마다 공통적으로 필요한 처리들을 한번의 설정으로 해결해준다.
둘 다 적용되었을 경우의 순서 : Global Filter PRE -> Custom Filter PRE -> Custom Filter POST -> Global Filter POST
application.yml 설정
server:
port: 8080
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
# Global Filter 설정하기
default-filters:
- name: GlobalFilter # 클래스명
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
filters:
- CustomFilter
# - AddRequestHeader=first-request, first-request-header-withyml
# - AddResponseHeader=first-response, first-response-header-withyml
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- CustomFilter
# - AddRequestHeader=second-request, second-request-header-withyml
# - AddResponseHeader=second-response, second-response-header-withyml
LoggingFilter
Api Gateway에서는 로깅을 위한 필터도 존재하는데, 여기서 로깅 필터도 Custom Filter라서 마이크로 서비스 마다 등록을 해주어야 한다. 강의 내용 처럼 second-service에 등록하자.
이 Logging Filter를 등록하게 되면 요청에 대한 흐름은 다음과 같다.
Load Balancer
기본적으로 라운드 로빈 방식으로 각 마이크로서비스를 한번씩 호출한다.
API Gateway에서uri에 lb://서비스명 이라고 설정해두었는데, 이 때 랜덤포트로 설정한 경우는 서비스 명으로 게이트웨이에서 인스턴스를 찾아 각 서비스를 한번씩 호출한다.
설정에 따라 랜덤하게 서버 인스턴스를 호출할 수 있다.
출처
'Language & Framework > Spring' 카테고리의 다른 글
[MSA] Spring Cloud Bus및 설정 정보 암호화 (0) | 2021.05.21 |
---|---|
[MSA] Spring Cloud Config (3) | 2021.05.15 |
[MSA] Service Discovery (0) | 2021.05.06 |
Service Layer 분리하기! (0) | 2021.05.05 |
비동기 이벤트를 테스트하는 방법 With JUnit (0) | 2021.04.10 |