m1ndy5's coding blog

Ditto 프로젝트 멀티 모듈 도입 3편 - Gateway 도입하기 본문

Toy Projects/Ditto - Discuss Today's Topic

Ditto 프로젝트 멀티 모듈 도입 3편 - Gateway 도입하기

정민됴 2024. 2. 6. 17:38

기존 모놀리식 구조에서 멀티모듈 구조로 변경을 하니 문제 아닌 문제가 한가지 발생했다.

바로 각 모듈의 포트번호가 다르다는 것!

예를 들면 user 모듈에 있는 api들은 8080, activity 모듈에 있는 api들은 8081, newsfeed 모듈에 있는 api들은 8082에서 돌고 있었기 때문에 각 api들의 요청에 따라 포트번호를 변경해 요청해줘야했고 여간 귀찮은 일이 아니었다!!

 

그래서 이를 해결하기 위에 spring cloud gateway를 도입해 특정 포트로 요청이 들어오게 되면 각 api를 알맞은 포트로 연결해주는 역할을 하도록 했다.

 

spring cloud gateway를 사용하려면 아래와 같은 의존성을 추가해주면 된다.

이 또한 스프링 부트의 버전에 따라 맞춰서 넣자!!

implementation 'org.springframework.cloud:spring-cloud-starter-gateway:4.1.0'

이 뒤에 gateway의 port번호를 나같은 경우에는 8083으로 설정해주었다.

 

이제 무엇을 할 것이냐!!

바로 jwt 토큰을 검증하는 필터를 게이트웨이에 요청이 들어올 때 거치게 할것이다!!

본격적으로 gateway filter를 만들어보자

AuthFilter

jwt 토큰의 유효성을 검증하는 필터고 이 필터에서 Claims을 파싱하여 memberId를 헤더에 달아둘 것이다.

@RefreshScope
@Component
@RequiredArgsConstructor
public class AuthFilter implements GatewayFilter {

    private final JwtTokenProvider jwtTokenProvider;
    private final RouterValidator routerValidator;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();

        if (routerValidator.isSecured.test(request)){

            if (this.isAuthMissing(request)) {
                throw new IllegalArgumentException();
            }

            String token = this.getAuthHeader(request);
            token = token.substring(7);

            if (!jwtTokenProvider.validateToken(token)) {
                throw new IllegalArgumentException();
            }

            this.updateRequest(exchange, token);
        }
        return chain.filter(exchange);

    }

    private String getAuthHeader(ServerHttpRequest request) {
        return request.getHeaders().getOrEmpty("Authorization").get(0);
    }

    private boolean isAuthMissing(ServerHttpRequest request) {
        return !request.getHeaders().containsKey("Authorization");
    }

    private void updateRequest(ServerWebExchange exchange, String token) {
        Claims claims = jwtTokenProvider.parseClaims(token);
        exchange.getRequest().mutate()
                .header("memberId", String.valueOf(claims.get("memberId")))
                .build();
    }
}

GatewayFilter를 implements 받아 filter method를 오버라이딩했다.

해당 filter 메서드에서는 인가가 필요한 요청인지 확인후에 토큰이 유효한지 확인 후 유효하다면 Claims에서 memberId를 추출해 헤더에 memberId를 넣어 다음으로 보내는 작업을 하고 있다.

인가가 필요하지 않은 요청들은 RouterValidator를 사용해서 로직을 굳이 타지 않게 했다.

RouterValidator

package org.example;

import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.function.Predicate;

@Component
public class RouterValidator {

    public static final List<String> openApiEndpoints = List.of(
            "/api/members", "/api/auth/email-authentication", "/api/auth/authentication-code", "/api/auth/login",
            "/api/follow/{toMemberId}/followings", "/api/follow/{toMemberId}/followers"
    );

    public Predicate<ServerHttpRequest> isSecured =
            request -> openApiEndpoints
                    .stream()
                    .noneMatch(uri -> request.getURI().getPath().contains(uri));
}

openApiEndpoints에 있는 api들에 match가 되는지 여부를 확인하는 메서드를 가지고 있다.

GatewayConfig

다음으로는 어떤 요청이 어떤 포트로 갈 것인지 연결을 해주는 GatewayConfig를 만들었다.

package org.example;

import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class GatewayConfig {

    private final AuthFilter authFilter;

    @Bean
    public RouteLocator ms1Route(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("userAPI", r -> r.path("/api/members/**", "/api/auth/**", "/api/mypage/**")
                        .filters(f -> f.filter(authFilter))
                        .uri("http://localhost:8080")
                )
                .route("activityAPI", r -> r.path("/api/posts/**", "/api/follow/**")
                        .filters(f -> f.filter(authFilter))
                        .uri("http://localhost:8081")
                )
                .route("newsfeedAPI", r -> r.path("/api/newsfeed/**")
                        .filters(f -> f.filter(authFilter))
                        .uri("http://localhost:8082")
                )
                .build();
    }
}

이 때 각각의 요청들이 필터를 거쳐야 들어올 수 있게 설정하였다.

 

각 인가된 memberId의 정보는 헤더에 담겨 있기 때문에

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/posts/{postId}/comments")
public class CommentController {

    private final CommentService commentService;

    @PostMapping
    public BaseResponse<CommentResponse> uploadComment(@RequestHeader("memberId") Long memberId, @PathVariable("postId") Long postId, @Valid @RequestBody CommentRequest commentRequest){
        return new BaseResponse<>(commentService.uploadComment(memberId, postId, commentRequest));
    }

    @GetMapping()
    public BaseResponse<List<CommentResponse>> getComments(@PathVariable("postId") Long postId){
        return new BaseResponse<>(commentService.getComments(postId));
    }

    @PatchMapping("/{commentId}")
    public BaseResponse<CommentResponse> updateComment(@RequestHeader("memberId") Long memberId, @PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId, @Valid @RequestBody CommentRequest commentRequest){
        return new BaseResponse<>(commentService.updateComment(memberId, postId, commentId, commentRequest));
    }

    @DeleteMapping("/{commentId}")
    public BaseResponse<CommentResponse> deleteComment(@RequestHeader("memberId") Long memberId, @PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId){
        return new BaseResponse<>(commentService.deleteComment(memberId, postId, commentId));
    }

    @PostMapping("/{commentId}/like")
    public BaseResponse<CommentLikeResponse> pushCommentLike(@RequestHeader("memberId") Long memberId, @PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId){
        return new BaseResponse<>(commentService.pushCommentLike(memberId, postId, commentId));
    }


}

이렇게 헤더에서 꺼내쓰면 된다!

 

기존 모놀리식 구조에서 멀티 모듈로 변환하면서 설계에 대해서 많은 고민과 헤맨 시간들을 통해 설계의 중요성과 필요성에 대해 느낄 수 있었고 어렴풋이 알고 있었던 개념들을 실제로 적용해가면서 ddd, msa, 멀티 모듈의 차이와 각각의 장단점을 몸소 느낄 수 있었던 경험이었다.