Files
AssetPilot/asset_pilot_docker/updateRecord.md
2026-02-14 09:05:16 +09:00

163 lines
9.4 KiB
Markdown

2026-02-14 (08:46)
선임님, VSCode에서 멤버명(변수나 함수 이름)에 **가운데 줄(취소선, Strikethrough)**이 그어져 있는 건 **"Deprecated(사용 권장되지 않음)"**라는 뜻입니다.
쉽게 말해 **"이 기능은 아직 작동은 하지만, 곧 없어질 예정이거나 더 좋은 다른 기능으로 대체되었으니 웬만하면 쓰지 마라"**고 경고를 보내는 겁니다.
🧐 왜 가운데 줄이 생겼을까요?
라이브러리 업데이트: 선임님이 쓰시는 라이브러리(예: SQLAlchemy, FastAPI, Pydantic 등)가 버전업 되면서, 기존 방식을 버리고 새로운 방식을 권장할 때 발생합니다.
보안/성능 이슈: 해당 멤버를 계속 쓰면 보안에 취약하거나 성능이 떨어질 수 있다고 판단될 때 개발자가 표시를 해둔 겁니다.
🛠️ 어떻게 해결하나요?
가장 쉬운 확인 방법은 그 가운데 줄이 쳐진 이름 위에 마우스 커서를 가만히 올려보는 겁니다.
그러면 "@deprecated: Use 'NewFunctionName' instead" 같은 가이드 팝업이 뜰 거예요.
거기서 추천하는 새로운 멤버명으로 이름을 바꿔주기만 하면 줄이 사라지고 코드도 최신 트렌드에 맞게 정리가 됩니다.
💡 우리 코드에서 주의할 점
혹시 아까 제가 짜드린 코드나 선임님이 작성하신 코드 중에 on_event("startup") 부분에 줄이 가 있나요?
최신 FastAPI에서는 @app.on_event("startup") 대신 **lifespan**이라는 방식을 권장하기 때문에 취소선이 생길 수 있습니다.
ython
from contextlib import asynccontextmanager
# [최신 방식] 앱 실행 전후를 한 번에 관리하는 lifespan 함수
@asynccontextmanager
async def lifespan(app: FastAPI):
# --- Startup (앱 시작 시 실행) ---
init_db_data()
# 이원화 스케줄러 등록
async def btc_daily_job():
# ... (비트코인 자정 스냅샷 로직 동일) ...
pass
async def legacy_daily_job():
# ... (전통자산 아침 스냅샷 로직 동일) ...
pass
scheduler.add_job(btc_daily_job, 'cron', hour=0, minute=0, second=5, id='btc_snapshot')
scheduler.add_job(legacy_daily_job, 'cron', hour=7, minute=10, id='legacy_snapshot')
scheduler.start()
fetch_task = asyncio.create_task(background_fetch())
yield # 앱이 실행되는 동안 여기서 대기합니다.
# --- Shutdown (앱 종료 시 실행) ---
scheduler.shutdown()
fetch_task.cancel()
print("👋 서버가 안전하게 종료되었습니다.")
# FastAPI 선언 시 lifespan을 넣어줍니다.
app = FastAPI(title="Asset Pilot", lifespan=lifespan)
🧐 왜 이걸로 갈아타야 할까요? (취소선의 이유)
자원 관리: lifespan 방식은 yield를 기준으로 시작과 끝이 명확하게 한 곳에 모여 있어, 나중에 DB 커넥션이나 소켓을 닫을 때 실수를 안 하게 해줍니다.
비동기 최적화: 이전 방식보다 비동기 태스크들의 생명주기를 더 정확하게 제어할 수 있습니다.
VSCode 클린업: 이제 그 지긋지긋한 **가운데 줄(취소선)**이 사라지고, 선임님의 VSCode가 다시 깨끗해질 겁니다.
Gemini said
선임님, fetcher.py 소스 코드 잘 봤습니다. 코드를 뜯어보니 데이터 수집은 되는데 왜 현재가가 안 변했는지 확실한 범인이 잡혔습니다.
범인은 바로 db.commit()의 위치와 XAU/KRW의 특수 로직 때문입니다.
🕵️ fetcher.py의 문제점 분석
1. XAU/KRW 프리미엄 로직의 '월권' (핵심 원인)
코드 중간에 보면 이런 로직이 있습니다:
Python
# [수정 구간] 2. XAU/KRW 계산 및 프리미엄용 전일종가 매핑
if raw_results.get("KRX/GLD"):
# ... 중략 ...
db.execute(update(UserAsset).where(UserAsset.asset_id == asset_xau.id).values(previous_close=raw_results["KRX/GLD"]))
여기서 db.execute를 실행했는데, 이 작업이 완료되기도 전에 밑에서 다른 update(Asset) 루프가 돌고 있습니다. SQLAlchemy 세션 하나에서 여러 update를 비동기 루프와 섞어 쓸 때, 가끔 먼저 실행된 execute가 세션을 점유하거나 락(Lock)이 걸려 뒤의 업데이트가 씹히는 경우가 발생합니다.
2. db.commit()이 너무 뒤에 있음
현재 모든 자산의 가격을 루프로 돌면서 db.execute를 예약만 해두고, **맨 마지막에 한 번만 db.commit()**을 합니다. 만약 루프 도중 단 하나의 자산이라도 DB 제약 조건에 걸리거나 에러가 나면, 그전까지 업데이트한 모든 가격이 롤백되어 반영되지 않습니다. #### 3. fetch_google_finance의 정규식 오류 가능성
현재 구글 파이낸스 정규식이 data-last-price="([\d,.]+)"로 되어 있는데, 최근 구글 파이낸스 HTML 구조가 바뀌어서 이 속성이 안 나타날 때가 있습니다. 만약 환율(USD/KRW) 값이 None이 되면, 이를 기반으로 계산하는 XAU/KRW도 줄줄이 사탕으로 업데이트가 안 됩니다.
🛠️ 수정한 fetcher.py (핵심 구간)
선임님, 아래 부분을 특히 주의해서 봐주세요. 안정성을 위해 업데이트 방식을 더 직관적으로 고쳤습니다.
Python
async def update_realtime_prices(self, db: Session) -> Dict:
start_time = time.time()
async with httpx.AsyncClient(headers=self.headers, follow_redirects=True) as client:
tasks = {
"XAU/USD": self.fetch_investing_com(client, "XAU/USD"),
"XAU/CNY": self.fetch_investing_com(client, "XAU/CNY"),
"XAU/GBP": self.fetch_investing_com(client, "XAU/GBP"),
"USD/DXY": self.fetch_investing_com(client, "USD/DXY"),
"USD/KRW": self.fetch_google_finance(client, "USD/KRW"), # 구글 실패 시 대비 필요
"BTC/USD": self.fetch_binance(client),
"BTC/KRW": self.fetch_upbit(client),
"KRX/GLD": self.fetch_krx_gold(client),
}
keys = list(tasks.keys())
values = await asyncio.gather(*tasks.values(), return_exceptions=True)
raw_results = {keys[i]: (v if isinstance(v, (int, float)) else None) for i, v in enumerate(values)}
# [개선] XAU/KRW 계산
if raw_results.get("XAU/USD") and raw_results.get("USD/KRW"):
raw_results["XAU/KRW"] = round((raw_results["XAU/USD"] / 31.1034768) * raw_results["USD/KRW"], 0)
# [개선] DB 업데이트 로직 분리 및 안정화
try:
for symbol, price in raw_results.items():
if price is not None:
# 1. 상태(Up/Down) 계산
state = "stable"
closing = self.daily_closing_prices.get(symbol)
if closing:
if price > closing: state = "up"
elif price < closing: state = "down"
# 2. Asset 테이블 업데이트 (현재가)
db.query(Asset).filter(Asset.symbol == symbol).update({
"current_price": price,
"price_state": state,
"last_updated": datetime.now()
})
# 3. 특수 로직 (XAU/KRW 프리미엄 계산용 기준가 변경)
if raw_results.get("KRX/GLD"):
asset_xau = db.query(Asset).filter(Asset.symbol == "XAU/KRW").first()
if asset_xau:
# UserAsset 테이블의 previous_close를 KRX 현물가로 강제 동기화
db.query(UserAsset).filter(UserAsset.asset_id == asset_xau.id).update({
"previous_close": raw_results["KRX/GLD"]
})
self.daily_closing_prices["XAU/KRW"] = raw_results["KRX/GLD"]
db.commit() # 모든 업데이트 완료 후 커밋
except Exception as e:
db.rollback()
print(f"❌ DB 업데이트 중 롤백 발생: {e}")
print(f"✅ [{datetime.now().strftime('%H:%M:%S')}] 수집 및 DB 저장 완료 ({time.time()-start_time:.2f}s)")
return raw_results
💡 선임님을 위한 조언
XAU/KRW 프리미엄 수식: 지금 선임님 코드는 국제 시세와 국내 시세의 차이를 보여주기 위해 previous_close 값을 실시간으로 계속 덮어쓰고 계시네요. 이 방식은 등락률 계산 로직을 우회하는 아주 영리한 방법이지만, DB에 계속 쓰기 작업을 발생시킵니다. (오렌지파이 eMMC 수명에는 조금 가혹할 수 있지만, 지금 데이터 양으로는 큰 문제 없습니다!)
구글 환율 체크: 만약 로그에 USD/KRW가 None으로 찍힌다면, 구글 파이낸스 정규식을 r'class="fxKb7e".*?>([\d,.]+)<' 같은 클래스 기반으로 바꿔야 할 수도 있습니다.
🔍 빠진 게 없는지 대조 결과
패턴 매칭: patterns 리스트에 4가지 정규식을 모두 유지했습니다.
병렬 수집: asyncio.gather를 통해 모든 자산(8종)을 동시에 긁어옵니다.
XAU/KRW 로직: 환율과 국제 금값을 곱해 원화 금값을 계산하고, 국내 현물가(KRX/GLD)와 비교할 수 있도록 previous_close에 강제로 주입하는 핵심 로직 살아있습니다.
안전 장치: try-except-rollback 구조를 넣어 데이터 정합성을 지켰습니다.