플랫폼/데이터 엔지니어링 과제 풀이 (구현)
저번 글 3줄요약
1. 이렇게 된 이상 최소한의 설치로 간다
2. 이렇게 된 이상 화합물 DB로 간다
3. 이렇게 된 이상 VScode를 켜자
저번 시간에는 이벤트를 만들기 위한 구상을 했고... 이번에는 그래서 어떻게 구현했는지를 얘기할거다. 도커는... 다음편에 얘기합시다...
제가 문외한이라고 했잖아요? 심지어 도커 컨테이너 만들줄도 모름… 이걸 혼자서 한 건 아니고, 일부는 구글 검색하고 일부는 채찍피티 부려먹었다. 제미나이는 사고모드 안 하면 에미나이 되더라고…
테이블 생성
import sqlite3 # SQLite
conn = sqlite3.connect("events.db")
cursor = conn.cursor()
전에도 얘기했지만 SQLite는 파이썬 깔 때 알아서 따라오는 친구라 설치를 할 필요가 없습니다. 저 conn… 저거 내가 오라클 할 때도 봤던 것 같음… 이게 오라클이요… 리눅스에서 SQLD 할 때는 터미널로 돌렸습니다. 근데 이 터미널이 한글 쓰다가 오타가 나면 아주 괴랄한 문자가 나오기도 하고, 다른 입력기처럼 뭐 위로 가서 수정해야징 이런게 안 돼서 따라가기 힘든 것도 있어서 파이썬이랑 연결해서 했었음…
저거 그리고 MySQL 파이썬에서 돌릴때도 나옵니다.
create_table_query = """
CREATE TABLE IF NOT EXISTS events (
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
search_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
event_type TEXT NOT NULL,
query TEXT,
result_count INTEGER,
compound_id INTEGER,
error_type TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
"""
cursor.execute(create_table_query)
conn.commit()
근데 쟤가 파이썬 설치할 때 따라온거지 엄연히 언어라 다르기때문에 쿼리 막 갖다 쓰면 VScode가 님아 이거 틀렸어염 합니다. 착한 코더 여러분들은 파이썬에서 SQL 쓸 때 쿼리를 따옴표 세 개로 감싸주시길 바랍니다.
엥? 근데 저 NOT EXISTS는 뭐예요? 테스트를 한번 하고 끝이 아니라 디버깅하고 돌리고 하면 여러번 돌릴 수도 있음. 근데 도커 패킹하면서 쟤를 같이 패킹하면서 걍 CREATE 주면 두번째부터는 아이 있는데 왜 또 만들래! 하면서 에러를 퉤잇합니다. 그러니까 표가 '없으면' 만들라는 얘기를 하는거다. 저 밑에 커밋이요? 저거 안 하면 CREATE문 실행만 하고 기껏 만든 테이블이 반영 안 돼서 있었는데 없어질 수 있음.
table_view_query = 'PRAGMA table_info(events);'
cursor.execute(table_view_query)
rows = cursor.fetchall()
for row in rows:
print(row)
뭐야 셀렉트문 어디감? 저 테이블은 테이블만 있고 인스턴스들이 없어서 셀렉트문 써도 암것도 안 나옵니다. 근데 테이블이 만들어졌으면 정보는 어쨌든 있을 거 아니예요? 그러니까 정보 주십쇼 하는거다.
이벤트 생성
def __init__(self):
self.conn = conn
self.cursor = cursor
self.search_id = 0 # 일단은 0
# 여기 있는 분자들은 약으로 쓰고 있는 분자들입니다. (진세노사이드는 인삼에 들어있는 사포닌)
# 오타는 일부러 넣은겁니다. 대신 오타의 경우 후속 이벤트도 없고, 검색 결과 수도 0입니다.
# artemisinin: 오타입니다. artemisinin이 맞아요.
# nafroxen: 얘도 오타입니다. naproxen이 맞아요.
# acetaminopen: acetaminophen이 맞습니다.
# gentamycin: gentamicin이 맞습니다.
self.compounds = ["aspirin","ibuprofen","acetaminopen","acetaminophen","arteminisin","artemisinin","imatinib","nafroxen","ginsenoside","salicylic acid","naproxen","fluoxetine","thalidomide","duloxetine","gentamycin","gentamicin","rifampicin"]
self.compound_results = { # 검색 결과 고정해야 합니다. 똑같은 검색어로 똑같은 DB 뒤져서 누구는 100건 누구는 200건 이건 에바입니다.
"aspirin": 120,
"ibuprofen": 85,
"acetaminophen": 143,
"artemisinin": 67,
"imatinib": 41,
"ginsenoside": 23,
"salicylic acid": 98,
"naproxen": 74,
"fluoxetine": 56,
"thalidomide": 12,
"duloxetine": 37,
"gentamicin": 29,
"rifampicin": 44
}
self.typo_corrections = { # 룰 7. 오타의 경우 검색 결과가 0개/룰 8. 오타때문에 결과가 0이 나왔다면, 그 사용자는 제대로 된 철자를 사용해서 다시 검색한다. (하다가 추가됨)
"acetaminopen": "acetaminophen",
"arteminisin": "artemisinin",
"nafroxen": "naproxen",
"gentamycin": "gentamicin"
}
self.current_search_id = 1
전체 코드 올려드리고 싶은데 이거 전체 코드 올리면 5000자 넘어가서 네이버에서는 짤립니다...
일단 저 오타는 일부러 넣은거고… 쟤들 다 뭐냐고? 일단 다들 알법한 아세트아미노펜, 이부프로펜, 아스피린은 생략하고… 아르테미시닌은 개똥쑥(풀때기 이름이 그럼)에 들어있는 성분인데 말라리아 잡는 데 쓰는 성분이고, 이마티닙은 글리벡이라고 하면 어? 그거? 하는 분들도 있을거다. 나프록센은 진통제인데 NSAID 계열이고, 플루옥세틴(=프로작)은 항우울제고, 탈리도마이드는 예전에 입덧에 직방이라고 해서 먹었는데 이성질체가 혈관 생성을 억제하는 효과가 있어서 기형아를 유발했던 약이다. 지금은 이 성질을 이용해서 제한적으로 항암제로 쓰는 중이다. 둘록세틴(=심발타)은 항우울제고, 젠타마이신이랑 리팜피신은 항생제. 진세노사이드는 사포닌 중에서도 특히 인삼에 들어있는 사포닌을 이르는 말이고(인삼 들어간거 광고할때 많이 나옴), 살리실산… 아스피린이랑 다릅니다. 아스피린은 '아세틸'살리실산이고 살리실산은 식물 호르몬임. 각질 없애는데 들어갑니다.
저게 사실은 그냥 코드 쭉 쓰면서 했다가 OOP 서타일로 클래스 안에 함수 넣어서 걍 이벤트 찍어내게 만든거다. 이렇게 하면 뭐 문제 생겼을 때 클래스만 건들면 걔가 찍어내는 객체가 바뀌잖아요.
# 시간 랜덤 생성기
def random_timestamp(self):
start_date = datetime(2026, 5, 1)
end_date = datetime(2026, 5, 8)
delta = end_date - start_date
random_seconds = random.randint(0, int(delta.total_seconds()))
random_time = start_date + timedelta(seconds=random_seconds)
return random_time.strftime("%Y-%m-%d %H:%M:%S")
이건 이벤트 시간 랜덤 생성기인데, 문제가 하나 있다. 전에 룰 얘기하면서 모든 이벤트는 검색이 선행되고 그 다음에 후행으로 진행된다고 했잖아요? 근데 시간을 랜덤으로 짜면서 거기에 대한 처리를 안 했다... ㅋㅋㅋㅋㅋㅋ 순서는 후행인데 시간이 먼저 나올 수도 있음... 하...
# 이벤트 생성기
def insert_event(self,search_id,user_id,event_type,query=None,result_count=None,compound_id=None,error_type=None):
event = {
"search_id": f"{search_id:06d}",
"user_id": f"{user_id:06d}",
"event_type": event_type,
"query": query,
"result_count": result_count,
"compound_id": (
f"{compound_id:06d}"
if compound_id is not None
else None
),
"error_type": error_type,
"timestamp": self.random_timestamp()
}
# SQL에 넣으려면 INSERT INTO가 필요합니다.
insert_query = """
INSERT INTO events (
search_id,
user_id,
event_type,
query,
result_count,
compound_id,
error_type,
timestamp
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
"""
self.cursor.execute(
insert_query,
(
event["search_id"],
event["user_id"],
event["event_type"],
event["query"],
event["result_count"],
event["compound_id"],
event["error_type"],
event["timestamp"]
)
)
# 지금은 과제니까 하나하나 커밋하는거지만, 착한 엔지니어 여러분들은 batch commit을 써주세요
# 커밋 한땀한땀 하는것도 다 시간 걸려요.
self.conn.commit()
이게 이벤트 생성하는 애다. 밑에 매개변수까지 만드는 애는 따로 있고 얘는 매개변수 입력받아서 이벤트 찍어내는 애라고 보시면 됩니다. 그 INSERT문 밑에 또 커밋이 있죠? 이거 원래 이렇게 하면 안됩니다... 지금이야 뭐 한 만개 나오고 땡이지만 실제로는 한 백만개 천만개 쌓일수도 있는데 이걸 일일이 커밋한다?

상사한테 이 소리 들을 수 있음.
# 검색에서 후행 이벤트 파생시킬 애
def generate_search_flow(self):
user_id = random.randint(1, 999999) # 사용자 아이디: 랜덤입니다. 마치 포켓몬 게임의 트레이너 아이디같은 것... (랜덤임)
query = random.choice(self.compounds) # 사용자의 검색어
search_id = self.current_search_id # 검색 아이디
self.current_search_id += 1
if query in self.typo_corrections: # 오타났으면
result_count = 0 # 결과 0개 (룰 7에 의거함)
else:
result_count = self.compound_results[query] # 아니면 개수 출력하시고
event = self.insert_event(search_id,user_id,"search",query=query,result_count=result_count) # 이벤트 생성
events = [event]
if result_count > 0:
self.generate_followup_events(events, search_id, user_id, query, result_count) # 후행 이벤트 따로 뺐습니다
else:
result_view_event = self.insert_event(search_id, user_id, "result_view", query=query, result_count=0)
events.append(result_view_event)
# 룰 8. 오타때문에 결과가 0이 나왔다면, 그 사용자는 제대로 된 철자를 사용해서 다시 검색한다.
if query in self.typo_corrections:
corrected_query = self.typo_corrections[query] # 오타때문에 결과가 안 떴으니 철자를 교정해서 재검색을 해줍니다
corrected_search_id = self.current_search_id
self.current_search_id += 1 # 사용자는 같지만 검색은 다시 하는거라서 아이디는 다름
corrected_result_count = self.compound_results[corrected_query] # 철자 정정했으니까 다시 결과가 나오겠죠?
corrected_event = self.insert_event(corrected_search_id, user_id, "search", query=corrected_query, result_count=corrected_result_count) # 그럼 다시 생성해야겠죠?
events.append(corrected_event)
self.generate_followup_events(events, corrected_search_id, user_id, corrected_query, corrected_result_count)
return events
주석이 잘못됐는데 얘가 모든 이벤트의 시발점같은 애임. 욕 아님.
주석에 포켓몬 아이디는 뭔 소리냐고요? 포켓몬 게임에 보면 트레이너 아이디라는 게 있습니다.

이 이미지에서 IDNo. 라고 쓰여있는 게 트레이너의 아이디인데, 구세대는 5자리였고(쟤는 리프그린이라 5자리) 6세댄가 7세대부터 6자리로 늘어났어요. 저기서 세이브파일을 날리고 다시 처음부터 시작하면 저 아이디 바뀝니다. 저게 랜덤이라 저 아이디 번호 12345나 23456같은 걸로 맞추려고 리셋하는 사람도 있어요.
근데 나는 검색 아이디도 난수로 하려고 했는데 채찍피티가 순차적으로 생성하게 만들었네... if query in self.typo_corrections 밑에 있는 건 오타났으니 정해서 찾는거다. gentamycin으로 찾았으면 0일거고, 아 철자 틀렸구나 하고 gentamicin으로 다시 검색한다는 얘기.
# 원래 한 덩어리였던 후행 이벤트를 따로 뺐습니다.
def generate_followup_events(self, events, search_id, user_id, query, result_count):
if random.random() < 0.95: # 5% 확률로 에러가 당신을 반깁니다
# 검색 결과 조회
result_view_event = self.insert_event(search_id, user_id, "result_view", query=query, result_count=result_count)
events.append(result_view_event)
# 검색 결과에서 개별 화합물 결과를 보겠죠?
chemical_view_count = random.randint(1, min(result_count, 5))
for _ in range(chemical_view_count):
# 하지만 1번만 보라는 법은 없죠?
compound_id = random.randint(1, result_count)
chemical_event = self.insert_event(search_id, user_id, "chemical_view", query=query, compound_id=compound_id)
events.append(chemical_event)
# 다운로드(CSV로)->다운로드는 한번만 합니다.
if random.random() < 0.7: # 실패할 확률 있음(30%)
csv_event = self.insert_event(search_id, user_id, "csv_download", query=query)
events.append(csv_event)
else:
error_event = self.insert_event(search_id, user_id, "error", error_type="csv_download_failed")
events.append(error_event)
else:
error_event = self.insert_event(search_id, user_id, "error", error_type="result_page_timeout")
events.append(error_event)
원래 한 덩어리였는데 왜 뺐냐를 먼저 말씀드리겠음... 이벤트 순서상 검색이 시발점이고 그 뒤에 후행 이벤트가 온다고 했는데, 이게 검색어 오타나서 정정하고 다시 검색한 검색어에도 적용이 돼야 합니다. 근데 안되는거야. 되게 하려면 저 코드부분을 if 오타 고 투 정정 밑에 또 넣으라는데 이렇게 되면 문제가 뭐게요? 100줄이 넘는 코드에서 위쪽 수정하면 아래쪽도 찾아서 수정해야됩니다. 그리고 내가 제일 싫어하는게 쓸데없이 복잡한거거든...
어쨌든 검색 이벤트에서 분기가 있다뿐이지 후행 이벤트는 같으니까 걍 저거 밖으로 빼서 덩어리 만들고 검색 이벤트에서 그 덩어리를 연결한거다. 그러면 저 덩어리만 수정하면 되니까 편하죠. 코드 줄 수가 줄어드는 건 아니겠지만.
if random.random() < 0.7… 에러 이벤트가 확률빨입니다…
'Coding > Python' 카테고리의 다른 글
| Faker (0) | 2026.06.12 |
|---|---|
| 플랫폼/데이터 엔지니어링 과제 풀이 (도커가 뭐길래) (0) | 2026.05.11 |
| 플랫폼/데이터 엔지니어링 과제 풀이 (Prologue-문제 그리고 구상) (0) | 2026.05.09 |
| Polars 데이터프레임도 시각화가 되나요? (0) | 2026.04.10 |
| Polars를 써보자 (0) | 2026.04.09 |