API Gateway Service의 역할 :
- 클라이언트 대신 엔드포인트를 요청하고 요청을 받으면 다시 클라이언트에게 전달해주는 프락시 역할을 해준다.
- 시스템 내부 구조는 숨기고 요청에 대해 적절히 가공해서 응답할 수 있다는 장점
=> 클라이언트가 직접적으로 microservice를 호출하지 않고 게이트웨이만 상대하도록 한다.
- 인증 및 권한 부여
- 서비스 검색 통합
- 응답 캐싱
- 정책, 회로 차단기
- 속도 제한
- 부하 분산
- 로깅, 추적, 상관관계
- 헤더, 쿼리 문자열 및 청구 변환
- ip 허용 목록에 추가
Netflix Ribbon
Spring cloud 에서의 MSA 간 통신
1) RestTemplate
- 자주 사용 : 인스턴스 만들고 get/post 등의 요청으로 보낼 수 있다.
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject("http://localhost:8080/", User.class, 200);
2) Feign Client
- Spring cloud에서는 Feign Client라는 API를 이용할 수 있다.
@FeignClient("stores")
public interface StoreClient {@RequestMapping(method=ReqeustMethod.GET, value="/stores")
List<Store> getStores();
}
인터페이스 생성
-> 애노테이션으로 호출하고 싶은 microservice 등록
-> 따로 주소를 다 쓰지 않고, 마치 자신의 서비스에서 쓰듯이 접속할 수 있다.
Ribbon: Client side Load Balancer
- 서비스 이름으로 호출
- 중간에 있는 API Gateway를 클라이언트 사이드로 옮겨온 방식
-> 비동기 방식으로 호출이 잘 안돼서 최근 사용 드물
-> Spring boot 2.4 -> maintenance
Netflix Zuul
- API Gateway 역할
ribbon과 zuul에 대한 대안 :
Spring Boot 2.4부터는 Spring Cloud Gateway가 공식적으로 권장되는 API 게이트웨이로 소개되었다. 따라서 Spring Boot 2.7.12 버전을 사용하는 경우, Spring Cloud Gateway를 대안으로 사용하는 것이 좋다. Spring Cloud Gateway는 Netflix Zuul과 비슷한 역할을 수행하면서도 더욱 유연하고 성능에 최적화되어 있다.
API Gataway 작동 방식을 확인하기 위해 버전을 낮춰서 zuul을 사용해보자.
먼저 서비스를 사용할 서버 두개를 생성한다.
first-service
second-service
lombok만 받고 컨트롤러만 구현해준다.
@RestController
@RequestMapping("/")
public class FirstServiceController {
@GetMapping("/welcome")
public String welcome(){
return "welcome to the First service";
}
}
yml 파일에 각각 포트를 8081과 8082로 설정하였다.
zuul 서버를 생성한다.
현재 2.4 보다 미만 버전을 spring.io에서 제공하지 않기 때문에 직접 버전을 낮추고 해당하는 version에 맞는 의존성을 추가해주어야 했다.
plugins {
id 'java'
id 'org.springframework.boot' version '2.3.8.RELEASE'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-netflix-zuul
implementation group: 'org.springframework.cloud', name: 'spring-cloud-netflix-zuul', version: '2.2.8.RELEASE'
}
tasks.named('test') {
useJUnitPlatform()
}
yml
server:
port: 8000
spring:
application:
name: my-zuul-service
zuul:
routes:
first-service:
path: /first-service/**
url: http://localhost:8081
second-service:
path: /second-service/**
url: http://localhost:8082
api gateway 프로젝트 생성하기
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.12'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2021.0.7")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
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-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
api gateway는 tomcat이 아닌 Netty 라는 Was 를 사용한다. 특징은 비동기 방식으로 운영된다는 점이다.
앞서 구현한 first-service의 컨트롤러 매핑 부분을 바꿔준다.
@RestController
@RequestMapping("/first-service")
public class FirstServiceController {
@GetMapping("")
public String home(){
return "redirect:/welcome";
}
@GetMapping("/welcome")
public String welcome(){
return "welcome to the First service";
}
}
yml 파일
server:
port: 8000
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/**
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
8000 번 포트에서 호출시 다음과 같이 라우팅 되는 것을 확인 할 수 있다.
클라우드 필터는 다음과 같은 흐름으로 진행된다.
자바 코드를 통해 filter를 구현하기
RouteLocator를 리턴해주면 yml 파일에 적용한 값을 자바 코드로 구현할 수 있다.
이때 header에 request와 response 값을 넣어줘보자.
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();
}
=> 앞서 작성한 yml 파일을 자바 코드로 옮긴 코드
filter 기능을 yml 파일에도 작성할 수 있다.
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-header2
- AddResponseHeader=first-response, first-response-header2
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, second-request-header2
- AddResponseHeader=second-response, second-response-header2
Custom Filter 만들기
@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
public CustomFilter() {
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("Custom PRE filter: request id -> {}", request.getId());
// Custom Post Filter
// 비동기 방식의 단일 값을 전달할 때 사용하는 Mono
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("Custom POST filter: response code -> {}", response.getStatusCode());
}));
};
}
// 반환 값을 만들어주기 위해 필요하다.
public static class Config {
// configuration properties를 추가할 수 있다.
}
}
yml 파일에서 설정한 것을 지워주고 customfilter를 추가한다.
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-header2
# - AddResponseHeader=first-response, first-response-header2
- CustomFilter
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
# - AddRequestHeader=second-request, second-request-header2
# - AddResponseHeader=second-response, second-response-header2
- CustomFilter
GlobalFilter 구현하기
공통적으로 실행될 수 있다.
GlobalFilter는 맨 처음과 마지막에 실행된다.
필터 작동 순서 :
gateway client > gateway handler > global filter > custom filter > logging filter > proxied Service
로깅 필터도 마찬가지 방식으로 구현할 수 있으며 우선순위를 주어 순서를 변경할 수 있다.
@Component
@Slf4j
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
public LoggingFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// 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());
// }
// return chain.filter(exchange).then(Mono.fromRunnable(()->{
// if (config.isPostLogger()) {
// log.info("Global Filter End: response code -> {}", response.getStatusCode());
// }
// }));
// });
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 PRE Filter: request id -> {}", request.getId());
}
return chain.filter(exchange).then(Mono.fromRunnable(()->{
if (config.isPostLogger()) {
log.info("Logging POST Filter: response code -> {}", response.getStatusCode());
}
}));
}, Ordered.LOWEST_PRECEDENCE);
return filter;
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
요청과 응답 흐름을 다음과 같이 되도록 바꿔보자.
클라이언트 요청을 API Gateway가 받아서 유레카 서버로 갔다가 찾아서 다시 gateway로 왔다가 dispatch 해준다.
디스커버리 서버에 등록된 주소로 포워딩 시켜주도록
first, second server를 eureka에 등록시켜 주고
yml에도 localhost 대신 lb: 주소 방식으로 작성해준다.
routes:
- id: first-service
uri: lb://MY-FIRST-SERVICE
predicates:
- Path=/first-service/**
filters:
- CustomFilter
- id: second-service
uri: lb://MY-SECOND-SERVICE
만약 First-service가 멀티로 작동중이면
lb 옵션에 의해 해당 서버를 찾고 로드 밸런싱을 하여 인스턴스를 분배한다.
이때 어떤 포트가 실행중인지를 확인해보고자 컨트롤러에 port를 보도록 코드를 간단하게 써보자.
Environment env;
@Autowired
public FirstServiceController(Environment env) {
this.env = env;
}
@GetMapping("/check")
public String check(HttpServletRequest request) {
log.info("Server port={}", request.getServerPort());
return String.format("Hi, there. This is a message from First Service on PORT %s"
, env.getProperty("local.server.port"));
}
'Programming > Java, Spring' 카테고리의 다른 글
[Spring Cloud] API Gateway Routing 시 마이크로서비스 식별 주소값을 제외하고 실용적인 uri를 전달하는 방식 (0) | 2023.06.01 |
---|---|
[Spring Cloud] Users MicroService를 Api Gateway에 등록하기 (0) | 2023.05.31 |
[Spring Cloud] Discovery Server 만들고 User service 등록하기 (0) | 2023.05.31 |
마이크로서비스(MSA)와 스프링 클라우드 (Spring Cloud) (0) | 2023.05.31 |
SpringBoot에서 profile 환경 설정 구성하기 (복수 profile 지정하는법) (0) | 2023.05.22 |