관계형 로직의 데이터베이스 - 다시 설계

Pt J·어제
post-thumbnail

관계형 로직의 데이터베이스 - 다시 설계

배경

FastAPI에서 Rust 로직을 호출하려면
Rust 쪽에서 Axum이나 Actix-web 등으로 가벼운 HTTP 서버를 띄워 FastAPI와 통신하게 하거나,
PyO3를 통해 Python 모듈로 빌드할 수 있다.

비교 항목PyO3 바인딩 방식 (.so / .pyd 확장)Axum 독립 서버 방식 (HTTP / gRPC)
아키텍처 형태단일 프로세스 (Python 프로세스 내에 종속)다중 프로세스 (네트워크로 연결된 마이크로서비스)
결합도 (Coupling)강한 결합. Rust 코드 변경 시 Python 패키지 재빌드 필요느슨한 결합. API 명세(Contract)만 맞으면 독립적 변경 가능
GIL (인터프리터 락)Python의 GIL 영향권 내에 있음 (쓰레드 제어 복잡)Python GIL과 완전히 무관. Rust 고유의 멀티쓰레딩 100% 활용
장애 격리 (Isolation)Rust나 C-FFI 단에서 Panic/Crash 발생 시 Python 전체 종료Rust 서버가 죽어도 FastAPI는 살아있음 (장애 전파 차단)
확장성 (Scaling)Python 프로세스를 늘릴 때 Rust 메모리도 같이 늘어남계산량이 많은 Rust 서버만 독립적으로 수평 확장(Scale-out) 가능
CI/CD 및 빌드maturin 등 파이썬 확장 빌드 툴체인 필요 (OS별 컴파일 복잡)표준 cargo build 및 일반적인 Docker 배포 파이프라인 사용

우리는 지금까지 PyO3를 사용했는데,
일반적인 백엔드 웹 아키텍처에서는 PyO3를 통한 내부 바인딩보다
Axum이나 gRPC를 활용한 독립 서버 분리 방식을 활용한
HTTP 마이크로서비스(Microservice)로 분리한 구조가
압독적으로 더 많이 쓰인다고 한다.

인프라/툴링, 싱글 프로세스 고속 연산 등에서는 PyO3 바인딩이 더 유용하지만
일반적인 백엔드 상황에서는 마이크로서비스가 더 낫다나.
가령 네트워크 오버헤드(HTTP 통신 비용) 조차 허용되지 않는
거대한 인공지능 모델 파이프라인 같은 데서는 PyO3가 좋다는 건데...
아무튼 내 실습 환경에서는 굳이?인 거다.

사전 조사를 하지 않고 일단 공부부터 했더니 덜 쓰이는 방식을 사용하게 되었다.
그런 의미에서 gRPC를 통해 서버를 분리해 보자.

gRPC는 JSON 텍스트 파싱 대신 이진(Binary) 스트림으로 데이터를 주고받아
HTTP API 마이크로서비스에 비해 데이터 밀도가 높고 속도가 압도적이다.

구조 변경

기존에 하던 것은 잠시 옆으로 치워두고 새 프로젝트를 생성해 보겠다.

~/workspace$ mv saju-analysis saju-analysis-legacy
~/workspace$ mkdir saju-analysis && cd saju-analysis
~/workspace/saju-analysis$ cargo new saju-engine
~/workspace/saju-analysis$ mkdir saju-gateway && cd saju-gateway
~/workspace/saju-analysis/saju-gateway$ uv init
~/workspace/saju-analysis/saju-gateway$ cd ..
~/workspace/saju-analysis$ mkdir proto
~/workspace/saju-analysis$ touch proto/saju.proto
~/workspace/saju-analysis$ touch saju-engine/build.rs
~/workspace/saju-analysis$ touch saju-gateway/generate_proto.sh
~/workspace/saju-analysis$ tree
.
├── proto
│   └── saju.proto # 양쪽 언어가 공유할 명세서
├── saju-engine
│   ├── Cargo.toml # Rust 프로젝트 설정 파일
│   ├── build.rs # 정적 컴파일 시 proto/saju.proto 참조
│   └── src # 코드 추가 예정
│       └── main.rs # gRPC 구동 서버
└── saju-gateway
    ├── README.md
    ├── generate_proto.sh # Python용 프로토버퍼 컴파일 스크립트
    ├── main.py # FastAPI 엔드포인트 및 gRPC 스텁 호출
    └── pyproject.toml # Python 프로젝트 설정 파일

5 directories, 8 files

옆으로 치워둔 프로젝트에서 필요한 파일들을 복사해 온다.

~/workspace/saju-analysis$ cp -r ../saju-analysis-legacy/scripts .
~/workspace/saju-analysis$ cp ../saju-analysis-legacy/src/db.rs saju-engine/src
~/workspace/saju-analysis$ cp ../saju-analysis-legacy/src/logger.rs saju-engine/src
~/workspace/saju-analysis$ cp ../saju-analysis-legacy/.env saju-engine
~/workspace/saju-analysis$ cp ../saju-analysis-legacy/docker-compose.yml saju-engine/ 
~/workspace/saju-analysis$ cp -r ../saju-analysis-legacy/postgres_data .
~/workspace/saju-analysis$ # 프로젝트 루트가 git 저장소였다면 .git 및 .gitignore 가 생성되지 않지만
~/workspace/saju-analysis$ # 여기선 git init을 안 했으므로 하위 디렉토리에 그것들이 존재한다
~/workspace/saju-analysis$ tree -a -I .git -I postgre_data
.
├── proto
│   └── saju.proto
├── saju-engine
│   ├── .env
│   ├── .gitignore
│   ├── Cargo.toml
│   ├── docker-compose.yml
│   ├── build.rs
│   └── src
│       ├── db.rs
│       ├── logger.rs
│       └── main.rs
├── saju-gateway
│   ├── .gitignore
│   ├── .python-version
│   ├── README.md
│   ├── generate_proto.sh
│   ├── main.py
│   └── pyproject.toml
└── scripts
    ├── init.sql
    ├── insert_ganji_interaction.sql
    └── insert_ganji_metadata.sql

6 directories, 18 files

필요한 Rust 파일을 생성하면 다음과 같다.

~/workspace/saju-analysis$ touch saju-engine/src/models.rs
~/workspace/saju-analysis$ touch saju-engine/src/saju.rs
~/workspace/saju-analysis$ touch saju-engine/src/services.rs
~/workspace/saju-analysis$ mkdir saju-engine/src/analysis
~/workspace/saju-analysis$ touch saju-engine/src/analysis/mod.rs
~/workspace/saju-analysis$ touch saju-engine/src/analysis/pattern.rs
~/workspace/saju-analysis$ touch saju-engine/src/analysis/interaction.rs
~/workspace/saju-analysis$ tree
.
├── proto
│   └── saju.proto # 양쪽 언어가 공유할 명세서
├── saju-engine
│   ├── Cargo.toml # Rust 프로젝트 설정 파일
│   ├── docker-compose.yml # PostgreSQL이 실행되는 docker 설정 파일
│   ├── build.rs # 정적 컴파일 시 proto/saju.proto 참조
│   └── src
│       ├── analysis
│       │   ├── interaction.rs # DB로부터 글자간의 관계 받아서 분석
│       │   ├── mod.rs
│       │   └── pattern.rs # DB를 사용하지 않고 할 수 있는 분석
│       ├── db.rs # DB 연결 풀 초기화 및 해제 (수정 안 해도 무방)
│       ├── logger.rs # 로그 수준 및 옵션 설정 (수정 안 해도 무방)
│       ├── main.rs # gRPC 구동 서버 (services.rs의 함수 호출)
│       ├── models.rs # 데이터 구조를 담은 파일
│       ├── saju.rs # 가장 기본적인 사주 계산
│       └── services.rs # 사용자에게 제공되는 로직의 구현
├── saju-gateway
│   ├── README.md
│   ├── generate_proto.sh # Python용 프로토버퍼 컴파일 스크립트
│   ├── main.py # FastAPI 엔드포인트 및 gRPC 스텁 호출
│   └── pyproject.toml # Python 프로젝트 설정 파일
└── scripts
    ├── init.sql # DB 스키마대로 테이블 및 인덱스 생성
    ├── insert_ganji_interaction.sql # 간지간의 관계 데이터 저장
    └── insert_ganji_metadata.sql # 간지 메타데이터 저장

7 directories, 20 files

흐름

여기서 사용된 계산 알고리즘은 매우 단순화된 형태로,
실제 사주 분석과는 차이가 있음을 밝힌다.

  • src/saju.rs
    • find_jd() : 율리우스일을 계산하는 도우미 함수
    • alculate_saju() : 생년월일시를 사주팔자로 변환하는 함수
  • src/analysis/pattern.rs
    • get_element_from_code() : 천간/지지 코드를 통해 그 글자의 오행을 판단하는 도우미 함수
    • calculate_element_scores() : 월간과 일간에 가중치가 들어간 오행 점수를 계산하는 함수
    • determine_strength() : 사주팔자의 신강/신약 여부를 판단하는 함수
    • map_ten_gods() : 일간과 대상 글자의 십성 관계를 판단하는 함수
  • src/analysis/interaction.rs
    • find_interactions() : DB상의 관계 데이터를 사용하여 관계를 추출하는 함수
  • src/service.rs
    • fetch_ganji_metadata() : 천간/지지 코드를 통해 글자 자체의 메타데이터를 추출하는 함수
    • register_user_analysis() : 사용자 정보를 넣어 사주 분석 후 DB에 저장하는 함수
    • get_analysis_by_user() : 이름과 생년월일을 통해 기존 분석을 조회하는 함수

[입력] /analyis
1. 이름, 성별, 생년월일시를 전달받아 register_user_analysis() 함수에서 다음을 수행한다.
2. alculate_saju() 함수를 통해 생년월일시를 사주 글자 코드로 변환한다.
3. calculate_element_scores() 함수를 통해 오행 점수를 계산한다.
4. determine_strength() 함수를 통해 신강/신약 여부를 계산한다.
5. find_interactions() 함수를 통해 글자간의 관계를 불러온다.
6. 작업 성공 여부를 반환한다.

[출력] /get/{id}
1. 이름을 전달받아 get_analysis_by_user() 함수에서 다음을 수행한다.
2. DB에서 사주 분석 데이터를 조회한다. (없으면 [입력]으로 유도)
3. fetch_ganji_metadata() 함수를 통해 각 글자의 메타데이터를 불러온다.
4. map_ten_gods() 함수를 통해 각 글자의 십성을 불러온다.
5. 각 글자 정보와 사주 분석 데이터를 통합하여 반환한다.

자, 이제 Rust 코드부터 하나씩 구현해 보자.

profile
Peter J Online Space - since July 2020 | 아무데서나 채용해줬으면 좋겠다

0개의 댓글