이전에 우리가 사용하던 서비스들의 환경은 주로 synchronize 된 환경들이 많았다. 예를 들어 사진을 업로드 하여 인코딩 이후 보관 및 공유하는 서비스가 있다고 가정해보자.
조금 앞쪽 부분인, 업로드 이후 인코딩 부분에 조금 자세히 보자.
위의 간단한 과정 중에, 사용자는 파일을 업로드한 뒤 인코딩과 연동되는 과정을 전부 기다려야 작업이 완료되었다는 것을 알 수 있다. 과연 사용자가 이 모든 과정을 지속적으로 기다릴 필요가 있을까?
위의 과정중 가장 큰 시간이 소모되는 부분은 인코딩 부분일 것이다. 저 인코딩 과정을 서버에서 끝내고 200을 던져줄 때까지 기다린다면 몇 분간 사용자는 하염없이 기다리기만 하여야 할 것이다.
위와 같은 부분을 해소 할 수 있는 design pattern이 Asynchronous Request-Reply Pattern이다. 오래 걸리는 작업을 decoupling 하여 비동기적으로 요청에 대한 응답을 주는 방법이다.
위의 예제를 이어서 보자. 사용자는 업로드가 끝난 뒤, 다른 작업을 할 수 있고 인코딩 작업이 끝난 뒤 알림 또는 바로 가기 버튼을 통해 업로드 된 사진을 확인할 수 있게 한다. 조금 더 단계를 나눠서 보자면 아래와 같다.
사용자의 사진이 업로드가 끝이나면 우선 202 accept를 던진다. 이때, 해당 request에 대한 validation만 진행하며 만약 validation이 통과되지 않으면 400을 리턴한다. 뒤쪽에 긴 작업이 가능한지만 확인하는 부분만 진행하며 빠르게 사용자에게 response를 주는게 목적이다.
accept 안의 헤더에는 일반적으로 아래와 같은 아이들이 포함되게 한다. (header의 이름은 다르지만 목적은 비슷하다.)
a. Location: 뒤쪽으로 전달된 인코딩 과정 및 연동 부분 작업이 끝나면 사용자에게 알려주는 부분이 필요한데, 해당 작업이 끝남 유무를 확인할 수 있는 url이 담겨 있어야한다.
b. Retry-After: 너무 잦은 polling을 막기 위하여 작업이 끝났는지에 대한 체크 주기를 정해준다.
사용자는 비동기 형태로 자신의 작업이 끝났는지를 위의 Location 헤더로 받은 url에 request를 보낸다. 여기서 작업 중이면 200, 작업이 끝나면 302로 redirection 링크를 받는다. 페이지를 302로 바로 보내도 되고, 특정 버튼으로 사용자가 선택할 수 있게끔 만들어도 된다.
위의 내용을 sequence diagram으로 보면 아래와 같다.
위의 예제를 가장 흔하게 볼 수 있고 잘 보여주는 것은 Azure를 사용하는 사람이라면 흔하게 볼 수 있는 portal 안의 Notifications 부분이라고 생각한다.
삭제나 배포를 요청해놓고 우린 다른 리소스들에 대한 정보나 추가 배포 및 삭제가 가능하다. 그리고 다되면 status icon이 초록색 체크로 변하고 해당 리소스를 볼 수 있는 버튼 또한 흔히 볼 수 있다.
우리가 예제로 사용할 코드는 아래에 있다.
https://github.com/mspnp/cloud-design-patterns/tree/main/async-request-reply
간만에 닷넷인데, 나와 같은 오류를 맛보는 사람은 아래와 같은 방법을 쓰자.
이런 화면을 맛본다면 stackoverflow가 점지한 방법을 사용해보니 잘된다!! 역시 stackoverflow는 신이야!!
C:\Users\\AppData\Roaming\NuGet 안의 Nuget.config 삭제
해당 예제는 bicep으로 infra를 배포하며, 아래와 같은 구조로 작동된다.
대략적인 배포가 끝나고 테스트를 해보면 아래와 같은 부분을 확인할 수 있다.
배포한 앱에서 요청을 수락해주는 api를 실행해보면, 위와 같이 202 accepted를 먼저 끊어주는 모습을 볼 수 있다. 이는 service bus와 같은 queue 시스템에 해당 작업을 enqueue 한 뒤, 작업의 진행을 확인할 수 있는 url을 리턴해주게 된다. 위의 response의 rawcontent에도 Location 헤더가 나와있으며 해당 헤더는 요청의 진행도를 확인할 수 있는 url을 담고 있다. url을 따라가면 아래와 같이 service bus에 담겨진 정보를 blob으로 저장하는 과정이 끝나, 우리가 처음 입력한 id/customername 값이 담겨진 blob을 다운받을 수 있게 된다.
사실 아래와 같이 코드에도 Service Bus에 넣는 부분이 나와있다.
하지만 진짜 지나갔는지 궁금하지 않은가?
위와 같이 서비스 버스 메시지 incoming과 outgoing을 보면 실제로 거쳐간 것으로 보여진다.
아래와 같은 여러 서비스들과 연계하여 사용할 수 있다.
위의 서비스 위에도, 비동기적으로 끊어 줄 수만 있다면 더 많은 서비스들이 나올 수도 있다.
이러한 pattern들의 핵심은 queue 서비스를 어떻게 어떤 서비스를 쓰냐 인데, Azure에서는 Event hubs와 Service Bus 두가지 선택지 중 하나를 보통 선택하게 된다. Azure queue도 있지만 옵션이 많은 paas 서비스들을 나는 주로 선호하는 편이다.
두 가지 서비스의 큰 차이점은, fifo가 필요하냐 아니냐로 구분하면 될 것 같은데, 내 기준에서는 Event Hubs를 더 많이 활용하는 것 같다.
내가 진행하고 있는 프로젝트에서도 특정 api들은 시간이 너무 많이 걸려 위와 같은 비동기를 고려하고 있다. client 쪽의 개발이 쉽지 않은 상황이라 아마 사용하기에는 무리가 있어 다른 방법을 고려할 확률이 높지만, client 개발이 용이한 프로젝트를 진행하며 process에 시간이 많이 들어가는 workload가 있다면 충분히 사용할만한 pattern 이라고 생각된다.