Spring Cloud Gateway 적용
기존의 프로젝트에 MSA를 적용함에 따라서 여러 서비스로 분리가 되면 해당 서비스만의 IP 주소와 포트번호가 지정되게 되는데 각 서비스마다 호출하는 것이 아닌 한 곳에서 요청을 관리하고 처리하는 것이 시스템을 관리하는 측면에서 더 유리하다.
SSM 프로젝트를 진행할 당시 웹 서버로 Nginx를 구성하였는데 해당 서버가 프록시 역할을 하여 클라이언트로 받은 요청을 적절하기 백엔드로 넘겨주게 된다.
이러한 프록시 역할을 똑같이 해주는 것이 Spring Cloud Gateway이다.
Spring Cloud Gateway
Spring Cloud Gateway는 앞서 언급했던 대로 API 게이트웨이 서버이다. 클라이언트에서 받은 요청을 다른 API로 라우팅 하고, 보안이나 모니터링, 메트릭을 모아서 처리할 수 있는 것이 Spring Cloud Gateway이다.
Spring Cloud Gateway는 Spring Boot, Spring Webflux, Project Reactor 기반으로 비동기 프로그래밍 방식을 지원하기 때문에 기존의 MVC, 스프링 데이터, 시큐리티 등 동기식 방법은 적용이 안 될 수도 있다.
스프링 클라우드 게이트웨이는 Spring Boot와 Spring Webflux에서 제공하는 Netty 서버가 필요하다. 따라서 기존의 서블릿 컨테이너를 사용하거나 애플리케이션을 war로 빌드했을 땐 동작하지 않는다.
본격적으로 Spring Cloud Gateway를 알아보기 전에 한 가지 찾아봐야 될 부분이 있었다.
웹 프로젝트를 진행하게 되면 주로 Spring Boot로 시작하게 되는데 Spring Boot를 실행하게 되면 내부적으로 Tomcat 서버가 실행된다. 조금 있다가 살펴보겠지만 Spring Cloud Gateway를 실행하게 되면 평소 사용하는 Tomcat이 아닌 Netty 서버가 WAS로 돌아가게 되는데 Netty가 뭔지에 대해서 이해가 필요했다.
간단한 Netty 설명
Netty를 찾아보니 자료를 많이 찾을 수 있었지만 양이 너무 많았다. (따로 기간을 잡고 깊게 파고들어야 할 듯)
여기서는 간단하게 Netty가 어떤 것이고, 왜 Spring Cloud Gateway에서 사용되었는지 알아보자.
Spring Cloud Gateway에서는 Tomcat이 아닌 Netty를 WAS로 사용하게 된다. 이 글에서 처음에 언급했던 것과 마찬가지로 모든 API 요청을 Gateway 서버 한 곳에서 받게 되기 때문에 성능적인 부분이 매우 중요해진다.
API 요청이 동기식으로 처리가 된다면 병목이 생길 것이고 그에 따라서 서비스의 질이 저하될 것이다.
기존의 Spring MVC는 하나의 스레드가 하나의 요청을 처리하게 되는데 비동기 WAS인 Netty는 하나의 스레드가 1개 이상의 요청을 처리할 수 있기 때문에 기존 방식보다 더 많은 요청을 처리할 수 있어 Gateway가 사용하기 적합한 것이다.
Spring Cloud Gateway의 구성 요소
Netty도 알아보았으니 이제 Spring Cloud Gateway의 구성 요소를 알아보자
Route
Gateway는 ID, 목적지 URI, predicate 컬렉션, 필터 컬렉션으로 구성되어 있고, prediacte들을 집계한 결과가 true일 경우 사용자가 지정한 route에 매칭된다.
Predicate
Predicate는 클라이언트로부터 받은 요청이 사용자가 정의한 조건을 충족하는지 테스트하는 구성요소로 이를 통해 헤더나 파라미터 같은 HTTP 요청 정보를 매칭시킬 수 있다.
Filter
특정 팩토리로 생성하는 GatewayFilter 인스턴스로 필터에선 다운스트림으로 요청을 전송하기 전후에 요청과 응답을 수정할 수 있다.
예를 들어 SSM 프로젝트 때 사용했던 웹 서버인 Nginx를 설정할 때 "/특정경로/api/~" 이런 식으로 들어오면 해당 요청에서 api를 제거하고 다시 요청을 백엔드 서버로 전달했던 것과 같다고 생각하면 된다.
Spring Cloud Gateway 동작 방식
공식 문서에서 제공하는 다이어그램을 보고 어떻게 동작하는지 알아보자.
먼저 클라이언트에서 Spring Cloud Gateway 서버에 요청을 전송한다.
전달받은 요청은 Gateway Handler Mapping이 처리하게 되는데 해당 요청이 사용자가 지정한 Route와 매칭된다고 판단되면 Gateway Web Handler로 전달한다.
해당 핸들러에선 요청에 맞는 필터 체인을 통해 요청을 처리한다.
필터 체인에 등록되는 필터는 프록시 요청을 전송하기 전과 후로 구분해서 로직을 실행할 수 있다.
필터 체인의 동작을 보면 먼저 사용자가 필터 체인에 등록한 모든 pre 필터 로직을 실행한다. 그다음 프록시 요청을 만들고, 해당 프록시 요청이 이루어진 후엔 다시 사용자가 등록한 post 필터 로직을 실행하게 된다.
공식 문서에서 제공하는 동작 과정에 대한 그림만 봐도 이해가 잘 되지만 그래도 내가 개발할 프로젝트에 Gateway를 적용했을 경우 어떻게 동작할지 그려보는 것도 좋은 방법이다.
아래의 그림은 draw.io를 사용해서 직접 그려본 그림이다.
사용자가 /profile/get이라는 API 요청을 보낸다면 Spring Cloud Gateway 서버에서 해당 요청을 받아서 개발자가 구현한 로직에 부합하게 되면 처리한다.
개발자가 profileImage 서비스의 고유 ID, 서비스 uri와 predicate로 /profile/**를 지정하여 Route를 설정하게 되면 앞서 클라이언트로부터 전달받은 /profile/get이라는 요청이 조건에 부합하게 되고 Gateway Web Handler에게 해당 요청을 보낸다.
Gateway Web Handler는 전달받은 요청을 아직 백엔드 서비스에서 요청이 처리되기 전이여서 Pre Filter Chain으로 보낸다.
Pre Filter Chain에 등록된 필터들을 통해 인증 및 인가, API 요청 유효성 검사 등을 거치게 되고 ProfileImage라는 백엔드 서비스로 API 요청이 전송된다.
ProfileImage 서비스에서 전달받은 API 요청을 처리하고 결과 값을 다시 Gateway로 보내주면 Post Filter Chain을 통해서 다시 필터를 거치게 되고 최종적으로 클라이언트가 결과 값을 반환받게 된다.
한 가지 팁으로는 Route에서 URI를 지정할 때 포트 없이 정의하면 HTTP, HTTPS는 각각 디폴트 포트 80, 443을 사용한다고 나와있다.
프로젝트에 적용해 보기
이제 Spring Cloud Gateway에 대해서 알아봤으니 프로젝트에 적용해 보자.
테스트를 해보기 위해서 구성하는 것이기 때문에 간단하게 Gateway를 구성할 수 있다.
먼저 Spring Boot 프로젝트를 하나 생성해 준다.
(현재 진행하는 프로젝트의 Spring Boot 버전은 2.7.13이고, 자바는 11 버전이다.)
Spring Cloud Gateway 의존성 추가
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.1.3</version>
</dependency>
Spring Cloud Gateway 의존성을 자신이 개발할 Spring Boot 버전에 맞게 추가해 준다.
(version을 설정하지 않으면 알아서 자동으로 Spring Boot 버전에 맞는 걸로 추가해 줌)
application.yml파일 수정
server:
port: 6060
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: ${ROUTE_PROFILE_ID}
uri: ${ROUTE_PROFILE_URI}
predicates:
- ${ROUTE_PROFILE_PATH}
application.yml 파일에 Gateway의 기본 정보와 Route 정보를 지정해 준다.
port 번호는 다른 서비스와 겹치지 않게 6060으로 설정했고 application 이름으로는 gateway로 설정했다.
Route 정보는 gateway의 routes에 작성하는데 MSA로 분리된 서비스가 여려 개여서 Route 정보를 여러 개 설정할 수 있다.
Route 구성을 살펴보면 먼저 id는 Gateway가 요청을 보내줄 서비스의 고유 ID를 지정하고, uri는 해당 서비스의 경로, predicate는 조건을 적용하는데 여기서는 "profile/"로 들어오는 모든 요청을 받겠다고 지정했다.
위의 구성 요소 외에도 filter를 추가할 수 있는데 추후 JWT 토큰이나 유효성 검증과 같은 필터를 추가할 예정이다.
spring:
application:
name: profile-service
API 요청을 받을 백엔드 서비스의 application.yml 파일에 application 이름으로 profile-service라고 설정하였다. 해당 설정이 Gateway에서 식별할 고유 ID가 된다.
설정을 모두 마치 Gateway 서버를 실행시켜 보면
이렇게 WAS가 기존의 Tomcat이 아닌 Netty로 돌아가는 것을 확인할 수 있다.
마지막으로 postman을 통해서 Gateway 주소와 포트번호로 profile 이미지를 불러오는 GET 요청을 보내서 결과를 확인해 봤다.
profileImage service는 7070 포트를 사용하고 있지만 Gateway를 통해서 API 요청을 전달받기 때문에 Gateway 포트번호인 6060으로 API 요청을 보내도 응답을 받을 수 있게 된다.
추가 및 보완할 점
간단하게 Spring Cloud Gateway를 적용하는 것은 크게 어렵지 않았다. 하지만 요청 값에 대한 유효성 검증이나 인증 및 인가 처리가 필요해 보였다.
그리고 가장 중요한 점이 하나 있는데 바로 Gateway 서버를 이중화하는 것이다.
모든 API 요청에 대해서 Gateway 서버 한 곳이 다 받게 되는데 문제는 요청이 많아짐에 따라서 서버에 부하가 발생해 Gateway 서버가 다운되면 다른 서비스들도 이용할 수 없는 문제가 생기게 된다.
이러한 문제를 사전에 예방하기 위해서 Gateway 서버를 이중화 혹은 그 이상으로 서버를 둬야지 정상적인 서비스 운영이 될 것이라고 판단된다.
Gateway 서버를 이중화하는 것은 추후 배포할 때 적용해 보도록 하자.
참고자료
'프로젝트' 카테고리의 다른 글
[프로젝트] - 면접 회고와 추후 계획 (1) | 2024.10.03 |
---|---|
[SSM_프로젝트] Spring Netflix Eureka 적용하기 (0) | 2024.06.13 |
[SSM_프로젝트] - 프로필 이미지 성능 개선하기 (0) | 2024.06.12 |
[SSM_프로젝트] 다시 처음으로... (0) | 2024.06.03 |
[SSM_프로젝트] - GitHub Actions와 AWS Code Deploy를 활용한 CI/CD 적용 - (2) (0) | 2024.06.01 |