๐ ํ์ฌ ์ํฉ ๋ฐ ๋ฐฐ๊ฒฝ ์ค๋ช
์ฐ๋ฆฌ๋ MSA ํ๊ฒฝ์์ ๊ฐ ์๋ฒ์์ ํ์ฉํ ์ ์๋ ๊ณตํต ๋ชจ๋์ ๋์ ํ๋ ค ํ๋ค.
- ๊ฐ ์๋ฒ๋ ๊ณตํต ๋ชจ๋(common-module)์ ํตํด ๊ณตํต ๋ก์ง์ ๊ณต์ ํ๊ณ ์์ผ๋ฉฐ,
- ํด๋ผ์ด์ธํธ์ ์์ฒญ์ Spring Cloud Gateway๋ฅผ ํตํด ๋ด๋ถ ์๋น์ค๋ก ๋ผ์ฐํ ๋๋ค.
๐จ ๋ฌธ์ ์ํฉ
์ด๊ธฐ ๊ฐ๋ฐ ๋น์, ์ฐ๋ฆฌ๋ ์ต์ํ Spring MVC ๊ธฐ๋ฐ์ผ๋ก ์๋น์ค๋ฅผ ๊ตฌ์ฑํ๊ณ ์์๋ค. ๊ทธ๋ฌ๋ Spring Cloud Gateway์ ์ธ์ฆ ํํฐ๋ฅผ ์ ์ฉํ๋ ๊ณผ์ ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค. ๊ฐ์ฅ ํต์ฌ์ ์ธ ์ธ์ฆ ํํฐ๊ฐ ์ ๋๋ก ๋์ํ์ง ์์๋ ๊ฒ์ด๋ค.
ํน์ ํ ํฐ์ ํตํด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ถ์ถํด ๋ค๋ฅธ ์๋ฒ๋ก ์ ํํ๋ ค ํ์ง๋ง, ์ฌ์ฉ์ ์ ๋ณด๊ฐ null์ธ ์ํ๋ก๋ ํํฐ๊ฐ ๊ทธ๋๋ก ํต๊ณผ๋๋ ํ์์ด ๋ํ๋ฌ๋ค. ์ด๋ฅผ ํตํด ํํฐ ๋ก์ง ์์ฒด๊ฐ ์ค์ ๋ก ์คํ๋์ง ์๊ณ ์์์์ ํ์ธํ ์ ์์๋ค.
โ๏ธ ์์ธ ๋ถ์
Spring Cloud Gateway ๊ณต์ ๋ฌธ์์ ๋๋ฌธ์ง๋ง ํ๊ฒ ๋ช ์๋์ด ์์๋ค. Spring Cloud Gateway๋ WebFlux ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋ฉฐ, Servlet ํ๊ฒฝ์์๋ ์๋ํ์ง ์๋๋ค.
๐จ ํด๊ฒฐ ๋ฐฉ๋ฒ
๐ฝ Spring Cloud Gateway ํํฐ
@Component
public class PassportFilter extends AbstractGatewayFilterFactory<Object> {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
private static final String PASSPORT_HEADER = "X-Passport";
private final WebClient webClient = WebClient.create();
@Value("${passport.server.url}")
private String passportServerUrl;
public PassportFilter() {
super(Object.class);
}
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
String authHeader = exchange.getRequest().getHeaders().getFirst(AUTHORIZATION_HEADER);
// 1) JWT๊ฐ ์์ผ๋ฉด Passport Server Call
if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
String jwt = authHeader.substring(BEARER_PREFIX.length());
return getPassportFromPassportServer(exchange, chain, jwt);
}
// 2) JWT๊ฐ ์์ผ๋ฉด 401
return respondWithUnauthorized(exchange);
};
}
/**
* JWT๋ฅผ ํตํด Passport ๋ฐ๊ธ
*/
private Mono<Void> getPassportFromPassportServer(ServerWebExchange exchange, GatewayFilterChain chain, String jwt) {
return webClient.post()
.uri(passportServerUrl + "/passports?jwt=" + jwt)
.retrieve()
.bodyToMono(PassportResponseDto.class)
.flatMap(response -> {
// Passport ๋ฐ๊ธ ์คํจ
if (response.getResult() == null || response.getResult().getPassport() == null) {
return respondWithUnauthorized(exchange);
}
// ๋ฐ๊ธ๋ Passport๋ฅผ X-Passport ํค๋๋ก ์ค์
ServerWebExchange mutated = exchange.mutate()
.request(r -> r.headers(h -> h.set(PASSPORT_HEADER, response.getResult().getPassport())))
.build();
// ๋ค์ ํํฐ๋ก ์งํ
return chain.filter(mutated);
})
.onErrorResume(e -> respondWithUnauthorized(exchange));
}
/**
* Unauthorized ์๋ต
*/
private Mono<Void> respondWithUnauthorized(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
์ ์ฝ๋๋ Passport ๋์ ์ผ๋ก ์ธํด ๋ค์ ๋ณต์กํ๊ฒ ๋๊ปด์ง ์ ์์ผ๋ ์ฐธ๊ณ ๋ง ๋ฐ๋๋ค. ๊ด๋ จ ๋ด์ฉ์ ๊ด์ฌ ์์ผ์ ๋ถ๋ค์ ์ฌ๊ธฐ๋ฅผ ์ฐธ๊ณ ํ์๋ฉด ๋์์ด ๋ ๊ฒ ๊ฐ๋ค.
์ฐ๋ฆฌ๋ Gateway ์๋ฒ์์๋ ๊ณตํต ๋ชจ๋์ ๋ํ ์์กด์ฑ์ ์ ๊ฑฐํ๊ณ , WebFlux ๊ธฐ๋ฐ์ผ๋ก ์ธ์ฆ ์๋ฒ์ ํต์ ํ๋ ๋ฐฉ์์ ์ ํํ๋ค. ์ฆ, Gateway๋ WebClient๋ฅผ ์ฌ์ฉํด ๋น๋๊ธฐ ๋ฐฉ์์ผ๋ก ์ธ์ฆ ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด๊ณ , ์ธ์ฆ์ด ์๋ฃ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ ๋ด๋ถ ์๋น์ค๋ก ์ ๋ฌํ๋ ๊ตฌ์กฐ๋ก ๊ตฌํํ๋ค.
์ด์ ๋์์, ๊ณตํต ๋ชจ๋์ ๋ค๋ฅธ ์๋น์ค๋ค์์๋ ๊ทธ๋๋ก ํ์ฉํ๋ฉด์ Spring MVC ๋ฐฉ์์ ์ ์งํ ์ ์๋๋ก ํ์ฌ, Gateway๋ WebFlux, ๋ด๋ถ ์๋น์ค๋ MVC๋ผ๋ ๊ตฌ์กฐ๋ฅผ ์์ฐ์ค๋ฝ๊ฒ ๋ถ๋ฆฌํด ๋๋ค.
๐ ๊ฒฐ๊ณผ ๊ด์ฐฐ
๋์ ์ดํ, ์ ๋ฐ์ ์ธ ์ธ์ฆ ํ๋ฆ์ด WebClient๋ฅผ ํตํ ๋น๋๊ธฐ ์ฒ๋ฆฌ ๋ฐฉ์์ผ๋ก ์์ ํ๋์๋ค. Spring Cloud Gateway์ ํํฐ ๋ก์ง ๋ํ WebFlux ์ฒด๊ณ ์์์ ์ ์์ ์ผ๋ก ์๋ํ๋ฉด์, ๊ธฐ์กด์ ๋ฐ์ํ๋ ์ฌ์ฉ์ ์ ๋ณด ๋๋ฝ ๋ฌธ์ ๋ ๋ง๋ํ ํด๊ฒฐ๋์๋ค. ๊ณตํต ๋ชจ๋์ ๊ธฐ๋ฐ์ผ๋ก ์ธ์ฆ ๋ก์ง์ ์ผ๊ด๋๊ฒ ๊ด๋ฆฌํ ์ ์๊ฒ ๋๋ฉด์, ์๋น์ค ๊ฐ ์ธ์ฆ ์ฒ๋ฆฌ์ ์ ์ง๋ณด์์ฑ๋ ํฌ๊ฒ ํฅ์๋์๋ค. ์ธ์ฆ ์๋ฒ์์ ํต์ ์ญ์ WebClient ๋๋ถ์ ๋ณ๋ ฌ๋ก ๋น ๋ฅด๊ณ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌ๋์๋ค.
๐ก ๊ณ ์ฐฐ
WebFlux์ ๋ํ ์ดํด๊ฐ ๋ถ์กฑํ ์ํ์์ ๋๋ฒ๊น ์ ์งํํ๋ค ๋ณด๋ ์์๋ณด๋ค ๋ง์ ์ด๋ ค์์ ๊ฒช์๋ค. ํ์ง๋ง ๊ฒฐ๊ตญ ๊ณต์ ๋ฌธ์ ์์ ๋ชจ๋ ํด๋ต์ด ์์๋ค.
์์ผ ๊ธฐ๋ฐ์ ์ค์๊ฐ ์๋น์ค๋ Gateway + ์ธ์ฆ ์๋ฒ ๊ตฌ์กฐ์์๋ WebFlux๊ฐ ์ฌ์ค์ ํ์๋ผ๋ ์ ์ ์ ์ ์์๋ค. Mono, Flux์ ๊ฐ์ ๋น๋๊ธฐ ํ๋ฆ์ ๋ํ ์ดํด๊ฐ ์์ด๋ฉด WebFlux์ ๋์ ์๋ฆฌ๋ฅผ ๋ ์ฝ๊ฒ ํ์ ํ ์ ์์ ๊ฒ ๊ฐ๊ณ , ์ด๋ฅผ ์ํด ๋น ๋ฅธ ์์ผ ๋ด์ ๊น์ด ์๊ฒ ํ์ตํ ๊ณํ์ด๋ค.