MSA 심장부를 설계하다: Theme4. MSA JWT 전달 전략 like Fractal

dev_will_d·2024년 4월 18일
4
post-thumbnail

🚨 참고) 이글은 Hexagonal Architecture 기반으로 설명을 하고 있습니다. MSA 심장부를 설계하다: Theme1. Hexagonal Architecture 먼저 읽으시고 이글을 읽으시는 것을 추천드립니다😀

위키 백과에서는 프랙탈 구조를 위와 같이 정의한다. 프랙탈 구조는 쉽게말해 무한히 반복되는 구조다. MSA 환경에서도 이러한 구조를 직면할 수 있는데 각 서버에 JWT 정보를 전달하는 전략을 구축할때 이러한 구조를 마주할 수 있다.

이번 MSA 심장부를 설계하다 4번째 시리즈에서는 분리 / 분산된 서버 환경에서 어떻게 JWT Context를 전달하는지에 대한 설명을 준비했다. 그리고 더 나아가 Passport 전략에 대해서도 간단히 언급하는 시간을 가지도록 하겠다.

API Gateway


MSA 개발 환경에서는 분리 / 분산된 서버를 조직적으로 관리하기 위해 하나의 진입점을 둔다. 이러한 단일 진입점을 API Gateway 서버라 부른다. API Gateway 서버는 각 서비스에 접근하기 위한 모든 요청을 수용하고 트래픽을 분리 / 분산 시킨다. 각 서비스에 분리 / 분산 하기전 API Gateway 서버는 Auth 서버와의 통신을 통해 인증 / 인가를 거친 후 JWT 정보와 함께 트래픽을 각 서버에 할당한다.

요청 Endpoint(Controller) 도달 흐름


API Gateway 서버로 부터 요청을 할당 받은 하나의 서버의 Endpoint 도달 과정을 살펴보면 위 그림의 오른쪽 사진과 같다. API Gateway 서버는 파싱한 JWT 정보를 Object 형태로 만든후 JSON 형태로 직렬화하여 Header에 주입해 정보를 전달한다. 이때 각 서버의 Filter에서는 Local Thread (MVC) or Reactor Context (WebFlux)에 이 값을 저장하여 유지하고, Resolver에서는 이 값을 다시 역직렬화 하여 Controller에 값을 주입 할 수 있는 상태를 구현한다. 더 자세한 내용은 아래를 살펴보자.

🚨 주의) WebFlux를 기반으로 Web Application Server를 구성한다면 특정 Context 정보를 Local Thread에 저장하면 안된다. 이유는 WebFlux는 하나의 요청에 대한 작업에 여러 Thread가 관여하여 Thread의 전환이 빈번하게 일어나기 때문이다. 자세한 내용은 이전에 작성한 MSA 심장부를 설계하다: Theme3. MVC, WebFlux 경계글을 참고 부탁드린다.

Local Thread, Reactor Contex JWT 주입 및 전달 원리


위의 그림을 보면 Filter에 저장된 값이 다시 뽑아져 다른 서버와 통신하면서 JWT Context를 전달하는 흐름을 알 수 있다. 실제 구현 코드는 아래의 코드를 참고 부탁드린다.

👨🏻‍💻 질문) 왜 이렇게 JWT 정보를 Local Thread, Reactor Context에 저장하고 뽑아내는 전략을 구축한것인가요?
우리가 API Gateway에서 본 그림을 보면 자원서버는 Private Network 환경에 있다는것을 알 수 있다. 즉 API Gateway를 통과하여 특정 자원서버로 도달했다는 말은 인증 / 인가에 대해 원활하게 수행했다고 해석할 수 있다. 그렇다면 내부의 자원 서버간의 통신을 할때 또 인증 / 인가를 거치는 것은 적합한가? 필자는 실제로 이 고민에 대해 깊게 했다. 필자의 결론은 이미 Gateway를 통과했다면 내부에서는 더이상 인증 / 인가를 거치지 않고 JWT 정보를 연속적으로 전달하는것이 더욱 효율적이고 Private Network 환경이니 안정하다 판단했다.(물론 이 방법도 정답은 아닐것이다. 오히려 보안 레벨을 높여야 되는 상황이면 내부에서 통신 할때도 인증 / 인가를 거치는것이 적합할 수 있다.) 즉, Filter에서 Header의 JWT JSON 값을 뽑아내 JWT JSON 정보를 Local Thread, Reactor Context에 저장하고 우리 서버와 통신할때는 같은 방식으로 Header에 값을 저장하여 전달한다. 위에서 서두에 프랙탈 구조에 대해 이야기 했는데 이렇게 하면 계속 해서 반복적인 구조로 Context가 유지되어 프랙탈 구조를 만들 수 있다. 필자도 처음에 이 구조를 다 구현해서 완성했을때 뭔가 프랙탈 구조를 구현한거 같아서 너무 기분이 좋았다 ㅎㅎㅎ...

* MVC (Local Thread 사용)

요청을 담당하는 CommonHttpClient에 Local Thread의 값을 뽑아내는 코드를 구현하여 JWT Context를 외부의 서버에 전달했다.

* WebFlux (Reactor Context 사용)

요청을 담당하는 CommonWebClient에서 Reactor Context의 값을 뽑아내는 코드를 구현했다. 이가 가능한 이유는 Filter CommonWebClient를 사용하는 시점에는 Filter에서 Context값을 저장하는 시점과 비교했을때 Up Stream이기 가능한 코드이다. 이러한 원리를 통해서 WebFlux에서도 JWT Context를 유지했다.

Endpoint(Controller) JWT 주입 및 전달 원리 With Resolver


Argument Resolver에서도 Header에서 JWT JSON 값을 뽑아 역직렬화 하여 Controller에 주입하는 코드를 구현한다.
👨🏻‍💻 질문) 이미 위에서 JWT Context 정보를 저장하였는데 왜 Resolver 까지 사용하는 건가요?
서버와 서버간의 JWT Context 유지는 Filter, Local Thread, Reactor Context를 통해서 실현하고, 비지니스 로직에 필요한 JWT 값을 전달하는 역할은 Resolver에서 주입한 값을 통해 실현하는것이 경계를 분명히 하여 클린 코드를 유지하는 방식이기에 이러한 구조를 채택하였다. 예를 들자면, Resolver를 구현하지 않아도 비지니스 로직을 구현할때 JWT 정보가 필요하다면 Context에서 값을 뽑아내 직렬화 하여 충분히 사용할 수 있다. 그러나 JWT의 어떤 특정 값만 사용하는 것인데 모든 정보를 뽑아내는것이 과연 타당한가? Controller에서 비니지스 레이어에서 필요한 특정 값을 넘겨주어 비지니스 레이어의 경계를 분명히 하는것이 더 타당하지 않을까? 그래서 Resolver를 구현했다. 추가적으로 Filter를 구현하지 않아도 충분히 서버와 서버간의 JWT Context 유지를 Resolver를 통해 가능하다 그러나 이 또한 비지니스 레이어에 불필요한 데이터를 넘겨주는 것과 같다. 위와 같은 이유로 경계를 분명히 하기 위해 Filter, Local Thread, Reactor Context 전략을 구축했다. 이렇게 함으로서 비지니스 레이어를 지키고 클린 코드를 작성할 수 있다. (이 부분은 필자도 정말 많이 고민하고 고민한 부분이다. 혹시 이해가 잘 안되는 분이 계신다면 과감하게 댓글 남겨주시면 감사하겠습니다😀) 실제 Resolver 주입 코드는 아래와 같다.

* MVC Resolver

* WebFlux Resolver

* Endpoint

흐름 정리


흐름을 다시 한번 정리해보면, API Gateway에서 전달된 JWT 정보는 Auth를 거처 validate 되어 Object로 만들어 지며, 이 Object는 또 직렬화 되어 Header에 값이 저장될것이다. 이러한 과정을 거쳐 API Gateway 서버에서 특정 서버에 요청을 Route 할것이고 이 요청을 받은 서버는 위에서 설명한 원리로 계속해서 JWT Context를 유지해 나간다.

Passport 전략


이 원리를 더욱 확장한다면 Passport 전략까지 구축을 할 수 있다. 즉 JWT 토큰 정보를 기반으로 사용자 정보를 User 서버에 요청하여 JWT 정보가 아닌 User 정보의 Context를 유지하는것이다. 그렇다면 왜 User Context를 유지하는 것인가 B2C 서비스는 사용자를 중심으로 비지니스가 수행된다. 즉 이말을 우리가 비지니스 로직을 작성할때 사용자 정보를 기반으로한 비지니스 로직을 매우 많이 작성한다는 말이다. 그리고 다시 이 말을 해석하면 그많큼 많은 서비스가 User 서비스에 수많은 요청을 한다는 것이다. 그렇다면 하나의 요청에 대해 매번 각 서비스에서 User 서버에 User의 정보를 요청하는 것이 아닌 한번만 요청하여 이 정보를 계속 유지할 수 있다면 더욱 효율적이고 User 서버의 트래픽을 낮출수 있지 않을까? 그래서 나온 방식이 Passport 방식이라고 할 수 있겠다. Passport 방식을 통한 서버 전체 최적화 글은 다른 포스트에서 작성하도록 하겠다.

마무리

MSA 구조는 개발하다 보면 가끔 아름답다는 생각이 든다. 이번 MSA 심장부를 설계하다: Theme4. MSA JWT 전달 전략 like Fractal 글에서는 그 아름다움을 다시금 느끼는 계기가 되었다.
긴글 읽어주셔서 감사합니다😀

profile
질문의 질이 답의 질을 결정한다.

6개의 댓글

comment-user-thumbnail
2024년 4월 20일

컨텍스트가 유지되고 있는지는 어떻게 알 수 있는 건가요? 테스트를 어떤 식으로 해보시는지 궁금합니다!

1개의 답글
comment-user-thumbnail
2024년 4월 24일

1.리졸버가 컨트롤러에서 Header 객체 payload를 말씀하시는 건가요?

2.Header 데이터 컨텍스트를 유지한다고 할 때,
톰캣의 경우 로컬쓰레드에 저장하여 사용 한다면, 데이터 저장 되는 기준이 로컬 쓰레드의 메모리라고 생각되는데요
웹플럭스의 경우 Reactor Context를 사용하신다고 했는데, 데이터가 저장되는 기준이 어떻게 될까요?

1개의 답글