feat(negodata): lps_db 읽기전용 연결 기반 — DBType.LPS + 조건부 엔진 등록

LPS(인터넷 최저가 검색) 연동 1단계. price_history 동기화 배치가 쓸
읽기전용 접속을 기존 확장점(DBType enum + 세션매니저 맵)에 등록한다.

- DBType.LPS=2, LpsDBConfig(read_* 만 — write 엔진 미등록으로 앱 차원
  읽기전용 강제, write 요청 시 KeyError)
- config name 비면 미등록(구버전 toml·LPS 없는 환경에서도 부팅 가능),
  배치는 is_registered() 로 사전 확인
- env override: LPS_DB_HOST/PORT/USER/PASSWORD/NAME (도커용)
- 검증: 엔진 등록·읽기 세션·write 가드 KeyError·lps_db.price_history
  실조회 확인

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
민헌 2026-07-10 15:11:18 +09:00
parent 18f6466a8b
commit 9e9de52200
5 changed files with 63 additions and 2 deletions

View File

@ -9,7 +9,7 @@ from common.database.model.models import MAIN_BASE
from common.enums import DBType, DBWRType, ErrorType from common.enums import DBType, DBWRType, ErrorType
from common.logger import LOG from common.logger import LOG
from common.singleton import Singleton from common.singleton import Singleton
from config.server_configs import main_db_config from config.server_configs import main_db_config, lps_db_config
class DBSessionManager(Singleton): class DBSessionManager(Singleton):
@ -47,6 +47,12 @@ class DBSessionManager(Singleton):
DBType.MAIN.value: self.create_engine(DBType.MAIN.value, DBWRType.DB_READ.value), DBType.MAIN.value: self.create_engine(DBType.MAIN.value, DBWRType.DB_READ.value),
} }
# LPS 결과 DB(lps_db) — **읽기전용**: read 엔진만 등록한다(write 로 열면 KeyError = 앱 차원 가드).
# config 의 name 이 비어 있으면 미사용(스키마 없는 환경에서도 부팅 가능, 배치는 is_registered 로 스킵).
if lps_db_config.name:
self.__db_type_map[DBType.LPS.value] = lps_db_config
self.__read_session[DBType.LPS.value] = self.create_engine(DBType.LPS.value, DBWRType.DB_READ.value)
def create_engine(self, db_type: int, db_wr_type: int): def create_engine(self, db_type: int, db_wr_type: int):
db_config = self.__db_type_map.get(db_type) db_config = self.__db_type_map.get(db_type)
if not db_config: if not db_config:
@ -83,6 +89,10 @@ class DBSessionManager(Singleton):
) )
return scoped_session return scoped_session
def is_registered(self, db_type: int) -> bool:
"""해당 논리 DB 가 등록돼 있는지 — 선택 연동(LPS 등)의 배치가 실행 전 확인하는 용도."""
return db_type in self.__db_type_map
async def dispose_all(self): async def dispose_all(self):
"""모든 엔진의 커넥션 풀을 정리한다. 앱 종료/테스트 종료 시 호출한다. """모든 엔진의 커넥션 풀을 정리한다. 앱 종료/테스트 종료 시 호출한다.
호출하지 않으면 풀 커넥션이 이벤트 루프 종료 후 GC 되며 경고를 남긴다. 호출하지 않으면 풀 커넥션이 이벤트 루프 종료 후 GC 되며 경고를 남긴다.

View File

@ -99,6 +99,7 @@ class DBType(Enum):
""" """
MAIN = 1 MAIN = 1
LPS = 2 # 인터넷 최저가 검색(lps_db) — 읽기전용(price_history 동기화 배치용, write 엔진 미등록)
class DBWRType(Enum): class DBWRType(Enum):

View File

@ -32,6 +32,20 @@ pool_size = 10
max_overflow = 20 max_overflow = 20
sslmode = "" # 로컬: "" / 관리형 DB: "require"|"verify-ca"|"verify-full" sslmode = "" # 로컬: "" / 관리형 DB: "require"|"verify-ca"|"verify-full"
# 인터넷 최저가 검색(LPS) 결과 DB — 읽기전용(price_history 동기화 배치).
# name 을 비우면 LPS 연동 비활성(엔진 미등록·배치 스킵). 도커는 LPS_DB_HOST 등 env 로 override.
[LpsDBConfig]
db_type = "postgresql"
name = "lps_db"
read_host = "127.0.0.1"
read_port = 5432
read_id = "postgres"
read_pw = "postgres"
show_log = false
pool_size = 2
max_overflow = 2
sslmode = ""
[JwtToken] [JwtToken]
access_key = "<JWT_ACCESS_SECRET>" access_key = "<JWT_ACCESS_SECRET>"
refresh_key = "<JWT_REFRESH_SECRET>" refresh_key = "<JWT_REFRESH_SECRET>"

View File

@ -39,6 +39,23 @@ class MainDBConfig(ConfigModel):
sslmode: str = "" sslmode: str = ""
# 인터넷 최저가 검색(LPS)의 결과 DB(lps_db) — **읽기전용** 접속.
# LPS 는 별도 서비스(자체 스키마 소유)이고, negodata 는 price_history 를 주기 배치로 읽어만 온다.
# 그래서 read_* 만 둔다(write 엔진 미등록 = 앱 차원 읽기전용 강제). name 이 비면 미사용(등록 스킵).
class LpsDBConfig(ConfigModel):
db_type: str = "postgresql"
name: str = "" # 비우면 LPS 연동 비활성(엔진 미등록, 배치 스킵)
read_host: str = ""
read_port: int = 5432
read_id: str = ""
read_pw: str = ""
show_log: bool = False
# 배치 전용이라 작은 풀이면 충분 (동시 사용처 = 스케줄러 잡 1개)
pool_size: int = 2
max_overflow: int = 2
sslmode: str = ""
class JwtToken(ConfigModel): class JwtToken(ConfigModel):
access_key: str = "" access_key: str = ""
refresh_key: str = "" refresh_key: str = ""

View File

@ -1,7 +1,7 @@
import os import os
from config.config_loader import Configs from config.config_loader import Configs
from config.config_models import WebServerConfig, LogConfig, MainDBConfig, JwtToken, StorageConfig, MailConfig from config.config_models import WebServerConfig, LogConfig, MainDBConfig, LpsDBConfig, JwtToken, StorageConfig, MailConfig
# 실행 환경 결정 (기본 local). 환경변수 APP_ENV 로 변경. # 실행 환경 결정 (기본 local). 환경변수 APP_ENV 로 변경.
APP_ENV = os.environ.get("APP_ENV", "local") APP_ENV = os.environ.get("APP_ENV", "local")
@ -18,6 +18,8 @@ configs = Configs(_config_file)
web_server_config: WebServerConfig = configs.get(WebServerConfig) web_server_config: WebServerConfig = configs.get(WebServerConfig)
log_config: LogConfig = configs.get(LogConfig) log_config: LogConfig = configs.get(LogConfig)
main_db_config: MainDBConfig = configs.get(MainDBConfig) main_db_config: MainDBConfig = configs.get(MainDBConfig)
# [LpsDBConfig] 섹션이 없는 toml(구버전)에서도 죽지 않게 기본값 폴백(name 빈 값 → LPS 연동 비활성).
lps_db_config: LpsDBConfig = configs.get(LpsDBConfig) or LpsDBConfig()
jwt_token_config: JwtToken = configs.get(JwtToken) jwt_token_config: JwtToken = configs.get(JwtToken)
storage_config: StorageConfig = configs.get(StorageConfig) storage_config: StorageConfig = configs.get(StorageConfig)
# [MailConfig] 섹션이 없는 toml(구버전)에서도 죽지 않도록 기본값으로 폴백(전 필드 빈 값 → 발송 시 EmailUnavailable). # [MailConfig] 섹션이 없는 toml(구버전)에서도 죽지 않도록 기본값으로 폴백(전 필드 빈 값 → 발송 시 EmailUnavailable).
@ -40,3 +42,20 @@ def _apply_db_env_override(cfg: MainDBConfig):
_apply_db_env_override(main_db_config) _apply_db_env_override(main_db_config)
# LPS 결과 DB(읽기전용) env override — 도커에서 host 등만 교체. 로컬은 toml 그대로.
def _apply_lps_db_env_override(cfg: LpsDBConfig):
if os.environ.get("LPS_DB_HOST"):
cfg.read_host = os.environ["LPS_DB_HOST"]
if os.environ.get("LPS_DB_PORT"):
cfg.read_port = int(os.environ["LPS_DB_PORT"])
if os.environ.get("LPS_DB_USER"):
cfg.read_id = os.environ["LPS_DB_USER"]
if os.environ.get("LPS_DB_PASSWORD"):
cfg.read_pw = os.environ["LPS_DB_PASSWORD"]
if os.environ.get("LPS_DB_NAME"):
cfg.name = os.environ["LPS_DB_NAME"]
_apply_lps_db_env_override(lps_db_config)