> For the complete documentation index, see [llms.txt](https://poyos-tech-blog.gitbook.io/poyos-tech-blog/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://poyos-tech-blog.gitbook.io/poyos-tech-blog/article/python-microservice-1.md).

# 외부 모듈을 위한 Python Microservice 구축기 (1)

FastAPI, Celery 분산 작업 관리자를 사용한 Python 클라이언트 서버 구현하기

파이썬만 지원하는 외부 서비스의 클라이언트 모듈을 지원하기 위해 작은 마이크로서비스 하나를 띄워야 했다.

Python 은 거의 코딩테스트용 언어로 사용했던 나에게 약간의 챌린지로써 좋은 기회인것 같아 이 작업을 맡았다.

그리하여 Core Server 에서 Python 클라이언트의 작업 결과를 수신하기 위해 새로운 서비스를 구성하기로 했다.

FastAPI 웹 프레임워크를 선택했고, 선정한 이유는 다음과 같다.

* 친절한 Docs
* 입문하기 좋은 익숙한 구조
* 비동기로 요청을 처리하는 ASGI 웹 서버
* Flask 보다는 필요한 모듈이 잘 구성되어 있고, Django 보다는 가볍다는 생각

### 배포 파이프라인 구축

기존에도 AWS ECR, ECS (Fargate) 를 사용한 서버리스 컨테이너를 주로 사용했기 때문에, 이번에도 빠른 파이프라인 구축을 위해 사용하게 되었다.

**Github Action** 을 사용하여 CI 를 만들었고 Docker 빌드 후 ECR 레지스트리 등록, ECS Task 개정을 거쳐 배포하도록 했고 하루만에 Dev 배포환경을 구축했다.

{% @mermaid/diagram content="sequenceDiagram
participant Github Action
participant AWS ECR
participant AWS ECS Fargate
Github Action->>Github Action: Docker Build
Github Action->>AWS ECR: Register Image
AWS ECS Fargate->>AWS ECR: Pull Image
AWS ECS Fargate->>AWS ECS Fargate: Task Revision, Service Deploy
AWS ECS Fargate->>Github Action: Send "Finish" Notification" %}

### 마이크로서비스간 통신 하기

MSA 구조를 만드는 데 있어 꽤 고민이 드는 부분은 통신 규칙을 정하는 것이 아닐까 싶다. 우리는 Python Client 에게 **데이터를 변경할 때** 메시지를 전달하기로 했다. 그리고 반대로 데이터를 페칭하는 경우는 **직접 호출**로 처리했다.

#### **직접 통신 방식**

`Spring Boot Server (OpenFeign) == Request => Python Server`

파이썬 서버로 직접 데이터를 쿼리하기 위한 통신은 OpenFeign 을 사용했다.

gRPC 같은 마이크로서비스에서 사용하기 좋은 프로토콜이 있지만, 학습곡선이 있다는 점과 성능적으로 큰 차이가 없으며 인터페이스 또한 별도 구성이 필요하기 때문에 오히려 번거롭다는 점을 고려해서, 생산성이 좋은 Spring Cloud 진영의 **OpenFeign** 을 팀원들과 회의를 통해 합의해서 도입했다.

#### **이벤트 드리븐 통신 방식**

`Spring Boot Server (Send Message) => AWS SQS => Celery Worker (Receive Message)`

`Python Server (Send Message) => AWS SQS => Spring Boot Server (Receive Message)`

중요한 데이터의 정합성을 위해, AWS SQS FIFO Queue 를 사용해서 통신 구조를 만들었다. 작성일 기준으로 AWS SQS 만 사용이 가능했기 때문에 (제한된 AWS 크레딧을 사용하는 상황) AWS SNS 같은 pub/sub 서비스까지는 도입하지 못했지만, 추후에는 도입하더라도 서비스가 가능하도록 구현해두었다.

메시지를 수신하는 곳이 Celery Worker 인 이유는 다음에서 설명한다.

### **SQS 메시지 수신을 위한 Listening Worker 설계**

Spring Boot 에서는 `starter-sqs` 라이브러리가 `@SqsListener` 어노테이션 기능을 제공하기 때문에 자체적인 **Polling 기능이 내장**되어 있어 통합이 매우 쉽지만, Python 진영에서는 이와 같은 Production Ready 라이브러리를 찾기 쉽지 않았다.

기본적인 AWS SDK 인 **boto3** 를 사용하면 메시지 수신을 할 수는 있지만 **”직접“** 큐에서 꺼내와야 하기 때문에, 기본적으로는 메시지가 큐에 전송된 후 **목적지에서 메시지를 자동으로 Polling 하지 못한다.**

따라서 `@SqsListener` 와 비슷한 **Long-Polling** 기능을 구현하기 위해 [**Celery**](https://github.com/celery/celery) 를 사용한 **Worker Process** 를 추가로 띄우는 작업을 수행했다.

#### **분산 태스크 관리자로 Celery 를 선택한 이유**

1. 압도적인 Star 와 풍부한 문서 내용
2. 비교적 입문하기 쉬움
3. 다양한 옵션과 Task 작업을 관리하기 좋은 구조

`@app.task` 의 역할을 두 Celery App 에 분할해서 할당했는데, Python Server ⇒ Spring Server 로 전송해야 할 Task 는 "Sender App" 에서 전담하고, 반대로 Spring Server ⇒ Python Server 로 수신하는 Listener 는 "Worker App" 으로 만들어서 별도의 Worker Process 로 띄워 주기적으로 Queue 를 Polling 해서 메시지가 있는지 확인하고 처리하는 역할을 수행한다.

{% @mermaid/diagram content="flowchart
A(AWS SQS FIFO Queue)
B\[Spring Server - Core]<--Send/Receive-->A
C1\[Python Server\_FastAPI with Celery - Sender Process]--Send-->A
C2\[Celery App - Worker Process]-.Receiving(Long-Polling).->A
B<--HTTP-->C1
C2-.Depends-on (Running on the Same Instance).->C1" %}

#### **Spring Server 에서 Message Publishing 하기**

Python 서버 측에서 메시지 수신을 받기 위해 Spring 측에서는 Python 서버에서 즉 Celery 에서 요구하는 메시지 Protocol 에 맞춰서 보내야 한다. 하지만 Celery 를 사용하는 어플리케이션 끼리 주고받는 상황이 일반적인 사용 사례인것으로 보여서 우리처럼 특이한, Spring 와 Celery App 과의 통신을 위한 사례가 있는지 열심히 찾아봤다.

작은 오픈소스로 몇가지 공개된 사례가 있어서 이를 바탕으로 Spring to Celery Message for SQS **메시지 생성기**를 만들기로 했다. (해당 오픈소스는 RabbitMQ 위주로 작성한 것이라 사용할 수 없었다)

Celery 의 기능을 세부적으로 이용하기 위해서는 메시지의 옵션을 적절히 설정해 주어야 하지만, 꼭 필요한 정보(Queue 이름, Task 이름, ID 등)만 사용해서 생성기를 만들었다. 이렇게 하면 메시지를 생성해서 Queue 에 보내게 되고, 이를 기반으로 Polling 하고 있던 Worker 가 수신하여 각 Task 를 처리한다.

```java
// Celery (protocol v2) Message Generator Helper Function
public static String generate(String taskName, String queueName, String payload) throws JsonProcessingException {
    return CeleryMessageProtocolV2.of(taskName, queueName, payload);
}
```

코드가 불필요하게 지면을 차지하기 때문에 별도의 gist 로 공유합니다.

<https://gist.github.com/kwt1326/4c33dd703977f8e69eb26f98ce7be832>

### 끝난줄 알았지만

Celery 를 사용한 분산 작업 처리는 분명 매우 효율적으로 작동한다. 하지만 사용하다 보니 치명적인 문제를 발견했는데, 위의 내용을 읽어봤을 때 아마 갸우뚱할 부분이 있을 것이다. 반대의 경우 (Python => Spring) 메시지 생성기 구현이 없는 이유는 **설계상 비효율적인 부분이 발견되어서 중단했고** 그것은 **메시지 페이로드 인터페이스** 때문이었다.

Celery 를 사용하면 요구하는 JSON 메시지 포맷을 맞춰서 보내야만 Celery 가 메시지의 내용을 분석하고 어느 Task 에 작업을 전달할 지 알 수 있다. 반면 Spring 에서의 SQS 라이브러리는 그런 인터페이스를 별도로 요구하지 않고 작업을 처리하는데 필수적인 데이터만 전달하면 되기 때문에, **Spring 서버를 작업하는 개발자의 입장에서는 이 메시지 인터페이스가 매우 불편할 수 있다.**

처음 Celery 로 구현할 때는 이런 인터페이스가 당연한 걸로 생각했었는데, 비교해서 분석해보니 Celery 에 강하게 결합되는 메시지가 지금 당장 사용할 수 있다고 해도 지속적인 개선에 걸림돌이 될 수 있을 것이라 판단했다.

그래서 고민 끝에, 작업한 내용은 아까웠지만 팀원들과 이 내용에 대해서 회의 후 전면적으로 수정하기로 결정했다. (2부에서 계속…)
