
Python과 Rust가 공유할 엄격한 명세서를 작성한다.
이것을 통해 Python과 Rust에서 프로토콜 버퍼 모듈을 생성하여 통신하게 된다.
proto/saju.protosyntax = "proto3"; package saju; // 사주 서비스 정의 service SajuEngine { rpc Register (RegisterRequest) returns (RegisterResponse); rpc GetProfile (ProfileRequest) returns (ProfileResponse); rpc CheckDbAlive (DbStatusRequest) returns (DbStatusResponse); } message RegisterRequest { string name = 1; string gender = 2; string birth_date = 3; // ISO 8601 String bool is_solar = 4; } message RegisterResponse { bool success = 1; string msg = 2; } message ProfileRequest { string name = 1; } message GanjiMetadata { int32 code = 1; string name_ko = 2; string name_hani = 3; string type_name = 4; string element = 5; string yin_yang = 6; } message ProfileResponse { string name = 1; string birth_date = 2; repeated GanjiMetadata ganjis = 3; // repeated는 Vec/List를 의미 repeated string ten_gods = 4; string analysis_json = 5; // JSONB 데이터를 문자열로 직렬화하여 전송 } message DbStatusRequest {} message DbStatusResponse { bool is_alive = 1; }
Rust 설정 파일에 필요한 크레이트들을 적절한 버전으로 추가해 준다.
saju-engine/Cargo.toml[package] name = "saju-engine" version = "0.1.0" edition = "2024" [dependencies] # gRPC 및 Protobuf 통신 핵심 의존성 tonic = "0.14" prost = "0.14" tonic-prost = "0.14" # 비동기 런타임 및 가반 시스템 tokio = { version = "1.52", features = ["full"] } chrono = { version = "0.4", features = ["serde"] } # 데이터베이스 및 직렬화 dotenvy = "0.15" uuid = { version = "1.23", features = ["v7", "serde"] } sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "macros", "uuid", "chrono", "json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # 로그 수집 tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } tracing-appender = "0.2" # 천문 연산 swiss-eph = "0.2" [build-dependencies] # 빌드 시점에 saju.proto를 Rust 코드로 컴파일하는 도구 tonic-build = "0.14" tonic-prost-build = "0.14"
프로젝트 루트에 proto 파일의 경로를 담아 build.rs 파일을 생성해 주어야
cargo build 명령어를 사용하여 빌드했을 때
src/main.rs 에서 사용할 수 있는 gRPC 코드가 자동 생성된다.
saju-engine/build.rsfn main() -> Result<(), Box<dyn std::error::Error>> { tonic_prost_build::configure() .compile_protos( &["../proto/saju.proto"], &["../proto"] )?; Ok(()) }
사용되는 모든 데이터 모델을 모아 놓는다.
서로 연관되어 있는 데이터는 이곳저곳에 분산되어 있으면
관리하기 어려워지므로 한 곳에 모아놓는 게 좋다.
saju-engine/src/models.rsuse serde::{Deserialize, Serialize}; use sqlx::FromRow; use chrono::{DateTime, Utc}; /// 간지 메타데이터 for SajuProfile /// table: ganji_metadata #[derive(Debug, Serialize, Deserialize, FromRow, Clone)] pub struct GanjiMetadata { pub code: i16, // 0~9, 10~21 pub name_ko: String, // 간지 이름 (한글) pub name_hani: String, // 간지 이름 (한자) pub type_name: String, // '천간' / '지지' pub element: String, // '목' / '화' / '토' / '금' / '수' pub yin_yang: String, // '음' / '양' } /// 십성 구분 for SajuProfile #[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Copy)] pub enum TenGods { BiGyeon, // 비견 GeopJae, // 겁재 SikShin, // 식신 SangGwan, // 상관 PyeonJae, // 편재 JeongJae, // 정재 PyeonGwan, // 편관 JeongGwan, // 정관 PyeonIn, // 편인 JeongIn, // 정인 } /// 오행 점수 for SajuAnalysis #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ElementScore { pub wood: i32, pub fire: i32, pub earth: i32, pub metal: i32, pub water: i32, } /// 신강/신약 여부 for SajuAnalysis #[derive(Debug, Serialize, Deserialize, Clone)] pub enum Strength { Strong(i32), // 신강 Weak(i32), // 신약 Neutral(i32), // 보통 } /// 관계 분석 데이터 for SajuAnalysis /// table: ganji_interaction + interaction_groups + interaction_metadata #[derive(Debug, Serialize, Deserialize)] pub struct InteractionReport { pub title: String, // 관계 이름 pub category: String, // 상호작용 유형 pub keywords: Vec<String>, // 핵심 에너지 목록 pub interpretation: String, // 인문학적 해석 } /// 분석 결과 전체 데이터 #[derive(Debug, Serialize, Deserialize, FromRow)] pub struct SaJuProfile { pub name: String, // 사용자 이름 pub gender: String, // 성별 pub birth_date: DateTime<Utc>, // 생년월일시 pub is_solar: bool, // 양력 여부 pub ganji_data: Vec<GanjiMetadata>, // 사주의 여덟 글자에 대한 메타데이터 pub ten_gods: Vec<TenGods>, // 사주의 여덟 글자의 십성 pub analysis_result: serde_json::Value, // 사주 분석 결과 JSON }
생년월일시를 사주의 여덟 글자로 변환하는 코드를 작성한다.
생일이 음력인 경우 양력으로 변환하여 계산한다.
saju-engine/src/saju.rsuse swiss_eph::safe; use chrono::{DateTime, Utc, Datelike, Timelike, TimeZone}; /// 율리우스일 계산 fn find_jd(year: i32, target_longitude: f64, start_month: i32) -> f64 { let mut jd = safe::julday(year, start_month, 1, 0.0); let flags = swiss_eph::SEFLG_SPEED; for _ in 0..10 { let res = safe::calc_ut(jd, swiss_eph::SE_SUN, flags).unwrap(); let diff = (res.longitude - target_longitude + 180.0).rem_euclid(360.0) - 180.0; // 오차가 충분히 작아질 때까지만 반복 if diff.abs() < 0.00001 { break; } jd -= diff / 0.9856; } jd } /// 천문학적 합삭 추적 (음력 초하루) /// 태양과 달의 황경 차이가 0이 되는 정밀한 율리우스일 반환 fn find_new_moon_near(approx_jd: f64) -> f64 { let mut jd = approx_jd; let flags = 0; for _ in 0..15 { let sun = safe::calc_ut(jd, swiss_eph::SE_SUN, flags).unwrap(); let moon = safe::calc_ut(jd, swiss_eph::SE_MOON, flags).unwrap(); let mut diff = (moon.longitude - sun.longitude).rem_euclid(360.0); if diff > 180.0 { diff -= 360.0; } // 오차가 충분히 작아질 때까지만 반복 if diff.abs() < 0.00001 { break; } // 달이 태양보다 하루에 약 12.19도 더 빠르게 이동함을 기준으로 보정 jd -= diff / 12.19075; } jd } /// 음력 생년월일을 양력 생년월일로 변환 fn convert_lunar_to_solar(lunar_date: DateTime<Utc>) -> DateTime<Utc> { let year = lunar_date.year(); let month = lunar_date.month() as i32; let day = lunar_date.day() as f64; // 해당 연도 1월 1일을 기준, 음력 1월 초하루는 약 한 달 뒤에 있다 let base_jd = safe::julday(year, 1, 1, 0.0); let mut current_new_moon = find_new_moon_near(base_jd + 30.0); // 생월을 기준으로 음력 월의 초하루 율리우스일 계산 for _ in 1..month { current_new_moon = find_new_moon_near(current_new_moon + 29.53); // 삭망월 주기 반영 } // 초하루(1일) 시점에 생일을 기준으로 일수(day - 1)를 더한다 let solar_jd = current_new_moon + (day - 1.0) + (lunar_date.hour() as f64 / 24.0) + (lunar_date.minute() as f64 / 1440.0); // 율리우스일을 양력 날짜로 변환 let (y, m, d, hour_fraction) = safe::revjul(solar_jd); let total_minutes = (hour_fraction * 24.0 * 60.0).round() as u32; let h = total_minutes / 60; let min = total_minutes % 60; Utc.with_ymd_and_hms(y, m as u32, d as u32, h, min, 0).unwrap() } /// 생년월일시를 입력받아 여덟 개 글자로 변환 pub fn calculate_saju(birth_date: DateTime<Utc>, is_solar: bool) -> [i16; 8] { // 음력일 경우 양력 시간선으로 정밀 치환 후 프로세스 진행 let solar_birth_date = if is_solar { birth_date } else { convert_lunar_to_solar(birth_date) }; // 서울 표준시(KST)와 서울 지방시(LST)의 오차 -30분 보정 적용 let target = solar_birth_date - chrono::Duration::minutes(30); let target_jd = safe::julday( target.year(), target.month() as i32, target.day() as i32, target.hour() as f64 + target.minute() as f64 / 60.0, ); // 0:인 1:묘 2:진 3:사 4:오 5:미 6:신 7:유 8:술 9:해 10:자 11:축 // 0:입춘 1:경칩 2:청명 3:입하 4:망종 5:소서 6:입추 7:백로 8:한로 9:입동 10:대설 11:소한 let jeolgi_points = [315.0, 345.0, 15.0, 45.0, 75.0, 105.0, 135.0, 165.0, 195.0, 225.0, 255.0, 285.0]; // 연주 계산 let mut year = target.year(); let ipchun = find_jd(year, jeolgi_points[0], 2); // 해당 연도 입춘 전에 태어난 사람은 전년도로 계산한다 if target_jd < ipchun { year -= 1; } let year_gan = ((year - 4) % 10).rem_euclid(10) as i16; let year_ji = ((year - 4) % 12).rem_euclid(12) as i16 + 10; // 월주 계산 let mut month_idx = 0; for i in 0..12 { let search_year = if i == 11 { year + 1 } else { year }; let start_month = ((i as i32 + 1) % 12) + 1; let jeolgi_jd = find_jd(search_year, jeolgi_points[i], start_month); // 월 구분 기준이 되는 절기가 사용자 생일을 넘긴 순간, 사용자 생일은 이전 달에 있던 게 된다. if target_jd < jeolgi_jd { month_idx = if i == 0 { 11 } else { i - 1 }; break; } // 루프 끝까지 돌 경우 11(축월)로 마무리 month_idx = i; } let month_gan = ((year_gan * 2 + 2 + month_idx as i16) % 10) as i16; let month_ji = ((month_idx as i16 + 2) % 12) + 10; // 일주 계산: 글로벌 표준 천문학 일수 변환 매크로 적용 let day_num = (target_jd + 0.5).floor() as i64; let ganji_idx = (day_num + 49) % 60; let day_gan = (ganji_idx % 10) as i16; let day_ji = ((ganji_idx % 12) + 10) as i16; // 시주 계산 let hour_idx = (((target.hour() as i16 + 1) % 24) / 2) as i16; let hour_gan = ((day_gan * 2 + hour_idx) % 10) as i16; let hour_ji = hour_idx + 10; [year_gan, year_ji, month_gan, month_ji, day_gan, day_ji, hour_gan, hour_ji] }
사주 분석 부분은 모듈화하여 작성한다.
saju-engine/analysis/mod.rspub mod interaction; pub mod pattern;
DB에서 관계를 뽑아내지 않고 글자들 자체로 얻을 수 있는 분석을
한 파일에 모아 놓는다.
saju-engine/analysis/pattern.rsuse crate::models::{ElementScore, Strength, TenGods}; /// 각 글자의 오행 판단 fn get_element_from_code(code: i16) -> i32 { // 0: 목, 1: 화, 2: 토, 3: 금, 4: 수 match code { 0 | 1 | 12 | 13 => 0, 2 | 3 | 15 | 16 => 1, 4 | 5 | 11 | 14 | 17 | 20 => 2, 6 | 7 | 18 | 19 => 3, 8 | 9 | 21 | 10 => 4, _ => -1, } } /// 오행 점수 계산 pub fn calculate_element_scores(codes: &[i16; 8]) -> ElementScore { let mut score = ElementScore { wood: 0, fire: 0, earth: 0, metal: 0, water: 0, }; let weights = [10, 10, 10, 25, 15, 10, 10, 10]; for (code, weight) in codes.iter().zip(weights.iter()) { let element = get_element_from_code(*code); match element { 0 => score.wood += weight, 1 => score.fire += weight, 2 => score.earth += weight, 3 => score.metal += weight, 4 => score.water += weight, _ => {}, } } score } /// 신강/신약 여부 판단 pub fn determine_strength(day_gan: i16, scores: &ElementScore) -> Strength { let self_element = get_element_from_code(day_gan); // 일간과 같은 오행 + 그것을 생하는 오행 let supporter_score = match self_element { 0 => scores.wood + scores.water, 1 => scores.fire + scores.wood, 2 => scores.earth + scores.fire, 3 => scores.metal + scores.earth, 4 => scores.water + scores.metal, _ => 0, }; if supporter_score >= 55 { Strength::Strong(supporter_score) } else if supporter_score <= 45 { Strength::Weak(supporter_score) } else { Strength::Neutral(supporter_score) } } /// 일간과 비교하여 각 글자의 십성 판단 pub fn map_ten_gods(day_gan: i16, target: i16) -> TenGods { let self_element = get_element_from_code(day_gan); let target_element = get_element_from_code(target); let is_same_yin_yang = (day_gan % 2) == (target % 2); let relation = (target_element - self_element).rem_euclid(5); match (relation, is_same_yin_yang) { (0, true) => TenGods::BiGyeon, (0, false) => TenGods::GeopJae, (1, true) => TenGods::SikShin, (1, false) => TenGods::SangGwan, (2, true) => TenGods::PyeonJae, (2, false) => TenGods::JeongJae, (3, true) => TenGods::PyeonGwan, (3, false) => TenGods::JeongGwan, (4, true) => TenGods::PyeonIn, (4, false) => TenGods::JeongIn, _ => unreachable!(), } } // 십성 한글로 변환 impl TenGods { pub fn as_ko_str(&self) -> &'static str { match self { TenGods::BiGyeon => "비견", TenGods::GeopJae => "겁재", TenGods::SikShin => "식신", TenGods::SangGwan => "상관", TenGods::PyeonJae => "편재", TenGods::JeongJae => "정재", TenGods::PyeonGwan => "편관", TenGods::JeongGwan => "정관", TenGods::PyeonIn => "편인", TenGods::JeongIn => "정인", } } }
DB에서 관계를 뽑아내어 분석하는 코드도 별개의 파일에 작성한다.
관계는 둘 이상의 글자가 만나야 의미가 있으므로
중복 없이 입력한 글자들을 입력으로 넣어 둘 이상이 나온 관계만 취급한다.
사주 분석은 여기서 모두 다루기 복잡하므로
쟁합(여러 개의 양간이 하나의 음간을 차지하려고 다투는 형국)이나
투합(여러 개의 음간이 하나의 양간을 차지하려고 다투는 형국) 같은 건 생략하겠다.
saju-engine/analysis/interation.rsuse sqlx::PgPool; use crate::models::InteractionReport; /// 사주 코드에 포함된 관계를 뽑아내어 반환 #[tracing::instrument(skip(pool))] pub async fn find_interactions( pool: &PgPool, codes: &[i16] ) -> Result<Vec<InteractionReport>, sqlx::Error> { let reports = sqlx::query_as!( InteractionReport, r#" SELECT metadata.title as "title!", groups.category as "category!", metadata.keyword as "keywords!", metadata.interpretation as "interpretation!" FROM ganji_interaction interaction JOIN interaction_groups groups ON interaction.group_id = groups.id JOIN interaction_metadata metadata ON interaction.group_id = metadata.group_id WHERE interaction.code = ANY($1) GROUP BY groups.id, metadata.id, metadata.title, groups.category, metadata.keyword, metadata.interpretation HAVING COUNT(interaction.id) >= 2 ORDER BY groups.id ASC "#, codes ) .fetch_all(pool) .await?; Ok(reports) }
사용자 정보를 받아 분석하고 DB에 결과를 저장하는 함수와
기존에 분석한 바 있는 사용자 정보를 불러오는 함수를 작성한다.
각 글자는 숫자 코드의 형태로 전달 시 사용자가 알아보기 어려우므로
메타데이터로 변환하여 반환한다.
saju-engine/src/service.rsuse sqlx::PgPool; use chrono::{DateTime, Utc}; use crate::saju; use crate::analysis::{interaction, pattern}; use crate::models::{GanjiMetadata, SaJuProfile}; #[derive(Debug)] pub enum SajuError { NotFound, DatabaseError(sqlx::Error), } // 글자 코드를 간지 메타데이터로 변환 #[tracing::instrument(skip(pool))] pub async fn fetch_ganji_metadata( pool: &PgPool, codes: &[i16; 8] ) -> Result<Vec<GanjiMetadata>, sqlx::Error> { let raw_data = sqlx::query_as!( GanjiMetadata, r#" SELECT code, name_ko, name_hani, type as "type_name!", element::text as "element!", yin_yang::text as "yin_yang!" FROM ganji_metadata WHERE code = ANY($1) "#, codes ) .fetch_all(pool) .await?; let lookup: HashMap<i16, GanjiMetadata> = raw_data .into_iter() .map(|g| (g.code, g)) .collect(); let mut ordered_ganji = Vec::with_capacity(8); for &code in codes { if let Some(meta) = lookup.get(&code).cloned() { ordered_ganji.push(meta); } } Ok(ordered_ganji) } // [입력] 사용자 정보를 받아 분석하고 DB에 저장 #[tracing::instrument(skip(pool))] pub async fn register_user_analysis( pool: &PgPool, name: &str, gender: &str, birth_date: DateTime<Utc>, is_solar: bool, ) -> Result<(), SajuError> { let mut tx = pool.begin().await .map_err(SajuError::DatabaseError)?; let codes = saju::calculate_saju(birth_date, is_solar); let element_scores = pattern::calculate_element_scores(&codes); let strength = pattern::determine_strength(codes[4], &element_scores); let interactions = interaction::find_interactions(pool, &codes).await .map_err(SajuError::DatabaseError)?; let analysis_result = serde_json::json!({ "element_scores": element_scores, "strength": strength, "interactions": interactions }); sqlx::query!( r#" INSERT INTO saju_profiles (name, gender, birth_date, is_solar, year_gan, year_ji, month_gan, month_ji, day_gan, day_ji, hour_gan, hour_ji, analysis_result) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) "#, name, gender, birth_date, is_solar, codes[0], codes[1], codes[2], codes[3], codes[4], codes[5], codes[6], codes[7], analysis_result ) .execute(&mut *tx) .await .map_err(SajuError::DatabaseError)?; tx.commit().await.map_err(SajuError::DatabaseError)?; Ok(()) } // [출력] 사용자 정보를 받아 DB에 저장된 정보 출력 #[tracing::instrument(skip(pool))] pub async fn get_analysis_by_user( pool: &PgPool, name: &str, ) -> Result<SaJuProfile, SajuError> { let profile = sqlx::query!( r#" SELECT name, gender, birth_date, is_solar, year_gan, year_ji, month_gan, month_ji, day_gan, day_ji, hour_gan, hour_ji, analysis_result FROM saju_profiles WHERE name = $1 "#, name ) .fetch_optional(pool) .await .map_err(SajuError::DatabaseError)? .ok_or(SajuError::NotFound)?; let codes = [ profile.year_gan.unwrap(), profile.year_ji.unwrap(), profile.month_gan.unwrap(), profile.month_ji.unwrap(), profile.day_gan.unwrap(), profile.day_ji.unwrap(), profile.hour_gan.unwrap(), profile.hour_ji.unwrap(), ]; let ganji_data = fetch_ganji_metadata(pool, &codes).await .map_err(SajuError::DatabaseError)?; let ten_gods: Vec<_> = codes.iter() .map(|&c| pattern::map_ten_gods(codes[4], c)) .collect(); Ok(SaJuProfile { name: profile.name, gender: profile.gender.unwrap_or_else(|| "M".to_string()), birth_date: profile.birth_date, is_solar: profile.is_solar.unwrap_or(true), ganji_data: ganji_data, ten_gods: ten_gods, analysis_result: profile.analysis_result.unwrap_or_default(), }) }
saju-engine/src/main.rsmod db; mod logger; mod models; mod saju; mod analysis; mod service; use tonic::{transport::Server, Request, Response, Status}; use chrono::DateTime; use std::net::SocketAddr; // proto/saju.proto 와 build.rs로 생성된 프로토컬 버퍼 모듈 로드 pub mod saju_proto { tonic::include_proto!("saju"); } use saju_proto::saju_engine_server::{SajuEngine, SajuEngineServer}; use saju_proto::{ RegisterRequest, RegisterResponse, ProfileRequest, ProfileResponse, GanjiMetadata, DbStatusRequest, DbStatusResponse }; pub struct MySajuEngine { pool: sqlx::PgPool, } #[tonic::async_trait] impl SajuEngine for MySajuEngine { // 등록 RPC async fn register( &self, request: Request<RegisterRequest>, ) -> Result<Response<RegisterResponse>, Status> { let req = request.into_inner(); let birth_date = DateTime::parse_from_rfc3339(&req.birth_date) .map_err(|_| Status::invalid_argument("유효하지 않은 날짜 형식"))? .with_timezone(&chrono::Utc); service::register_user_analysis(&self.pool, &req.name, &req.gender, birth_date, req.is_solar) .await .map_err(|e| Status::internal(format!("등록 실패: {:?}", e)))?; Ok(Response::new(RegisterResponse { success: true, msg: "gRPC를 통해 성공적으로 등록 완료".into(), })) } // 조회 RPC async fn get_profile( &self, request: Request<ProfileRequest>, ) -> Result<Response<ProfileResponse>, Status> { let req = request.into_inner(); let profile = service::get_analysis_by_user(&self.pool, &req.name) .await .map_err(|e| match e { service::SajuError::NotFound => Status::not_found("존재하지 않는 사용자"), service::SajuError::DatabaseError(db_err) => Status::internal(db_err.to_string()), })?; let grpc_ganjis = profile.ganji_data.into_iter().map(|g| GanjiMetadata { code: g.code as i32, name_ko: g.name_ko, name_hani: g.name_hani, type_name: g.type_name, element: g.element, yin_yang: g.yin_yang, }).collect(); let grpc_ten_gods: Vec<String> = profile.ten_gods.into_iter() .map(|s| s.as_ko_str().to_string()) .collect(); Ok(Response::new(ProfileResponse { name: profile.name, birth_date: profile.birth_date.to_rfc3339(), ganjis: grpc_ganjis, ten_gods: grpc_ten_gods, analysis_json: profile.analysis_result.to_string(), })) } // DB 헬스체크 RPC async fn check_db_alive( &self, _request: Request<DbStatusRequest>, ) -> Result<Response<DbStatusResponse>, Status> { let alive = db::is_alive(); Ok(Response::new(DbStatusResponse { is_alive: alive })) } } sync fn shutdown_signal() { tokio::signal::ctrl_c() .await .expect("Ctrl+C 신호 핸들러 등록 실패"); tracing::info!("Ctrl+C 감지"); } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { dotenvy::dotenv().ok(); let _log_guard = logger::init_tracing(); tracing::info!("사주 분석 엔진 내부 시스템 초기화 시작"); let db_url = std::env::var("DATABASE_URL") .expect("환경변수 DATABASE_URL 설정 필요"); db::connect(&db_url).await?; let pool = db::DB_POOL.get() .expect("DB 커넥션 풀 로드 실패") .clone(); // gRPC 네이티브 네트워킹 설정 및 서버 기동 let addr: SocketAddr = "127.0.0.1:50051".parse()?; let engine = MySajuEngine { pool }; tracing::info!("gRPC 통신용 바이너리 프로토콜 채널 활성화: {}", addr); Server::builder() .add_service(SajuEngineServer::new(engine)) .serve_with_shutdown(addr, shutdown_signal()) .await?; // 서버 연동 종료 절차 db::close().await; Ok(()) }
saju-engine/build.rs 가 정상 작동하려면
OS 레벨에 protoc가 설치되어 있어야 하므로
다음 명령어를 통해 설치한다. (Mac 기준)
~/workspace/saju-analysis$ brew install protobuf
도커 서비스를 실행하고 Rust 엔진을 컴파일 및 실행한다.
~/workspace/saju-analysis$ cd saju-engine ~/workspace/saju-analysis/saju-engine$ docker-compose up -d ~/workspace/saju-analysis/saju-engine$ cargo build --release ~/workspace/saju-analysis/saju-engine$ cargo run
터미널에서 gRPC 작동을 확인하기 위해서는 grpcurl이 필요하다.
새 터미널에서 grpcurl을 설치하고 명령어를 입력해 보자.
~/workspace/saju-analysis/saju-engine$ brew install grpcurl ~/workspace/saju-analysis/saju-engine$ grpcurl -plaintext \ -proto ../proto/saju.proto \ '127.0.0.1:50051' \ saju.SajuEngine/CheckDbAlive { "isAlive": true }
~/workspace/saju-analysis/saju-engine$ grpcurl -plaintext \ -proto ../proto/saju.proto \ -d '{"name": "Daniil", "gender": "M", "birth_date": "1999-01-08T06:00:00Z", "is_solar": true}' \ '127.0.0.1:50051' \ saju.SajuEngine/Register { "success": true, "msg": "gRPC를 통해 성공적으로 등록 완료" }
~/workspace/saju-analysis/saju-engine$ grpcurl -plaintext \ -proto ../proto/saju.proto \ -d '{"name": "Daniil"}' \ '127.0.0.1:50051' \ saju.SajuEngine/GetProfile { "name": "Daniil", "birthDate": "1999-01-08T06:00:00+00:00", "ganjis": [ { "code": 4, "nameKo": "무", "nameHani": "戊", "typeName": "천간", "element": "토", "yinYang": "양" }, { "code": 12, "nameKo": "인", "nameHani": "寅", "typeName": "지지", "element": "목", "yinYang": "양" }, { "code": 1, "nameKo": "을", "nameHani": "乙", "typeName": "천간", "element": "목", "yinYang": "음" }, { "code": 11, "nameKo": "축", "nameHani": "丑", "typeName": "지지", "element": "토", "yinYang": "음" }, { "code": 6, "nameKo": "경", "nameHani": "庚", "typeName": "천간", "element": "금", "yinYang": "양" }, { "code": 18, "nameKo": "신", "nameHani": "申", "typeName": "지지", "element": "금", "yinYang": "양" }, { "code": 5, "nameKo": "기", "nameHani": "己", "typeName": "천간", "element": "토", "yinYang": "음" }, { "code": 13, "nameKo": "묘", "nameHani": "卯", "typeName": "지지", "element": "목", "yinYang": "음" } ], "tenGods": [ "편인", "편재", "정재", "정인", "비견", "비견", "정인", "정재" ], "analysisJson": "{\"element_scores\":{\"earth\":45,\"fire\":0,\"metal\":25,\"water\":0,\"wood\":30},\"interactions\":[{\"category\":\"천간합\",\"interpretation\":\"유연한 을목과 강직한 경금이 만나 서로를 보완하며 강한 결속력과 의리를 발휘합니다.\",\"keywords\":[\"정의\",\"결속\",\"의리\"],\"title\":\"을경합(乙庚合): 인의의 합\"},{\"category\":\"지지방합\",\"interpretation\":\"계절적 동질성을 가진 강력한 목기가 형성되어 거침없는 시작과 추진력을 보여줍니다.\",\"keywords\":[\"생동\",\"추진\",\"동료\"],\"title\":\"인묘진(寅卯辰) 방합: 봄의 세력\"},{\"category\":\"지지육충\",\"interpretation\":\"시작과 결실의 기운이 부딪혀 매우 활동적이며 거주지나 직업의 변동이 잦을 수 있습니다.\",\"keywords\":[\"활동\",\"변화\",\"속도\"],\"title\":\"인신충(寅申沖): 역마의 정면충돌\"},{\"category\":\"지지형\",\"interpretation\":\"강력한 세 기운이 얽혀 시시비비를 가려야 하니, 전문적인 제어 기술이나 리더십이 요구됩니다.\",\"keywords\":[\"조절\",\"수술\",\"기술\"],\"title\":\"인사신(寅巳申) 삼형: 권력의 제어\"}],\"strength\":{\"Strong\":70}}" }
analysisJson을 보기 좋게 정리하면 다음과 같다."analysisJson": "{ \"element_scores\":{ \"earth\":45, \"fire\":0, \"metal\":25, \"water\":0, \"wood\":30 }, \"interactions\":[ { \"category\":\"천간합\", \"interpretation\":\"유연한 을목과 강직한 경금이 만나 서로를 보완하며 강한 결속력과 의리를 발휘합니다.\", \"keywords\":[\"정의\",\"결속\",\"의리\"], \"title\":\"을경합(乙庚合): 인의의 합\" }, { \"category\":\"지지방합\", \"interpretation\":\"계절적 동질성을 가진 강력한 목기가 형성되어 거침없는 시작과 추진력을 보여줍니다.\", \"keywords\":[\"생동\",\"추진\",\"동료\"], \"title\":\"인묘진(寅卯辰) 방합: 봄의 세력\" }, { \"category\":\"지지육충\", \"interpretation\":\"시작과 결실의 기운이 부딪혀 매우 활동적이며 거주지나 직업의 변동이 잦을 수 있습니다.\", \"keywords\":[\"활동\",\"변화\",\"속도\"], \"title\":\"인신충(寅申沖): 역마의 정면충돌\" }, { \"category\":\"지지형\", \"interpretation\":\"강력한 세 기운이 얽혀 시시비비를 가려야 하니, 전문적인 제어 기술이나 리더십이 요구됩니다.\", \"keywords\":[\"조절\",\"수술\",\"기술\"], \"title\":\"인사신(寅巳申) 삼형: 권력의 제어\" } ], \"strength\":{ \"Strong\":70 } }"
대충 살자 샘플 데이터에서 수정 안 해서 성별 잘못 들어간 다냐 데이터처럼