왜 엑셀만으로는 부족해지는가
엑셀은 업무 자동화를 시작하기에 좋은 도구입니다. 파일을 열어 데이터를 확인하고, 필요한 열을 정리하고, 보고서를 만들기 쉽습니다. 하지만 데이터가 많아지고 여러 사람이 동시에 다루기 시작하면 문제가 생깁니다.
같은 고객 정보가 여러 파일에 중복 저장됩니다. 누군가 오래된 파일을 기준으로 보고서를 만들기도 합니다. 주문 데이터는 최신인데 재고 파일은 전날 기준일 수 있습니다. 파일 이름에 날짜가 붙어 있어도 실제 내용이 언제 기준인지 확실하지 않은 경우도 많습니다.
이때 필요한 것이 데이터베이스입니다. 데이터베이스는 업무 데이터를 정해진 구조에 맞게 저장하고, 필요한 조건으로 조회하고, 여러 데이터 사이의 관계를 관리하는 시스템입니다. 자동화의 품질은 결국 데이터를 얼마나 안정적으로 가져오고 저장하느냐에 달려 있습니다. 파이썬 스크립트가 아무리 잘 작성되어 있어도 원본 데이터가 중복되거나 누락되어 있으면 결과도 신뢰하기 어렵습니다.
데이터베이스를 연결한다는 것은 단순히 엑셀 대신 다른 저장소를 쓰는 일이 아닙니다. 업무 데이터의 기준점을 만드는 일입니다. 고객, 주문, 재고, 매출 같은 핵심 데이터를 한곳에서 읽고, 필요한 결과를 다시 안전하게 저장하는 흐름을 만드는 것입니다.
AI 생성 이미지
SQL은 데이터베이스와 대화하는 언어다
SQL은 데이터베이스에 질문하고 지시하는 언어입니다. 처음에는 문법이 낯설지만, 업무 자동화에서 자주 쓰는 구조는 많지 않습니다. 핵심은 SELECT, FROM, WHERE, GROUP BY, ORDER BY입니다.
SELECT customer_name, order_date, amount
FROM orders
WHERE order_date >= '2026-05-01'
ORDER BY order_date;이 문장은 orders라는 주문 테이블에서 2026년 5월 1일 이후 주문만 가져오고, 날짜순으로 정렬하라는 뜻입니다.
집계도 자주 사용합니다.
SELECT customer_id, SUM(amount) AS total_amount
FROM orders
WHERE order_date BETWEEN '2026-05-01' AND '2026-05-31'
GROUP BY customer_id;이 문장은 5월 주문을 고객별로 합산합니다. 엑셀의 피벗 테이블과 비슷한 역할을 SQL로 수행한다고 보면 됩니다.
여러 테이블을 연결할 때는 JOIN을 사용합니다.
SELECT
c.customer_name,
o.order_date,
o.amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2026-05-01';주문 테이블에는 고객 ID만 있고, 고객명은 고객 테이블에 따로 있을 수 있습니다. JOIN은 이 두 테이블을 고객 ID 기준으로 연결합니다. 실무에서는 고객, 주문, 상품, 재고, 매출 데이터가 분리되어 있는 경우가 많기 때문에 JOIN은 매우 자주 사용됩니다.
파이썬에서 데이터베이스에 접속하는 기본 흐름
파이썬은 데이터베이스에서 데이터를 읽고, pandas로 가공하고, 결과를 다시 저장하는 중간 작업자 역할을 합니다. 기본 흐름은 『엑셀과 파일 업무 자동화하기』에서 다룬 엑셀 자동화와 크게 다르지 않습니다.
접속 정보 확인
데이터베이스 연결
SQL 실행
결과를 DataFrame으로 변환
검증과 가공
결과 저장
로그 기록과 예외 처리
처음 연습할 때는 별도 서버가 필요 없는 SQLite를 사용하면 좋습니다. SQLite는 하나의 파일을 데이터베이스처럼 사용하는 방식이라 실습과 작은 자동화에 적합합니다. 실제 회사 시스템에서는 PostgreSQL, MySQL, SQL Server, Oracle 같은 데이터베이스를 사용할 수 있지만, 파이썬 코드의 기본 구조는 비슷합니다.
from pathlib import Path
import sqlite3
import pandas as pd
DB_PATH = Path("data/business.db")
query = """
SELECT
c.customer_name,
o.order_date,
o.amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date BETWEEN ? AND ?
ORDER BY o.order_date;
"""
start_date = "2026-05-01"
end_date = "2026-05-31"
with sqlite3.connect(DB_PATH) as conn:
df = pd.read_sql_query(query, conn, params=(start_date, end_date))
print(df.head())여기서 중요한 점은 날짜 값을 SQL 문자열에 직접 붙이지 않고 params로 전달했다는 것입니다. 이렇게 하면 값과 SQL 문장을 분리할 수 있어 실수와 보안 위험을 줄일 수 있습니다. 사용자가 입력한 값을 그대로 SQL에 이어 붙이는 방식은 피해야 합니다.
실무 데이터베이스에 접속할 때는 아이디, 비밀번호, 호스트 주소가 필요합니다. 이 정보는 코드에 직접 쓰지 않습니다. 『파이썬 자동화를 위한 기본 작업 환경 만들기』에서 다룬 것처럼 환경 변수나 .env 파일로 분리합니다.
import os
from sqlalchemy import create_engine
from sqlalchemy.engine import URL
def get_required_env(name):
value = os.getenv(name)
if not value:
raise RuntimeError(f"{name} 환경 변수가 없습니다.")
return value
url = URL.create(
drivername="postgresql+psycopg",
username=get_required_env("DB_USER"),
password=get_required_env("DB_PASSWORD"),
host=get_required_env("DB_HOST"),
database=get_required_env("DB_NAME"),
)
engine = create_engine(url)비밀번호에 @, :, / 같은 특수문자가 들어 있으면 접속 문자열을 직접 이어 붙였을 때 뜻하지 않게 해석될 수 있습니다. 위 예시처럼 URL.create()를 사용하면 이런 문제를 줄일 수 있습니다. 회사 데이터베이스는 민감한 고객 정보와 매출 정보를 담고 있을 가능성이 큽니다. 자동화 스크립트의 편의를 위해 접속 정보를 코드에 남기면 나중에 저장소 공유, 메신저 전달, 로그 출력 과정에서 정보가 노출될 수 있습니다. 접속 정보는 항상 코드 밖에서 관리해야 합니다.
고객, 주문, 재고, 매출 조회 자동화 예시
데이터베이스 자동화의 대표적인 예는 정기 조회입니다. 매일 아침 전날 주문과 재고 부족 상품을 확인하고, 월말에는 고객별 매출을 집계하는 식입니다.
예를 들어 다음과 같은 테이블이 있다고 가정하겠습니다.
customers: 고객 정보orders: 주문 정보products: 상품 정보inventory: 재고 정보
전날 주문 현황을 고객과 상품 기준으로 조회하는 SQL은 다음과 같이 작성할 수 있습니다.
import sqlite3
import pandas as pd
from pathlib import Path
from datetime import date, timedelta
DB_PATH = Path("data/business.db")
output_dir = Path("output")
output_dir.mkdir(parents=True, exist_ok=True)
yesterday = (date.today() - timedelta(days=1)).isoformat()
query = """
SELECT
o.order_id,
o.order_date,
c.customer_name,
p.product_name,
o.quantity,
o.amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN products p ON o.product_id = p.product_id
WHERE o.order_date = ?
ORDER BY o.amount DESC;
"""
with sqlite3.connect(DB_PATH) as conn:
orders_df = pd.read_sql_query(query, conn, params=(yesterday,))
output_path = output_dir / f"orders_{yesterday}.xlsx"
orders_df.to_excel(output_path, index=False)이 코드는 전날 주문을 조회해 엑셀 파일로 저장합니다. 엑셀 보고서를 직접 만드는 방식과 다르게, 원본 데이터는 데이터베이스에 있고 엑셀은 결과물입니다. 이 차이가 중요합니다. 엑셀 파일을 기준 데이터로 삼으면 어느 파일이 최신인지 계속 확인해야 합니다. 반대로 데이터베이스를 기준으로 삼으면 자동화 스크립트는 필요한 시점에 최신 데이터를 조회해 결과만 만들면 됩니다.
재고 부족 상품을 찾는 작업도 자동화하기 좋습니다.
query = """
SELECT
p.product_id,
p.product_name,
i.stock_qty,
i.safety_stock
FROM inventory i
JOIN products p ON i.product_id = p.product_id
WHERE i.stock_qty < i.safety_stock
ORDER BY i.stock_qty ASC;
"""
with sqlite3.connect(DB_PATH) as conn:
low_stock_df = pd.read_sql_query(query, conn)
if not low_stock_df.empty:
output_dir = Path("output")
output_dir.mkdir(parents=True, exist_ok=True)
low_stock_df.to_excel(output_dir / "low_stock.xlsx", index=False)이런 업무는 규칙이 명확합니다. 현재 재고가 안전 재고보다 적으면 목록에 포함하면 됩니다. 이 경우 AI가 필요하지 않습니다. AI를 사용하면 오히려 결과의 일관성을 떨어뜨릴 수 있습니다. 숫자 비교, 날짜 필터링, 중복 확인, 합계 계산은 SQL과 파이썬으로 처리하는 것이 적합합니다.
반면 재고 부족 사유를 담당자 메모에서 요약하거나, 고객별 주문 감소 원인을 자유 형식 문서에서 찾는 일은 AI가 도움을 줄 수 있습니다. 즉, 데이터 조회와 계산은 규칙 기반으로 처리하고, 비정형 텍스트 해석은 AI를 보조적으로 사용하는 것이 안전합니다.
엑셀과 데이터베이스 사이의 동기화 설계
현실의 업무는 한 번에 데이터베이스 중심으로 바뀌지 않습니다. 여전히 엑셀로 받은 거래처 목록, 영업팀이 작성한 주문 수정 파일, 물류팀이 관리하는 재고 조정 파일이 존재합니다. 따라서 엑셀과 데이터베이스 사이의 동기화 작업이 필요합니다.
동기화에서 가장 먼저 정해야 할 것은 기준 데이터입니다.
데이터베이스가 기준이고 엑셀은 입력 양식인가?
엑셀이 기준이고 데이터베이스는 결과 저장소인가?
양쪽 모두 수정될 수 있는가?
가장 단순하고 안전한 방식은 데이터베이스를 기준으로 두고, 엑셀은 임시 입력 양식으로 사용하는 것입니다. 사용자가 엑셀에 신규 고객이나 수정 요청을 입력하면 파이썬이 파일을 읽고 검증한 뒤 데이터베이스에 반영합니다.
이때 바로 운영 테이블에 넣기보다 중간 테이블을 사용하는 것이 좋습니다. 이를 스테이징 테이블이라고 합니다. 스테이징 테이블은 임시 검수 공간입니다.
from pathlib import Path
import sqlite3
import pandas as pd
DB_PATH = Path("data/business.db")
excel_path = Path("data/incoming/new_customers.xlsx")
required_columns = ["customer_id", "customer_name", "region"]
incoming = pd.read_excel(excel_path)
incoming.columns = [str(col).strip().lower() for col in incoming.columns]
missing = set(required_columns) - set(incoming.columns)
if missing:
raise ValueError(f"필수 열이 없습니다: {sorted(missing)}")
incoming = incoming[required_columns].dropna(subset=["customer_id", "customer_name"])
incoming["customer_id"] = incoming["customer_id"].astype(str).str.strip()
incoming["customer_name"] = incoming["customer_name"].astype(str).str.strip()
with sqlite3.connect(DB_PATH) as conn:
incoming.to_sql("stg_customers", conn, if_exists="replace", index=False)여기까지는 엑셀을 읽어 필수 열을 확인하고, 빈 고객 ID와 고객명을 제거한 뒤 스테이징 테이블에 저장한 것입니다. 아직 운영 테이블에는 반영하지 않았습니다.
다음 단계에서 중복되지 않은 고객만 운영 테이블에 추가합니다.
insert_sql = """
INSERT INTO customers (customer_id, customer_name, region)
SELECT
s.customer_id,
s.customer_name,
s.region
FROM stg_customers s
WHERE NOT EXISTS (
SELECT 1
FROM customers c
WHERE c.customer_id = s.customer_id
);
"""
with sqlite3.connect(DB_PATH) as conn:
conn.execute("PRAGMA foreign_keys = ON")
result = conn.execute(insert_sql)
print(f"추가된 고객 수: {result.rowcount}")핵심은 customer_id를 기준으로 중복을 막는 것입니다. 이름은 바뀔 수 있고, 같은 이름의 고객이 있을 수도 있습니다. 동기화에는 반드시 고유한 키가 필요합니다. 고객은 고객 ID, 주문은 주문 ID, 상품은 상품 ID처럼 행을 식별할 수 있는 값을 정해야 합니다.
수정 작업은 더 조심해야 합니다. 예를 들어 엑셀에 있는 고객명이 기존 데이터베이스의 고객명과 다르면 자동으로 덮어쓸 것인지, 변경 후보 목록으로 저장한 뒤 담당자 승인을 받을 것인지 결정해야 합니다. 고객명, 주소, 사업자번호, 결제 조건처럼 업무 영향이 큰 데이터는 사람의 승인 절차를 두는 편이 안전합니다.
데이터 무결성을 지키는 방법
데이터 무결성은 데이터가 정확하고 일관된 상태를 유지한다는 뜻입니다. 자동화에서는 속도보다 무결성이 먼저입니다. 잘못된 데이터를 빠르게 많이 넣는 자동화는 업무를 돕는 것이 아니라 문제를 키웁니다.
데이터베이스에서는 다음 제약 조건을 활용해 무결성을 지킬 수 있습니다.
기본 키: 각 행을 고유하게 식별하는 값
고유 제약: 같은 값이 중복 저장되지 않도록 막는 규칙
외래 키: 주문의 고객 ID가 실제 고객 테이블에 존재하는지 확인하는 규칙
NOT NULL: 반드시 값이 있어야 하는 열에 빈값을 막는 규칙
CHECK: 수량이나 금액이 음수가 되지 않도록 제한하는 규칙
예를 들어 주문 테이블은 다음과 같은 규칙을 가질 수 있습니다.
CREATE TABLE orders (
order_id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
product_id TEXT NOT NULL,
order_date TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
amount REAL NOT NULL CHECK (amount >= 0),
FOREIGN KEY (customer_id) REFERENCES customers(customer_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);이 구조에서는 주문 수량이 0보다 작거나 같으면 저장할 수 없습니다. 존재하지 않는 고객 ID로 주문을 저장하는 것도 막을 수 있습니다. 다만 SQLite는 연결할 때마다 외래 키 검사를 켜야 하므로, 쓰기 작업을 할 때는 예시처럼 PRAGMA foreign_keys = ON을 실행하는 습관을 들이는 것이 좋습니다. PostgreSQL이나 MySQL 같은 서버형 데이터베이스에서는 설정 방식이 다르지만, 핵심은 데이터베이스 자체가 마지막 방어선 역할을 하게 만드는 것입니다.
이런 규칙은 파이썬 코드에서도 검사할 수 있지만, 데이터베이스에도 함께 두는 것이 좋습니다. 자동화 스크립트가 여러 개로 늘어나면 어떤 스크립트에서 검증이 빠질 수 있기 때문입니다.
중복 입력과 재실행 문제 막기
자동화는 한 번만 실행된다고 가정하면 안 됩니다. 네트워크 오류, 파일 누락, 권한 문제 때문에 중간에 실패할 수 있습니다. 담당자가 같은 파일로 다시 실행할 수도 있습니다. 따라서 같은 작업을 여러 번 실행해도 결과가 망가지지 않도록 설계해야 합니다.
예를 들어 주문 파일을 데이터베이스에 넣는 자동화가 있다고 하겠습니다. 같은 파일을 두 번 실행했을 때 주문이 두 번 들어가면 안 됩니다. 이를 막으려면 주문 ID에 고유 제약을 걸고, 이미 존재하는 주문은 건너뛰거나 업데이트해야 합니다.
insert_sql = """
INSERT INTO orders (
order_id, customer_id, product_id, order_date, quantity, amount
)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(order_id) DO NOTHING;
"""
columns = ["order_id", "customer_id", "product_id", "order_date", "quantity", "amount"]
rows = orders_df[columns].itertuples(index=False, name=None)
with sqlite3.connect(DB_PATH) as conn:
conn.execute("PRAGMA foreign_keys = ON")
conn.executemany(insert_sql, rows)SQLite에서는 위와 같이 ON CONFLICT를 사용할 수 있습니다. 예전 예제에서 자주 보이는 INSERT OR IGNORE도 비슷한 목적으로 쓰이지만, 중복뿐 아니라 다른 제약 오류까지 조용히 무시할 수 있으므로 운영 자동화에서는 조심해야 합니다. 다른 데이터베이스에서는 ON CONFLICT, MERGE, ON DUPLICATE KEY UPDATE 같은 문법을 사용합니다. 문법은 다르지만 목적은 같습니다. 이미 처리한 데이터를 다시 넣지 않도록 만드는 것입니다.
또 하나의 방법은 처리 이력을 남기는 것입니다.
CREATE TABLE import_logs (
import_id TEXT PRIMARY KEY,
file_name TEXT NOT NULL,
row_count INTEGER NOT NULL,
imported_at TEXT NOT NULL,
status TEXT NOT NULL
);파일명, 행 수, 처리 시각, 성공 여부를 남겨두면 같은 파일이 다시 들어왔을 때 확인할 수 있습니다. 문제가 생겼을 때도 언제 어떤 파일이 반영되었는지 추적할 수 있습니다. 파일명이 바뀔 수 있는 환경이라면 파일 해시값이나 업무 기준일 같은 식별 정보를 함께 남겨두면 더 안전합니다.
백업 테이블과 트랜잭션으로 안전하게 반영하기
운영 데이터에 변경을 가하는 자동화는 항상 되돌릴 방법을 생각해야 합니다. 특히 대량 업데이트나 삭제는 위험합니다. 실행 전에 대상 데이터를 백업 테이블에 복사해 두면 문제가 생겼을 때 복구할 수 있습니다.
from datetime import datetime
backup_table = "customers_backup_" + datetime.now().strftime("%Y%m%d_%H%M%S")
with sqlite3.connect(DB_PATH) as conn:
conn.execute(f"CREATE TABLE {backup_table} AS SELECT * FROM customers")다만 테이블 이름처럼 SQL 구조에 들어가는 값은 사용자 입력을 그대로 쓰면 안 됩니다. 위 예시는 코드 내부에서 만든 안전한 날짜 문자열을 사용했습니다. 사용자나 파일에서 받은 값을 테이블명, 열 이름, 정렬 기준 같은 SQL 구조에 넣어야 한다면 허용 목록을 만들어 그 안의 값만 사용해야 합니다.
여러 변경 작업은 트랜잭션으로 묶어야 합니다. 트랜잭션은 작업 묶음입니다. 모든 작업이 성공하면 반영하고, 중간에 하나라도 실패하면 전체를 되돌립니다.
from datetime import datetime
backup_table = "customers_backup_" + datetime.now().strftime("%Y%m%d_%H%M%S")
try:
with sqlite3.connect(DB_PATH) as conn:
conn.execute("PRAGMA foreign_keys = ON")
conn.execute(f"CREATE TABLE {backup_table} AS SELECT * FROM customers")
conn.execute("UPDATE customers SET region = '서울' WHERE region = 'Seoul'")
conn.execute("UPDATE customers SET region = '부산' WHERE region = 'Busan'")
except Exception as e:
print(f"작업 실패: {e}")
raisewith sqlite3.connect() 블록 안에서 오류가 발생하면 변경 사항이 자동으로 취소됩니다. 실제 운영 환경에서는 print 대신 로그 파일에 남기고, 필요하면 담당자에게 알림을 보내야 합니다. 또한 대량 UPDATE나 DELETE를 실행하기 전에는 같은 조건으로 SELECT를 먼저 실행해 대상 건수가 예상과 맞는지 확인하는 습관이 필요합니다.
운영 가능한 데이터베이스 자동화 체크리스트
데이터베이스 자동화는 단순 조회로 시작해도 곧 저장, 수정, 삭제 작업으로 확장됩니다. 다음 항목을 습관처럼 확인해야 합니다.
원본 데이터의 기준은 어디인가?
테이블마다 고유 키가 있는가?
필수 열과 데이터 타입을 검증하는가?
중복 실행해도 결과가 안전한가?
운영 테이블 반영 전에 스테이징 테이블을 사용하는가?
대량 변경 전 백업을 만드는가?
변경 작업을 트랜잭션으로 묶는가?
접속 정보와 개인정보가 코드나 로그에 노출되지 않는가?
실패 시 담당자가 원인을 추적할 수 있는 로그가 있는가?
중요한 변경에는 사람의 승인 단계가 있는가?
이 체크리스트는 복잡한 시스템에서만 필요한 것이 아닙니다. 작은 고객 목록 동기화, 월별 매출 집계, 재고 부족 알림 같은 작업에도 필요합니다. 자동화는 한 번 성공하는 것보다 같은 조건에서 안정적으로 반복되는 것이 더 중요합니다.
다음 자동화를 위한 연결점
이 장에서는 엑셀 중심 업무를 넘어 데이터베이스와 업무 데이터를 연결하는 방법을 살펴보았습니다. SQL로 필요한 데이터를 조회하고, 파이썬으로 가공하고, 엑셀과 데이터베이스 사이의 동기화를 설계했습니다. 또한 중복 입력 방지, 백업, 트랜잭션, 무결성 같은 운영 안정성의 기본 원칙도 다루었습니다.
앞으로 API, 이메일, 협업 도구, AI 자동화를 연결하더라도 데이터 흐름의 기본은 같습니다. 신뢰할 수 있는 곳에서 데이터를 가져오고, 처리 전후를 검증하고, 결과를 추적 가능한 방식으로 저장해야 합니다. 데이터베이스는 이 흐름의 중심 저장소가 됩니다. 자동화가 커질수록 데이터베이스를 안정적으로 다루는 능력이 전체 시스템의 품질을 결정합니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.