728x90
반응형

회사에서 API 게이트웨이 서버를 만들게 되었다.

검색을 해도 중구난방 잘 안되어있어서 경험하면서 기본적인 틀을 기록하고자 한다.

 

1. build.gradle에 추가해준다.

ext {
    set('snippetsDir', file("build/generated-snippets"))
    set('springCloudVersion', "2022.0.4")
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

dependencies {

	...
    
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'org.postgresql:postgresql'
    runtimeOnly 'org.postgresql:r2dbc-postgresql'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'
}

 

2. application-properties.yml 파일 수정

server:
  port: 8070

spring:
  jackson:
    timezone: Asia/Seoul
  data:
    r2dbc:
      repositories:
        enabled: true
  datasource:
    url: r2dbc:postgresql://localhost:5433/postgres
    username: postgres
    password: postgres
    driver-class-name: org.postgresql.Driver
  r2dbc:
    url: r2dbc:postgresql://localhost:5433/postgres
    username: postgres
    password: postgres
  cloud:
    gateway:
      default-filters: # Gateway 공통 필터
        - name: GlobalFilter
          args:
            baseMessage: hello world
      routes:
        - id: router-1
          uri: http://localhost:3000
          predicates:
            - Path=/**

 

비동기 서버를 위한 r2dbc를 사용해야 한다.

 

3. GlobalFilter 생성

@Slf4j
@Component
public class GlobalFilter extends AbstractGatewayFilterFactory<FilterDto> {

    public GlobalFilter() {
        super(FilterDto.class);
    }

    @Override
    public GatewayFilter apply(FilterDto dto) {
        return (exchange, chain) -> {
            log.info("GlobalFilter baseMessage: {}", dto.getMessage());
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("GlobalFilter End: {}", exchange.getResponse());
            }));
        };
    }
}

 

4. FilterDto 생성

@Getter
public class FilterDto {

    private String message;
}

 

5. ApiRoute Entity 생성

@Entity
@Getter
@Table(name = "api_route")
@TableGenerator(name = "api_route", allocationSize = 1)
public class ApiRoute {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "api_route_seq")
    @SequenceGenerator(name = "api_route_seq", sequenceName = "api_route_seq", allocationSize = 1)

    @Column(name = "id")
    private String id;

    @Column
    private String routeIdentifier;
    @Column
    private String uri;
    @Column
    private String method;
    @Column
    private String path;
}

 

6. ApiRouteRepository 생성

@Repository
public interface ApiRouteRepository extends R2dbcRepository<ApiRoute, String> {

}

 

7. ApiRouteService 생성

Service Interface는 아래의 Override 한 메소드만 추가해주면 된다.

@Service
@RequiredArgsConstructor
public class ApiRouteServiceImpl implements ApiRouteService {

    private final ApiRouteRepository apiRouteRepository;

    @Override
    public Flux<ApiRoute> getAll() {
        return this.apiRouteRepository.findAll();
    }

    public Mono<ApiRoute> create(ApiRoute apiRoute) {
        return this.apiRouteRepository.save(apiRoute);
    }

    public Mono<ApiRoute> getById(String id) {
        return this.apiRouteRepository.findById(id);
    }
}

 

8. ApiPathRouteLocatorImpl 생성

@AllArgsConstructor
public class ApiPathRouteLocatorImpl implements RouteLocator {

    private final ApiRouteService apiRouteService;
    private final RouteLocatorBuilder routeLocatorBuilder;

    @Override
    public Flux<Route> getRoutes() {
        RouteLocatorBuilder.Builder routesBuilder = routeLocatorBuilder.routes();
        return apiRouteService.getAll()
            .map(apiRoute -> routesBuilder.route(String.valueOf(apiRoute.getRouteIdentifier()),
                predicateSpec -> setPredicateSpec(apiRoute, predicateSpec)))
            .collectList()
            .flatMapMany(builders -> routesBuilder.build()
                .getRoutes());
    }

    private Buildable<Route> setPredicateSpec(ApiRoute apiRoute, PredicateSpec predicateSpec) {
        BooleanSpec booleanSpec = predicateSpec.path(apiRoute.getPath());
        if (!StringUtils.isEmpty(apiRoute.getMethod())) {
            booleanSpec.and()
                .method(apiRoute.getMethod());
        }
        return booleanSpec.uri(apiRoute.getUri());
    }

    @Override
    public Flux<Route> getRoutesByMetadata(Map<String, Object> metadata) {
        return RouteLocator.super.getRoutesByMetadata(metadata);
    }
}

 

9. GatewayConfig 생성

@Configuration
@Slf4j
public class GatewayConfig {

    @Bean
    public RouteLocator routeLocator(ApiRouteService routeService,
        RouteLocatorBuilder routeLocationBuilder) {
        return new ApiPathRouteLocatorImpl(routeService, routeLocationBuilder);
    }

}

 

여기까지가 기본적인 프록시를 위한 라우터이다.

다음 부터는 API로 route를 CRUD 하기 위한 작업이다.

 

10. ApiRouteRouter Configuration 생성

@Configuration
public class ApiRouteRouter {

    @Bean
    public RouterFunction<ServerResponse> route(ApiRouteHandler apiRouteHandler) {
        return RouterFunctions.route(POST("/routes")
                .and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::create)
            .andRoute(GET("/routes/{routeId}")
                .and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::getById)
            .andRoute(GET("/routes/refresh-routes")
                .and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::refreshRoutes);
    }
}

 

11. ApiROuteHandler 생성

@RequiredArgsConstructor
@Component
@Slf4j
public class ApiRouteHandler {

    private final ApiRouteService routeService;

    private final GatewayRoutesRefresher gatewayRoutesRefresher;

    public Mono<ServerResponse> create(ServerRequest serverRequest) {
        Mono<ApiRoute> apiRoute = serverRequest.bodyToMono(ApiRoute.class);
        return apiRoute.flatMap(route ->
            ServerResponse.status(HttpStatus.OK)
                .contentType(MediaType.APPLICATION_JSON)
                .body(routeService.create(route), ApiRoute.class));
    }

    public Mono<ServerResponse> getById(ServerRequest serverRequest) {
        log.info("serverRequest.pathVariable(\"routeId\") = {}",
            serverRequest.pathVariable("routeId"));
        final String apiId = serverRequest.pathVariable("routeId");
        Mono<ApiRoute> apiRoute = routeService.getById(apiId);
        return apiRoute.flatMap(route -> ServerResponse.ok()
                .body(fromValue(route)))
            .switchIfEmpty(ServerResponse.notFound()
                .build());
    }

    public Mono<ServerResponse> refreshRoutes(ServerRequest serverRequest) {
        gatewayRoutesRefresher.refreshRoutes();
        return ServerResponse.ok().body(BodyInserters.fromObject("Routes reloaded successfully"));
    }
}

 

12. GatewayRoutesRefresher 생성

@Component
public class GatewayRoutesRefresher implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    /**
     * Refresh the routes to load from data store
     */
    public void refreshRoutes() {
        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
    }
}

 

 


만약, domain Hostname에 따라 Proxy되는 서버의 주소를 변경하고자 하면 다음과 같은 방법을 쓸 수 있다.

@Override
    public GatewayFilter apply(HostNameFilterDto hostNameFilterdto) {

        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            request.getHeaders();

            String[] split = request.getURI().getHost().split("\\.");
            String siteId = split[0];

            Mono<Site> siteMono = siteService.findById(siteId);
            Site site = siteMono.share().block();

            String uri = Objects.requireNonNull(site).getConnectHost();
            int port = site.getConnectPort();
            if (port != 80) {
                uri += ":" + port;
            }

            Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
            Route newRoute = Route.async()
                .id(site.getId())
                .uri(uri)
                .predicate(serverWebExchange -> false)
                .order(Objects.requireNonNull(route).getOrder())
                .filters(route.getFilters())
                .build();
            exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, newRoute);

            return chain.filter(exchange);
        };
    }
728x90
반응형

+ Recent posts