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 구조를 넣어 데이터 정합성을 지켰습니다.