Spring Cloud Gateway는 요청을 라우팅하는 것에 대해 정의하는 방법은 크게 두 가지가 있다. 자바 코드로 정의하는 방법인 Java DSL(Domain-Specific Languages)과 application.yml
과 같은 설정 파일에서 정의하는 방법이다.
이전 글 - Spring Cloud Gateway RouteDefinitionLocator & RouteLocator 코드 분석 에서는 이렇게 정의된 route 정보들을 어떻게 메모리에 가지고 있는지를 알아보았다면, 이번 글에서는 라우팅 정보를 가지고 있는 Route 객체에 대해 살펴본다.
org.springframework.cloud.gateway.route.RouteDefinition
application.yml
과 같은 설정 파일에서 정의한 라우팅 정보들을 인스턴스화 할 수 있는 class (이전 글 PropertiesRouteDefinitionLocator 부분 참고)@Validated
public class RouteDefinition {
private String id;
// Route 매칭 조건인 Predicate는 하나 이상 있어야 한다.
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList<>();
// 요청이 어떤 필터들을 거쳐야하는지에 대한 정보를 가진다.
@Valid
private List<FilterDefinition> filters = new ArrayList<>();
// 라우팅될 서비스(Proxied Service)의 uri 정보
@NotNull
private URI uri;
// 기타 데이터(공통화할 수 없는 라우팅 정보)들을 저장할 수 있는 객체
private Map<String, Object> metadata = new HashMap<>();
private int order = 0;
// 빈 생성자로 객체를 생성한 뒤, setter로 값을 넣을 수 있다.
public RouteDefinition() { }
// text를 입력 파라미터로 받아서 그 text를 파싱해서 RouteDefnition 객체를 만들 수 있다.
// 하지만, 해당 버전의 코드에서는 사용되는 곳을 찾을 수 없었다.
public RouteDefinition(String text) {
// name=value,name=value
int eqIdx = text.indexOf('=');
if (eqIdx <= 0) {
throw new ValidationException("Unable to parse RouteDefinition text '" + text
+ "'" + ", must be of the form name=value");
}
// 맨처음 = 의 이전 값은 id로 처리한다.
setId(text.substring(0, eqIdx));
// id 이후 String은 , 을 기준으로 배열로 만든다.
String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), ",");
// 배열의 가장 처음 값은 proxied service uri
setUri(URI.create(args[0]));
// 배열의 처음 값을 제외한 요소들은 predicate 객체로 만들어서 멤버변수 predicates에 넣어준다.
for (int i = 1; i < args.length; i++) {
this.predicates.add(new PredicateDefinition(args[i]));
}
// 해당 생성자는 멤버변수 filters에 데이터를 넣을 수 없다.
}
//... getter, setter, equals, hashCode, toString 생략
}
RouteDefinition(String text)
생성자 테스트@Test
public void testRouteDefinitionFromText() {
String text = "thisIsId=https://blog.jungbin.kim,Host=*.example.com";
RouteDefinition routeDefinition = new RouteDefinition(text);
System.out.println(routeDefinition.toString());
// RouteDefinition{id='thisIsId', predicates=[PredicateDefinition{name='Host', args={_genkey_0=*.example.com}}], filters=[], uri=https://blog.jungbin.kim, order=0, metadata={}}
}
org.springframework.cloud.gateway.route.Route
public class Route implements Ordered {
private final String id;
private final URI uri;
private final int order;
// 요청 정보가 담긴 ServerWebExchange를 입력받아서 RouteDefinition의 predicates에 정의된 조건들에 매칭되는지를 검사
private final AsyncPredicate<ServerWebExchange> predicate;
private final List<GatewayFilter> gatewayFilters;
private final Map<String, Object> metadata;
// 생성자는 private으로 되어 있어, builder 로만 객체를 생성하도록 한다.
private Route(String id, URI uri, int order,
AsyncPredicate<ServerWebExchange> predicate,
List<GatewayFilter> gatewayFilters, Map<String, Object> metadata) {
// set 멤버변수 생략...
}
// 생략...
}
java.util.function.Predicate
, 입력 타입이 Generic 인 T이고 반환 타입이 Boolean인 함수형 인터페이스.org.springframework.cloud.gateway.handler.AsyncPredicate
, 입력 타입이 Generic 인 T이고, 반환 타입이 Webflux의 Publisher(Mono, Flux)로 감싸진 Boolean (Function<T, Publisher<Boolean>>
)public abstract static class AbstractBuilder<B extends AbstractBuilder<B>> {
// ... predicate 를 제외한 Route와 동일한 멤버 변수들과 그 멤버변수에 넣을 수 있는 메서드들 제공
public Route build() {
Assert.notNull(this.id, "id can not be null");
Assert.notNull(this.uri, "uri can not be null");
// Builder로 만들어도 결국 build 할 때는 AsyncPredicate로 변환된다.
AsyncPredicate<ServerWebExchange> predicate = getPredicate();
Assert.notNull(predicate, "predicate can not be null");
return new Route(this.id, this.uri, this.order, predicate,
this.gatewayFilters, this.metadata);
}
}
public static class AsyncBuilder extends AbstractBuilder<AsyncBuilder> {
protected AsyncPredicate<ServerWebExchange> predicate;
}
public static class Builder extends AbstractBuilder<Builder> {
protected Predicate<ServerWebExchange> predicate;
}
Map<String, Object>
객체로 되어 있어서, 비즈니스 로직에 필요한 정보들을 가질 수 있음application.yml
기반 metadata 설정 (출처-Spring Cloud Gateway Docs > 11. Route Metadata Configuration)spring:
cloud:
gateway:
routes:
- id: route_with_metadata
uri: https://example.org
metadata:
optionName: "OptionValue"
compositeObject:
name: "value"
iAmNumber: 1
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
route.getMetadata();
// key에 해당하는 Object를 가져와서 사용한다.
// 위의 설정에서 가져올 수 있는 키들은 optionName, compositeObject, iAmNumber 등이 존재
route.getMetadata(someKey);
Spring Cloud Gateway에서 Java DSL(Domain-Specific Languages)로 정의된 Route 객체들과 설정 파일에서 정의한 route 정보들을 어떻게 메모리에 가지고 있는지를 RouteDefinitionLocator
, RouteLocator
객체를 통해 알아본다.
Java DSL 방식은 별도의 RouteLocator Bean 을 정의하고, 그 내부에 route 정보를 작성한다.
application.yml 과 같은 설정 파일에서 정의된 route 정보 같은 경우, RouteDefinition 객체로 역직렬화한 뒤 RouteDefinitionRouteLocator 에서 Route 객체로 변경된다.
위에서 언급된 RouteLocator Bean 들은 하나로 합쳐지며 캐싱된다. (CompositeRouteLocator, CachingRouteLocator)
캐싱된 Route 객체들을 가지고 있는 RouteLocator Bean 은 이전 글 - Spring Cloud Gateway Web Handler 코드 분석에서 살펴본 RoutePredicateHandlerMapping 에 주입되어 클라이언트 요청과 매핑하는데 사용된다.
public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}
PropertiesRouteDefinitionLocator
, InMemoryRouteDefinitionRepository
, CompositeRouteDefinitionLocator
, DiscoveryClientRouteDefinitionLocator
, CachingRouteDefinitionLocator
들이 존재
Flux<RouteDefinition>
합쳐지는 구조application.yml
> spring.cloud.gateway.routes
에 정의되어 있는 route 정보들을 제공하는 메서드 존재// 생성자
public PropertiesRouteDefinitionLocator(GatewayProperties properties) {
// 설정 객체인 GatewayProperties을 주입받는다.
this.properties = properties;
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
// 설정 객체에 있는 route 정보를 반환
return Flux.fromIterable(this.properties.getRoutes());
}
org.springframework.cloud.gateway.config.GatewayProperties
application.yml
의 spring.cloud.gateway
prefix로 갖는 설정 구현 객체// in-memory RouteDefinition 저장 멤버변수
private final Map<String, RouteDefinition> routes = synchronizedMap(new LinkedHashMap<String, RouteDefinition>());
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
// 저장되어 있는 RouteDefinition 반환
return Flux.fromIterable(routes.values());
}
public interface RouteDefinitionWriter {
Mono<Void> save(Mono<RouteDefinition> route);
Mono<Void> delete(Mono<String> routeId);
}
org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint
@PostMapping("/routes/{id}")
public Mono<ResponseEntity<Object>> save(@PathVariable String id, @RequestBody RouteDefinition route)
@DeleteMapping("/routes/{id}")
public Mono<ResponseEntity<Object>> delete(@PathVariable String id)
// 생성자
public CompositeRouteDefinitionLocator(Flux<RouteDefinitionLocator> delegates, IdGenerator idGenerator) {
this.delegates = delegates;
this.idGenerator = idGenerator;
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
// 각 RouteDefinitionLocator의 RouteDefinition들을 하나로 합쳐준다.
return this.delegates
.flatMapSequential(RouteDefinitionLocator::getRouteDefinitions)
.flatMap(routeDefinition -> {
// route Id 가 없는 경우 randomId를 만들어준다.
if (routeDefinition.getId() == null) {
return randomId().map(id -> {
routeDefinition.setId(id);
if (log.isDebugEnabled()) {
log.debug("Id set on route definition: " + routeDefinition);
}
return routeDefinition;
});
}
return Mono.just(routeDefinition);
});
}
public interface RouteLocator {
Flux<Route> getRoutes();
}
Flux<Route>
합쳐지며, CachingRouteLocator Bean에서 캐시하는 구조
private final Map<String, RoutePredicateFactory> predicates = new LinkedHashMap<>();
private final Map<String, GatewayFilterFactory> gatewayFilterFactories = new HashMap<>();
// 생성자
public RouteDefinitionRouteLocator(RouteDefinitionLocator routeDefinitionLocator,
List<RoutePredicateFactory> predicates,
List<GatewayFilterFactory> gatewayFilterFactories,
GatewayProperties gatewayProperties,
ConfigurationService configurationService) {
this.routeDefinitionLocator = routeDefinitionLocator;
this.configurationService = configurationService;
// 주입받은 RoutePredicateFactory 들을 멤버변수 predicates 에 넣는다. key는 predicate 의 이름.
initFactories(predicates);
// 주입받은 GatewayFilterFactory 들을 멤버변수 gatewayFilterFactories 에 넣는다. key는 filter의 이름.
gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));
this.gatewayProperties = gatewayProperties;
}
org.springframework.cloud.gateway.handler.predicate
패키지 참고org.springframework.cloud.gateway.filter.factory
패키지 참고getRoutes 메서드:
@Override
public Flux<Route> getRoutes() {
// routeDefinitionLocator 에서 RouteDeinition들을 가져와서 Route 객체로 변환한다.
Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions()
.map(this::convertToRoute);
//... (생략) routes 변환 과정 에러 처리
// (생략) routes 디버그
return routes;
}
public class CompositeRouteLocator implements RouteLocator {
private final Flux<RouteLocator> delegates;
public CompositeRouteLocator(Flux<RouteLocator> delegates) {
this.delegates = delegates;
}
@Override
public Flux<Route> getRoutes() {
// 각 RouteLocator의 Route들을 하나로 합쳐준다.
return this.delegates.flatMapSequential(RouteLocator::getRoutes);
}
}
GatewayAutoConfiguration
에서 List<RouteLocator>
를 주입 받아서 하나의 RouteLocator (CompositeRouteLocator
)로 만들어서 CachingRouteLocator
로 주입public RouteLocator cachedCompositeRouteLocator(List<RouteLocator> routeLocators) {
return new CachingRouteLocator(
new CompositeRouteLocator(Flux.fromIterable(routeLocators)));
}
public class CachingRouteLocator implements Ordered, RouteLocator,
ApplicationListener<RefreshRoutesEvent>, ApplicationEventPublisherAware {
//...
}
생성자:
Flux<Route>
를 가져와서 정렬하여 저장.private static final String CACHE_KEY = "routes";
public CachingRouteLocator(RouteLocator delegate) {
this.delegate = delegate;
routes = CacheFlux.lookup(cache, CACHE_KEY, Route.class)
.onCacheMissResume(this::fetch);
}
private Flux<Route> fetch() {
return this.delegate.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE);
}
onApplicationEvent 메서드:
ApplicationListener<RefreshRoutesEvent>
를 구현하기 위한 Override 메서드@Override
public void onApplicationEvent(RefreshRoutesEvent event) {
try {
// delegate하고 있는 RouteLocator에서 Flux<Route> 를 가져와 Mono<List<Route>> 로 변경
fetch().collect(Collectors.toList()).subscribe(list -> Flux.fromIterable(list)
// 구독 후 Flux<Signal<Route>>로 변환된 상태에서 Mono<List<Signal<Route>>> 로 변환
.materialize().collect(Collectors.toList()).subscribe(signals -> {
applicationEventPublisher
.publishEvent(new RefreshRoutesResultEvent(this));
// List<Signal<Route>> 상태로 캐시 업데이트
cache.put(CACHE_KEY, signals);
}, throwable -> handleRefreshError(throwable)));
}
catch (Throwable e) {
handleRefreshError(e);
}
}
Spring Cloud Gateway(v2.2.7.RELEASE
) 코드에서 Web Handler 부분을 분석한다.
WebHandler는 요청을 처리하기 위한 인터페이스이며,
Spring Cloud Gateway에서는 WebHandler 를 구현한 FilteringWebHandler 에서 Route에 설정된 Filter들으로 Filter Chain을 만들어 처리한다.
이 포스트에서는 FilteringWebHandler.handle 부분과 이 부분에서 만드는 DefaultGatewayFilterChain 의 동작 코드를 살펴본다.
org.springframework.cloud.gateway.handler.FilteringWebHandler
org.springframework.web.server.WebHandler
을 구현.
Mono<Void> handle(ServerWebExchange exchange);
메서드를 구현하도록 함.org.springframework.cloud.gateway.config.GatewayAutoConfiguration
에서 Bean으로 등록된다.// 생성자.
public FilteringWebHandler(List<GlobalFilter> globalFilters) {
// loadFilters 에서 GlobalFilter -> OrderedGatewayFilter(GatewayFilter)로 변경하여 globalFilters 멤버 변수에 저장.
this.globalFilters = loadFilters(globalFilters);
}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
// 요청에 해당하는 Route 객체에 적용되야하는 gateway filter 들
List<GatewayFilter> gatewayFilters = route.getFilters();
// 멤버 변수로 가지고 있는 global filter 들
List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
// gateway filter + global filter
combined.addAll(gatewayFilters);
// 순서에 따라 정렬
AnnotationAwareOrderComparator.sort(combined);
//...
// DefaultGatewayFilterChain는 아래 별도 설명. 요청에 대한 필터 체이닝의 시작점.
return new DefaultGatewayFilterChain(combined).filter(exchange);
}
private static class DefaultGatewayFilterChain implements GatewayFilterChain {
private final int index;
private final List<GatewayFilter> filters;
// 외부에서 생성할 경우 사용하는 생성자. FilteringWebHandler.handle 에서 생성한다.
DefaultGatewayFilterChain(List<GatewayFilter> filters) {
this.filters = filters;
this.index = 0;
}
// 내부 filter 메서드에서만 사용. 상위 filterChain 객체를 기반으로 index가 증가된 filterChain 을 만들수 있는 생성자
private DefaultGatewayFilterChain(DefaultGatewayFilterChain parent, int index) {
this.filters = parent.getFilters();
this.index = index;
}
public List<GatewayFilter> getFilters() {
return filters;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange) {
// defer를 사용하여 실행을 구독 시점까지 지연한다.
return Mono.defer(() -> {
if (this.index < filters.size()) {
GatewayFilter filter = filters.get(this.index);
// index가 증가된 DefaultGatewayFilterChain 객체 생성하여, 다음에 적용될 filter의 index 값을 갖도록 함.
DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this,
this.index + 1);
// GatewayFilter.filter 메서드에 exchange와 index가 증가된 chain 을 넘긴다.
// GatewayFilter.filter 메서드는 입력으로 온 chain의 filter 메서드를 호출하여 반환함으로 다음 filter가 실행되도록 한다.
return filter.filter(exchange, chain);
}
else {
return Mono.empty(); // complete
}
});
}
}
Spring Cloud Gateway(v2.2.7.RELEASE
) 코드에서 Handler Mapping 부분(아래 이미지 음영 외 부분)을 분석한다.
Spring Cloud Gateway의 클라이언트 요청 처리는 Spring 프레임워크에서 처리하는 것과 동일하며, Gateway에 맞게 HandlerMapping 객체를 확장(RoutePredicateHandlerMapping)하여 요청을 처리할 WebHandler를 찾는데 사용한다.
org.springframework.web.server.adapter.HttpWebHandlerAdapter
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
if (this.forwardedHeaderTransformer != null) {
request = this.forwardedHeaderTransformer.apply(request);
}
// ServerWebExchange를 생성
ServerWebExchange exchange = createExchange(request, response);
LogFormatUtils.traceDebug(logger, traceOn ->
exchange.getLogPrefix() + formatRequest(exchange.getRequest()) +
(traceOn ? ", headers=" + formatHeaders(exchange.getRequest().getHeaders()) : ""));
// getDelegate 으로 대상 WebHandler를 가져와서 exchange를 넘긴다.
// Spring Cloud Gateway의 경우나 @EnableWebFlux를 사용하는 경우, 대상 WebHandler = DispatcherHandler
return getDelegate().handle(exchange)
.doOnSuccess(aVoid -> logResponse(exchange))
.onErrorResume(ex -> handleUnresolvedError(exchange, ex))
.then(Mono.defer(response::setComplete));
}
org.springframework.web.reactive.DispatcherHandler
// HandlerMapping -- map requests to handler objects
@Nullable
private List<HandlerMapping> handlerMappings;
// HandlerAdapter -- for using any handler interface
@Nullable
private List<HandlerAdapter> handlerAdapters;
// HandlerResultHandler -- process handler return values
@Nullable
private List<HandlerResultHandler> resultHandlers;
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
return Flux.fromIterable(this.handlerMappings)
// (HandlerMapping) 요청 정보가 들어있는 exchange로 해당하는 Handler를 찾음.
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
// (HandlerAdapter) invokeHandler 에서 this.handlerAdapters의 루프를 돌면서 해당하는 handlerAdapter를 찾고, 주어진 Handler로 요청을 처리.
.flatMap(handler -> invokeHandler(exchange, handler))
// (HandlerResultHandler) handleResult 에서 this.resultHandlers의 루프를 돌면서 해당하는 handleResult를 찾고, 응답 헤더를 수정하거나 응답에 데이터를 쓰는 것과 같은 처리.
.flatMap(result -> handleResult(exchange, result));
}
org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping
org.springframework.cloud.gateway.config.GatewayAutoConfiguration
에서 Bean으로 등록.org.springframework.web.reactive.handler.AbstractHandlerMapping
를 상속받음.
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
// ... 생략
// lookupRoute: 별도 설명
return lookupRoute(exchange)
.flatMap((Function<Route, Mono<?>>) r -> {
// ... exchange 업데이트 로직 생략
// webHandler = FilteringWebHandler = Gateway Filter Chain 호출
return Mono.just(webHandler);
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
// ... 매칭되는 RouteDefinition 를 찾지 못할 경우 로그 남기는 로직 생략
})));
}
// 주석 및 코드 일부 생략
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes() // Flux<Route> 반환
// Route를 순서대로 검사
.concatMap(route -> Mono.just(route).filterWhen(r -> {
// route 의 predicate로 필터링
return r.getPredicate().apply(exchange);
})
.doOnError(e -> logger.error(
"Error applying predicate for route: " + route.getId(),
e))
.onErrorResume(e -> Mono.empty()))
.next()
.map(route -> {
// 현재 버전에선 아직 구현이 안되어 있지만, Route의 Validation 체크를 하는 부분
validateRoute(route, exchange);
return route;
});
}
Selenium 는 브라우저를 자동화한다. Selenium WebDriver, Selenium IDE, Selenium Grid 세가지 프로젝트들이 있다.
참조: 공식 홈페이지(selenium.dev)
$ brew cask install chromedriver
==> Downloading https://chromedriver.storage.googleapis.com/83.0.4103.39/chromed
######################################################################## 100.0%
==> Verifying SHA-256 checksum for Cask 'chromedriver'.
==> Installing Cask chromedriver
==> Linking Binary 'chromedriver' to '/usr/local/bin/chromedriver'.
🍺 chromedriver was successfully installed!
$ pip3 install --user selenium
$ pip3 install selenium
Could not install packages due to an EnvironmentError: [Errno 13] Permission denied: 'RECORD'
Consider using the `--user` option or check the permissions.
--user
옵션을 붙여서 시스템 폴더가 아닌 유저 폴더에 설치한다.pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available.
$ brew reinstall python
을 통해 python을 다시 설치한다.selenium-test.py
)# selenium의 webdriver를 가져온다.
from selenium import webdriver
# brew 로 설치된 chromedriver의 path
path = '/usr/local/bin/chromedriver'
# 크롬 드라이버를 사용
browser = webdriver.Chrome(path)
# 브라우저에 띄우고 싶은 URL 입력. 이 경우 로그인부터 하기 위해 로그인창에 접근한다.
browser.get('https://login.toast.com')
# 로그인 창에서 자동으로 정보를 입력
browser.find_element_by_xpath("//input[@type='text']").send_keys("********")
browser.find_element_by_xpath("//input[@type='password']").send_keys("********")
# 입력된 로그인 정보 제출
browser.find_element_by_xpath("//button[@type='submit']").click()
# 로그인 이후, 확인이 필요한 페이지로 이동
browser.get('https://after-login.toast.com')
# 확인하고 싶은 로직. 여기선 테스트로 refresh 를 0.5에 한번씩 하도록 했다.
import time
max_time = 5
start_time = 0
refresh_time_in_seconds = 0.5
while(start_time < max_time):
browser.refresh()
start_time += refresh_time_in_seconds
time.sleep(refresh_time_in_seconds)
browser.quit()
$ python3 selenium-test.py
개발자를 확인할 수 없기 때문에 ‘chromedriver’을(를) 열 수 없습니다.
$ cd /usr/local/Caskroom/chromedriver/
# 폴더 내 버전 확인 후 이동
$ cd 83.0.4103.39
$ xattr -d com.apple.quarantine chromedriver
마이크로 서비스 아키텍처에서는 여러 마이크로 서비스 어플리케이션들로 구성되어 있으므로 각각 서비스 어플리케이션에서 공통 기능(하나의 서버 어플리케이션에서 Filter로 처리하던 기능들)을 구현해야하는 힘든 점이 있다. 그러한 공통 기능을 Gateway 서비스를 통해서 하나의 진입점에서 처리해줄 수 있다. Spring Cloud Gateway는 Gateway 서비스를 구현할 수 있는 Spring 프로젝트이다.
cross cutting concerns
)을 제공해준다.
Spring Cloud Gateway application.yml
설정 예시:
spring:
cloud:
gateway:
routes:
# 이 부분이 하나의 route이다.
- id: host_route
uri: https://example.org # 목적지 Uri
predicates: # 이 조건들을 가질 때, 위 목적지 Uri로 라우트해준다.
- Host=**.somehost.org
filters: # 정의된 필터들을 거친다.
- AddRequestHeader=X-Request-red, blue # 목적지 Uri에 요청할 때 헤더에 X-Request-red: blue 를 추가하여 보낸다.
v2.2.x
branch 를 보면 zuul core v1 을 사용master
branch (v3.0.0.M1)을 보면 zuul을 찾아볼 수 없다. (2020.05.29 기준)spring-cloud-netflix-zuul
은 새로운 기능을 추가하지 않는 모듈 대상에 포함되어 있다.테스트 이유: Vue 에서 click event를 선언하는데, method 이름만 선언된 경우와 method 실행을 선언할 때 다른 파라미터 값을 넘겨주었다.
undefined
undefined
출력undefined
@click=""
안에 선언되는 메소드, 변수(이 경우에는 clickEventFunction, event
)는 vue component 에서 접근 가능한 것이다.undefined
를 출력$event
를 이용한다.onclick=""
에서 ""
안에 있는 내용(자바스크립트)을 실행해주는 것과 동일하기 때문이다. 함수 이름만 있으면 실행안되는 게 당연하다.undefined
undefined
출력RxJS Observable.zip 으로 여러 비동기를 묶을 때, 그 개별 비동기 안에 있는 비동기도 기다리는가를 실험해보았다. 결론은 zip은 zip으로 묶은 비동기들만 기다리고, 그 내부에서 호출한 비동기는 zip과 상관없이 실행된다.
Vue 프로젝트와 관련된 이유:
Promise
)로 동작한다.그전에 현재 상태에 대한 테스트 모델을 rxjs로 구현해보았다.
const { from, zip } = require('rxjs')
const sub1 = () =>
new Promise((resolve, reject) => {
console.log(' sub1 function begin')
setTimeout(() => {
console.log(' sub1 resolve')
resolve(' sub1 resolve')
}, 700)
console.log(' sub1 function end')
})
const sub2 = () =>
new Promise((resolve, reject) => {
console.log(' sub2 function begin')
setTimeout(() => {
console.log(' sub2 resolve')
resolve(' sub2 resolve')
}, 300)
console.log(' sub2 function end')
})
const main1$ = () =>
from(
new Promise((resolve, reject) => {
console.log('main1 function begin')
sub1()
sub2()
console.log('main1 function end')
resolve('main1 resolve')
})
)
const main2$ = () =>
from(
new Promise((resolve, reject) => {
console.log('main2 function begin')
setTimeout(() => resolve('main2 resolve'), 400)
console.log('main2 function end')
})
)
console.log('zip start')
zip(main1$(), main2$()).subscribe(() => console.log('zip end'))
zip start
main1 function begin // main1 함수 실행
sub1 function begin // sub1, sub2 순차적으로 비동기적으로 실행 후, 함수 자체는 끝나며 resolve 되길 기다림
sub1 function end
sub2 function begin
sub2 function end
main1 function end
main2 function begin // main2 함수 실행. resovle 되기를 기다림
main2 function end
sub2 resolve // sub2 는 300ms 이후 resolve
zip end // main2 가 끝나는 400ms 이후, main1, main2 두개가 모두 끝나있어 zip end 로그 남음.
sub1 resolve // sub1 은 호출된지 700ms 이후에 종료되어 zip end가 보이고 나서 로그가 남음.
개발 환경에서는 잘 동작하던 기능이 스테이징 환경에서 이상 동작하는 현상이 발생하였다. 그 현상은 특정 메뉴 영역의 mouseout 이벤트가 제대로 동작하지 않는 것이었다.
cssnano 문서를 보니 zindex 수정해주는 기능은 설정을 통해서 된다. 별다른 설정을 하고 있진 않은데 왜 zindex를 수정해주는걸까? 바로 버전 문제였다.
optimize-css-assets-webpack-plugin
의 버전은 v4.0.3
이고, v4.0.3의 package.json를 보면 "cssnano": "^3.10.0"
를 사용한다.After built the z-index changed를 보면 비슷한 현상에 대한 리포트가 있다. 그 답변 중 하나인 only use safe optimizations for css minification을 적용해본다.
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const webpackConfig = {
// ...
plugins: [
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
})
]
// ...
}
css 최소화 처리 옵션으로 safe를 넣어 z-index 변경을 해주지 않을 수 있다. (또는 버전업을 통해 해결할 수도 있다.)
]]>프로젝트가 시간에 지남에 따라 Vue Global mixin 컴포넌트가 방대해졌고, 특정 도메인 컴포넌트에서만 쓸 것 같은 기능들이 들어간 것을 발견하였다. 이를 분리하면서 생각했던 내용들을 정리하였다.
Vue mixin은 컴포지션(Composition)을 통해 다른 컴포넌트의 정의된 속성(methods, computed, created 등)을 가져와 합치는 기능이다. 이를 전역적으로 컴포지션해서 쓰는 것이 Global mixin
이다.
진입점(app.js
또는 index.js
등)에서 직접 mixin을 한다:
import Vue from 'vue';
import Common from './mixins/Common.vue';
Vue.mixin(Common);
Common.vue
의 정의된 속성들을 어느 컴포넌트에서나 this
로 접근하여 사용이 가능하다. 이곳에 선언되면 전역적으로 의존성을 갖게 된다. 정의된 속성이 어디서 사용되는지는 Common.vue
만 봐서는 알 수 없고, 전체 컴포넌트 대상으로한 검사가 필요하다.
이미 시간이 많이 지난 프로젝트의 경우
mixin 컴포넌트 내 함수 중에는 컴포넌트 내에서만 사용되고 외부에서 사용되지 않을 수 있다. 자바에서 private 접근자로 외부에서의 접근을 막는 것과 같이 Vue 컴포넌트에서도 그런 기능을 제공해주는지 찾아보았다.
Vue 스타일 가이드 ‘Private 속성 이름’을 보면, 두가지 추천 스타일이 있다.
속성 이름 앞에 $_{name scope}_{속성이름}
붙이는 방법:
var myGreatMixin = {
// ...
methods: {
$_myGreatMixin_update: function () {
/*
이런 이름 규칙을 사용한다고 해도 다른 컴포넌트에서 접근을 못하진 않는다. 개발자들 간의 규약으로 봐야할 것 같다.
*/
}
}
}
private 함수 정의를 export 해주지 않는 방법:
// Even better!
var myGreatMixin = {
// ...
methods: {
publicMethod() {
// ...
myPrivateFunction()
}
}
}
function myPrivateFunction() {
/*
이 경우에는 this 접근자로 mixin 내부 속성을 접근하기 힘들다.
bind 등의 scope 변경 함수를 이용해서 this를 사용할 수 있는 방법도 있지만, 코드가 뭔가 불필요해지는 부분이 생긴다는 느낌이 든다.
*/
}
export default myGreatMixin