๐Ÿ‘€ ํ˜„์žฌ ์ƒํ™ฉ ๋ฐ ๋ฐฐ๊ฒฝ ์„ค๋ช…

์šฐ๋ฆฌ๋Š” 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์˜ ๋™์ž‘ ์›๋ฆฌ๋ฅผ ๋” ์‰ฝ๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๊ณ , ์ด๋ฅผ ์œ„ํ•ด ๋น ๋ฅธ ์‹œ์ผ ๋‚ด์— ๊นŠ์ด ์žˆ๊ฒŒ ํ•™์Šตํ•  ๊ณ„ํš์ด๋‹ค.

 

 

๐Ÿ“ ์ฐธ๊ณ  ์ž๋ฃŒ

soeun2537