본문 바로가기
TIL _Today I Learned/2024.10

[DAY 68] 데이터 엔지니어링

by gamdong2 2024. 10. 25.
[천재교육] 프로젝트 기반 빅데이터 서비스 개발자 양성 과정 9기
학습일 : 2024.10.25

📕 학습 목록

  • 데이터 엔지니어링
  • 데이터 파이프라인 모듈 구성
  • Connection Pool
  • ORM
  • DB Connection 살습

 

📗 기억할 내용

1. 데이터 엔지니어링

1) 데이터 엔지니어링이란?

  • 데이터 엔지니어링: 데이터를 수집하고 활용할 수 있도록 시스템을 구축하는 것
  • 대규모 데이터를 효율적으로 수집, 저장, 처리 및 전송하기 위한 기술과 인프라를 개발하는 작업 

 

2) 데이터 관리와 분석

  • Data Lake(DL): 다양한 형태의 데이터를 원시 형태로 대규모로 저장하는 시스템
    • 구조화된 데이터 뿐만 아니라 비구조화된 데이터까지 모두 수용
    • 원시 데이터저장, 스키마 미리 정의하지 않음, 대용량 데이터 처리에 유리 
  • Data Warehouse(DW): 여러 소스에서 데이터를 가져와 분석에 적합하도록 정리된 구조화된 데이터 저장소
    • 정제된 데이터 저장, 스키마 정의 필요  
    • 비즈니스 인텔리전스(BI), 보고서 생성에 사용 
  • Data Mart(DM): 특정 부서에서 사용할수 있도록 데이터 웨어하우스의 일부 데이터를 추출해 소규모로 저장한 것
    • 특정 목적에 맞춘 소규모 데이터 저장소 
    • 빠르고 간편한 접근 가능, 특정 비즈니스 기능에 최적화 
  • Data Governance: 데이터의 품질, 보안, 규정 준수를 보장하기 위해 데이터를 관리하고 정책을 수립하는 체계적인 프로세스

    • 데이터의 일관성, 무결성, 접근 권한 등을 관리
    • 기업의 데이터 활용 가치를 극대화
  • Data Pipeline: 데이터를 수집, 처리, 저장하는 일련의 자동화된 과정(≒ ETL)
    •  데이터 파이프라인의 주요 단계
      • 데이터 수집: 다양한 소스(데이터베이스, 파일, API, IoT 디바이스 등)로 부터 데이터를 가져옴
      • 데이터 처리: 수집한 데이터를 변환, 정제, 통합
      • 데이터 저장 및 전달: 처리된 데이터를 DL, DW, DM 또는 분석 시스템에 저장∙전달하여 분석∙의사결정에 활용할 수 있게 함  
    • 데이터 파이프라인의 활용 예시
      • 웹 로그 분석: 웹사이트 로그 데이터를 수집하여 사용자 행동을 분석하고, 마케팅 전략 수립에 활용
      • 데이터 통합: 여러 시스템의 데이터를 통합하여 하나의 저장소에 저장해, 전체 데이터를 통합적으로 분석
      • 머신러닝 데이터 준비: 데이터를 수집하고 전처리하여 머신러닝 모델에 적합한 데이터셋을 제공

 

3) Data vs Meta Data

  • 데이터
    • 실제 내용이나 정보 그 자체
    • 분석, 저장, 처리의 대상
  • 메타데이터
    • 데이터를 설명하는 데이터; 데이터에 대한 설명 또는 속성 정보
    • 데이터를 더 잘 이해하고 관리할 수 있도록 도움
    • 검색∙분류를 더 쉽게 해줌

4) OLTP vs OLAP

  • OLTP (Online Transaction Processing, 온라인 트랜잭션 처리)
    • 트랜잭션을 실시간으로 처리하고 업데이트하는 시스템
    • 은행, 쇼핑몰 등에서 발생하는 실시간 데이터 작업에 적합 
  • OLAP (Online Analytical Processing, 온라인 분석 처리)
    • 대량의 데이터를 분석하고 비즈니스 의사결정을 돕는 시스템
    • 매출이나 고객 행동 패턴 분석같이 의사결정을 위한 데이터 분석에 적합

5) Batch vs Stream

  • Batch Processing: 많은 양의 데이터를정해진 시간에 일괄적으로 처리하는 것
  • Stream Processing: 실시간으로 들어오는 데이터를 계속 처리하는 것

 

2. 데이터 파이프라인 모듈 구성

1) 데이터 파이프라인 모듈 파일의 각 역할

  • 데이터 파이프라인이 데이터를 수집, 처리, 저장하는 과정을 어떻게 나누고 있는지 확인

 

2) 데이터 파이프라인 모듈 구성 설계 원칙

  • 객체 지향적 프로그래밍: 파이프라인 모듈이 객체 지향적 방식으로 설계. 즉, 각 기능을 클래스와 객체 단위로 나누어 관리하고 재사용성을 높이는 방식으로 설계
  • 하나의 실행 파일은 하나의 기능만 담당: 각 파일이 특정한 하나의 역할에만 집중하도록 설계. 예를 들어, extract.py 는 데이터 추출만 담당하는 식으로 모듈화 됨
  • 기능과 연관 있는 직관적인 명명: 각 파일이 하는 기능을 직관적으로 알 수 있도록 명명. 예를 들어, query.py는 쿼리와 관련된 기능을 맡고 있음
  • 관련성 있는 실행 파일은 같은 폴더 아래에: 관련된 파일들(예: 데이터베이스 관련 파일, 파이프라인 관련 파일 등)은 같은 폴더에 그룹화되어 구조적으로 정리됨

3) 절차적 프로그래밍 vs 객체 지향 프로그래밍

  • 절차적 프로그래밍 (Procedural Programming): 순차적 흐름에 따라 프로그램을 작성하는 방식
    • 함수(or 프로시저)를 사용해 코드를 모듈화
    • 함수가 데이터에 접근해 작업을 수행 
  • 객체지향 프로그래밍(OOP, Object-Oriented Programming): 객체라는 개념을 통해 프로그램을 구조화하는 방식
    • 클래스와 객체를 사용해 데이터를 캡슐화
    • 객체 간 상호작용을 통해 작업을 수행 
  절차적 프로그래밍 객체지향 프로그래밍
구조 순차적 흐름, 함수 기반 객체와 클래스 기반
데이터 관리 함수와 데이터가 분리되어 있음 데이터와 메서드를 객체 단위로 묶어서 관리
재사용성 함수 단위로 재사용 가능 상속과 다형성을 통한 높은 재사용성
유지보수성 복잡한 프로그램에서 유지보수가 어려움 코드 구조가 모듈화되어 유지보수가 쉬움
적용 예시 작은 규모의 프로젝트, 단순한 스크립트 대규모 프로젝트, 복잡한 시스템
예제 언어 C, Pascal Java, Python, C++

 

3. Connection Pool

1) 정의

  • 데이터베이스와의 연결을 효율적으로 관리하기 위해 일정량의 Connection 객체를 미리 생성하여 풀에 저장해 두는 구조

2) 작동 원리

  • 클라이언트가 데이터베이스 연결을 요청할 때마다 새 연결을 생성하는 대신, 이미 만들어진 Connection 객체를 빌려줌
  • 작업이 끝난 후에는 Connection 객체가 반납되어 다시 Pool에 저장됨
    • 데이터베이스 연결은 시스템 자원을 사용하므로, 작업이 끝난 후 반드시 풀로 반납해야 함
    • 이를 위해 Python의 with문을 사용하면 자동으로 연결이 반납되어 자원 누수를 방지할 수 있음
  • 적정 Connection 수 공식: https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing#the-formula
    • 큰 Connection Pool 은 메모리 소모가 큰 대신 대기 시간이 적어짐
    • 작은 Connection Pool은 메모리 소모가 적은 대신 대기 시간이 길어짐

3) 파이썬 with문 활용 예시

  • 이 방식은 Connection Pool 내의 Connection 자원을 효율적으로 관리하고, 성능을 최적화 함
import mysql.connector

with mysql.connector.connect(...) as connection:
    with connection.cursor() as cursor:
        # 작업 수행
# with문이 끝나면 자동으로 connection이 풀에 반납됨

 

4. ORM(Object Relational Mapping)

1) 정의

  • 프로그래밍 언어(ex: 파이썬)의 객체를 관계형 데이터베이스의 테이블에 매핑해주는 기술
  • 데이터베이스와의 상호작용을 객체 지향적으로 데이터를 처리할 수 있음

2) 작동 방식

  • SQL 쿼리를 직접 작성하지 않고 객체를 통해 데이터베이스에 접근하고 데이터를 조작함
  • 파이썬에서는 Django ORM, SQLAlchemy ORM이 대표적임

3) 장점

  • 코드 기반 데이터 조작: SQL 쿼리 대신 코드로 데이터 조작이 가능하여 프로그래밍에 더 집중할 수 있음
  • 재사용성과 유지보수: 데이터베이스 스키마 변경 시에도 ORM 모델만 수정하면 되므로, 재사용 및 유지보수가 편리
  • DBMS 종속성 감소: 특정 데이터베이스 관리 시스템(DBMS)에 종속되지 않아 다양한 DBMS로 손쉽게 교체할 수 있음

 

📘코드 실습

[파이썬  클래스 선언]

1) 필요한 패키지 한 번에 설치

# requirements.txt
psycopg2==2.9.9
pandas==2.1.1
numpy==1.26.1
ipykernel==6.25.2
SQLAlchemy==2.0.22
python-dotenv==1.0.0
pymysql==1.1.0
pyarrow==14.0.0
click==8.1.7
Faker
easydict
boto3
shortuuid==1.0.11
streamlit==1.31.1
streamlit-autorefresh==1.0.1
altair==4.0
vega_datasets==0.9.0

① ctrl shift ` : requirements.txt 파일 위치로 경로 이동하여 터미널 실행

② pip install -r requirements.txt : requirements.txt 파일 내 패키지들 한 번에 설치

 

2) 외부에서 파일 실행하기

① 폴더(test_folder) 생성 → 폴더 내에 두 가지 파일 생성

  • __init__.py : __init__.py 파일이 있어야 파이썬은 해당 폴더를 패키지로 인식함 → import로 불러올 수 있게 됨
  • test.py : 클래스(test_class)를 정의할 파일
[tip] __xxx__ 표기법 (Dunder Name, 던더 네임)
: 파이썬에서 특별한 의미나 내부 동작을 정의하는 데 사용됨. __init__이나 __tablename__처럼, 파이썬이 자동으로 인식하고 특정 기능을 수행하도록 만들어진 이름

 

②  test.py 에 코드 입력 후 실행

# test.py
print("오늘은 날씨가 좋습니다.")
print("Hello World")

"""
오늘은 날씨가 좋습니다.
Hello World
"""

 

③ test_folder 폴더와 같은 경로에 test2.py 생성

④ test2.py에서 test_folder>test.py 가 실행되게 만들기

# test2.py
from test_folder import test

"""
오늘은 날씨가 좋습니다.
Hello World
"""

 

3) 클래스 정의 후, 외부에서 호출하기

[tip] 클래스와 객체 개념 정리

* 클래스와 객체의 개념

- 클래스(Class): 객체를 생성하기 위한 청사진(설계도). 클래스에는 특정 유형의 객체가 가지는 속성(데이터)과 메서드(기능)가 정의됨
- 객체(Object): 클래스에 의해 생성된 구체적인 인스턴스.하나의 클래스에서 여러 객체를 만들 수 있으며, 각 객체는 독립적인 상태와 속성을 가질 수 있음

* __init__ 메서드의 의미와 역할
- 초기화 (Initialization): __init__ 메서드는 객체가 생성될 때 자동으로 실행됨. 주로 객체의 초기 속성을 설정하는 역할을 하며, 이를 통해 객체가 생성되자마자 필요한 초기 값들을 가질 수 있게 함
- 매개변수 전달: 객체를 생성할 때 필요한 초기값들을 __init__의 매개변수로 받을 수 있음. 이 값들은 각각의 객체마다 다를 수 있으며, 객체가 생성될 때 사용자가 원하는 초기 상태로 설정할 수 있음
- 인스턴스 변수 설정: self 키워드를 사용해 인스턴스 변수에 접근하고 초기값을 할당. 인스턴스 변수는 각 객체마다 독립적으로 존재하는 변수로, 하나의 클래스에서 생성된 여러 객체가 각기 다른 상태를 가질 수 있도록 도움
- 예를 들어, 다음과 같은 __init__ 메서드를 가진 클래스가 있을 때 이 클래스에서 생성된 객체는 name과 age 라는 인스턴스 변수를 가지며, 각 객체는 서로 다른 이름과 나이를 가질 수 있음
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

* __init__ 메서드를 사용하는 이유
- 객체의 독립성 유지: __init__ 메서드를 통해 각 객체가 생성될 때 필요한 초기 값을 설정하여 객체마다 독립적인 속성을 가질 수 있게 함. 이는 객체 지향 프로그래밍의 중요한 원칙인 객체 독립성을 유지하는 데 필수적임
- 자동 초기화: 객체를 생성할 때마다 동일한 초기 설정이 필요하다면, __init__ 메서드에 정의해두어 객체 생성과 동시에 초기 설정이 자동으로 이루어지도록 할 수 있음
- 예를 들어, 각 객체의 name이나 age와 같은 기본 속성을 미리 설정하여 생성 후 별도의 설정 없이 사용할 수 있게 함
- 코드 가독성 및 유지보수성 향상: __init__ 메서드로 초기값 설정이 체계적으로 이루어지면, 코드의 구조가 명확해지고, 유지보수성이 향상됨

* __init__ 메서드 정의가 필수인가?
- __init__ 메서드는 필수가 아니며, 클래스에 특별한 초기화 작업이 필요 없을 경우 생략할 수 있음
- 단, __init__을 정의하지 않으면 객체 생성 후 초기 속성을 수동으로 설정해야 하기 때문에, 매번 객체 생성 시 추가 설정을 반복해야 할 수 있음

 

① 클래스(test_class)가 정의된 파일(test.py) 만들기

  • 클래스 : 겍체(인스턴스)를 생성할 수 있는 일종의 툴을 제공  
  • 클래스 변수 : 클래스에 속해 있는 변수로, 모든 인스턴스가 공통으로 사용하게 됨
  • __init__ 메서드  : 생성자 메서드. 클래스의 인스턴스(객체)가 생성될 때 자동으로 호출되어 객체의 초기 설정을 해줌
    • self : 생성된 인스턴스 자신을 가리킴. 인스턴스 내부에 데이터를 저장하려면 self를통해 접근해야 함
    • A, B, C : 생성 시 외부에서 전달받는 값. self.a = A 와 같이, 전달받은 값들은 인스턴스 변수 a, b, c에 저장함
    • self.a, self.b, self.c : 인스턴스 변수. 각각 인스턴스별로 독립적으로 존재. 인스턴스마다 다른 값을 가질 수 있음
  • 나머지 메서드들 : 클래스 내부에 있는 함수들은 "메서드"라고 부름
    • text_concat 메서드 : self.a 와 self.b 의 값을 문자열로 연결해 반환
    • text_concat_v2/v3 메서드 : self.a, self.b, self.c에 클래스 변수 temp0, temp1을 덧붙여 연결한 문자열을 반환
      • temp=temp0 : text_concat_v2 메서드를 호출할 때 기본적으로 temp0 값을 사용할 것을 지정한 것
# text_concat_v2() 메서드에 매개변수 입력/미입력시 반환되는 결과
obj = test_class("A", "B", "C")
print(obj.text_concat_v2())      # 출력: "AB0" (기본값 사용)
print(obj.text_concat_v2("X"))   # 출력: "ABX" (매개변수 값 사용)
# test.py
class test_class:  # 클래스 선언
    temp0 = "0" # 클래스 변수
    temp1 = "1"

    def __init__(self, A, B, C):  # __init__() 메서드
        self.a = A
        self.b = B
        self.c = C

    def text_concat(self):  # 인스턴스의 매개변수를 받지 않음(인스턴스에서 해당 메서드 호출 시 매개변수 입력할 경우, 오류 발생)
        return self.a + self.b
    
    def text_concat_v2(self, temp=temp0):  # 인스턴스의 매개변수를 받음. 받지 않을 경우 기본값(클래스 변수 temp0)으로 대체 
        return self.a + self.b + temp
        
    def text_concat_v3(self, temp=temp1):
        return self.c + temp

 

② 다른 파일(test2.py)에서 클래스 불러오기 및 인스턴스 생성(example)

# test2.py
from test import test_class  # test.py 파일에서 test_class 클래스를 불러옴

# 인스턴스 생성
example = test_class("안녕하세요", "!", "밥 먹으러 가요.")

# 메서드 호출
result1 = example.text_concat()
result2 = example.text_concat_v2()
result3 = example.text_concat_v3()

# 결과 출력
print(result1)  # 출력: 안녕하세요!
print(result2)  # 출력: 안녕하세요!0
print(result3)  # 출력: 밥 먹으러 가요.1

 

[파이썬에서 DB Connection]

1. psycopg2 라이브러리를 통한 Connection

[요약]
- psycopg2: PostgreSQL 데이터베이스와의 연결을 제공
- cursor.execute(): 연결된 데이터베이스에서 SQL을 실행하고 데이터를 처리
- db.commit(): 데이터베이스에 대한 모든 변경 작업을 확정; 변경된 데이터를 데이터베이스에 영구적으로 저장
  • psycopg2 : 파이썬에서 PostgreSQL 데이터베이스와 연결하고, SQL 쿼리를 실행할 수 있도록 해주는 라이브러리 
    • PostgreSQL 연결: psycopg2.connect()를 사용해 PostgreSQL 데이터베이스에 연결할 수 있으며, 연결된 데이터베이스에서 쿼리를 실행하고 결과를 받을 수 있음
    • SQL 실행: psycopg2를 통해 SELECT, INSERT, UPDATE, DELETE 등 다양한 SQL 쿼리를 실행할 수 있음
    • 트랜잭션 관리: psycopg2는 트랜잭션을 자동으로 관리하며, 변경사항을 커밋하거나 롤백할 수 있는 기능을 제공
  •  cursor : 데이터베이스와의 연결을 통해 SQL 쿼리문을 실행하고 결과를 처리하는 객체
    • 쿼리 실행: cursor.execute() 메서드를 사용하여 SQL 쿼리문을 실행할 수 있음. 예를 들어, cursor.execute("SELECT * FROM table_name")과 같이 쓸 수 있음
    • 결과 처리: 쿼리 실행 후 반환된 데이터를 가져오기 위해 fetchone(), fetchall(), fetchmany() 등의 메서드를 사용할 수 있음
      • fetchone(): 한 행씩 결과를 가져옴
      • fetchall(): 모든 결과를 리스트로 가져옴
      • fetchmany(size): 지정한 크기만큼의 결과를 가져옴
    • 메모리 공간 관리: cursor는 데이터베이스에서 반환된 결과값을 메모리에 저장하고, 이를 순서대로 가져오도록 지원함
    • 리소스 해제: 데이터베이스 작업이 완료되면 cursor.close()로 cursor를 닫아 메모리와 데이터베이스 리소스를 해제하는 것이 좋음

1) 데이터베이스 Connection

import psycopg2

# 데이터베이스 연결 → 객체(db)에 저장 
db = psycopg2.connect(
    host = '127.0.0.1',
    dbname = 'postgres',
    user = 'postgres',
    password = '1234',
    port=5432
)

# 커서 생성 > 특정 SQL 문장을 처리한 결과를 담고 있는 영역을 가리키는 일종의 포인터 / 쿼리문에 의해서 반환되는 결과값들을 저장하는 메모리 공간
cursor=db.cursor()

 

2) CRUD 실행

① SQL 쿼리문을 변수에 할당

# SQL 쿼리문을 변수에 할당

# CREATE TABLE 쿼리
create_query = """
CREATE TABLE lecture (
    id SERIAL PRIMARY KEY,
    name VARCHAR(32),
    year INT,
    gender VARCHAR(10),
    count INT
);
"""

# INSERT 쿼리
insert_query = """
INSERT INTO lecture (id, name, year, gender, count)
VALUES (1, 'Tom', 2024, 'M', 1100);
"""

# UPDATE 쿼리
update_query = """
UPDATE lecture
SET id = 1,
    name = 'CHUNJAE',
    year = 1994,
    gender = 'M',
    count = 1000
WHERE id = 1;
"""

# DELETE 쿼리
delete_query = "DELETE FROM lecture WHERE id = 1;"

# DROP TABLE 쿼리
drop_query = "DROP TABLE lecture;"

 

② SQL 쿼리문 실행

  • CREATE TABLE : 테이블 생성
# 'lecture'라는 테이블을 생성하고, 각 열의 데이터 타입을 정의함
cursor.execute(create_query)
db.commit()  # 테이블 생성 후 변경사항을 커밋하여 저장
  • INSERT ROW : 데이터 넣기
# 'lecture' 테이블에 id, name, year, gender, count 값을 가진 한 행을 추가
cursor.execute(insert_query)
db.commit()  # 데이터 삽입 후 변경사항을 커밋하여 저장
  • SELECT : 넣은 데이터확인
# 'lecture' 테이블에 추가된 데이터를 확인하기 위해 전체 데이터 조회
cursor.execute("SELECT * FROM lecture")
cursor.fetchall()  # 결과를 모두 가져옴
  • UPDATE ROW : 테이블에서 id=1인 행의 데이터 업데이트
# 'lecture' 테이블에서 id가 1인 행의 데이터를 업데이트
cursor.execute(update_query)
db.commit()  # 데이터 수정 후 변경사항을 커밋하여 저장

# 업데이트 잘 됐는지 확인
# 업데이트된 데이터를 확인하기 위해 다시 전체 데이터 조회
cursor.execute("SELECT * FROM lecture")
cursor.fetchall()  # 업데이트된 결과를 모두 가져옴
  • DELETE ROW : 테이블에서 id=1인 행을 삭제
# DELETE ROW
# 'lecture' 테이블에서 id가 1인 행을 삭제
cursor.execute(delete_query)
db.commit()  # 데이터 삭제 후 변경사항을 커밋하여 저장

# 삭제 잘 됐는지 확인
# 삭제된 결과를 확인하기 위해 다시 전체 데이터 조회
cursor.execute("SELECT * FROM lecture")
cursor.fetchall()  # 삭제 후 결과를 모두 가져옴 (이 행이 비어 있어야 정상 삭제)
  • DROP TABLE : 테이블 삭제
# 'lecture' 테이블을 삭제
cursor.execute(drop_query)
db.commit()  # 테이블 삭제 후 변경사항을 커밋하여 저장

# 드랍 잘 됐는지 확인; 해당 테이블이 없어서 에러남
# cursor.execute("SELECT * FROM lecture")
# cursor.fetchall()  # 이 행을 실행하면 테이블이 없어서 에러 발생

③ Connection 종료

cursor.close()  # 이제 쿼리문 돌려도 에러뜸

 

2. SQLAlchemy  라이브러리를 통한 Connection

1) SQLAlchemy ORM 방식

  • ORM(Object-Relational Mapping, 객체 관계 매핑): 객체 지향 프로그래밍 언어를 사용하여 데이터베이스의 데이터를 관리할 수 있게 해주는 기술. 즉, 관계형 데이터베이스의 테이블과 레코드를 파이썬 코드 내의 클래스와 객체로 매핑하여 SQL 쿼리 없이도 데이터베이스 조작이 가능하게 함
    • ORM의 장점
      • 코드의 간결성: SQL 쿼리문을 작성하지 않고도 객체를 사용해 데이터베이스를 조작할 수 있어 코드가 간결해짐
      • 데이터베이스 독립성: ORM을 사용하면 다양한 데이터베이스 시스템(MySQL, PostgreSQL, SQLite 등)에서 동일한 코드를 사용할 수 있음
      • 보안성과 유지보수성: SQL 쿼리를 직접 사용하지 않으므로 SQL 인젝션과 같은 보안 문제를 줄일 수 있고, 유지보수도 용이해짐
  • SQLAlchemy 라이브러리: 파이썬의 대표적인 ORM 라이브러리
    • SQLAlchemy의 주요 기능
      • ORM 기능: 데이터베이스의 테이블을 파이썬 클래스와 연결하고, 테이블의 행을 객체로 다룰 수 있음
      • SQL 표현 언어: SQLAlchemy는 복잡한 SQL 쿼리를 작성하기 위한 SQL 표현 언어를 제공
      • 데이터베이스 연결 관리: 다양한 데이터베이스 시스템과 연결할 수 있으며, 트랜잭션 관리와 연결 풀링 기능을 제공
      • 세션(Session): SQLAlchemy의 세션은 데이터베이스와의 상호작용을 관리하는 객체로, 데이터를 조회, 추가, 수정, 삭제하는 작업을 처리함. 세션은 트랜잭션을 통해 작업 단위로 데이터 변경 사항을 일괄 반영하며, 데이터베이스의 일관성을 유지하는 데 도움을 줌. 각 작업은 세션을 통해 수행되며, commit() 호출 시 데이터베이스에 최종 반영됨

① 데이터베이스 Connection 및 Session 생성

  psycopg2 cursor SQLAlchemy session
사용 목적 SQL 쿼리를 직접 작성∙실행하여 데이터베이스를 조작하는 저수준 객체 ORM으로 객체와 데이터베이스를 매핑하여 상호작용하는 고수준 객체
트랜잭션 제어 db.commit() 호출 필요 session.commit() 호출 필요
상태 관리 없음 파이썬 객체의 상태 관리 (변경 추적)
예시 사용 방식 cursor.execute("SELECT ...") session.add(object), session.query()
from sqlalchemy import create_engine, text  # SQLAlchemy 라이브러리에서 데이터베이스 연결 생성과 SQL 쿼리 실행을 위한 모듈 임포트
from sqlalchemy.orm import sessionmaker      # 세션 생성에 필요한 sessionmaker 함수 임포트

# 데이터베이스 연결 정보 설정
engine_name = 'postgresql'  # 사용할 데이터베이스 엔진 설정 (PostgreSQL 사용)
user_id = 'postgres'        # 데이터베이스 로그인에 사용할 사용자 ID
user_pw = '1234'            # 데이터베이스 로그인에 사용할 사용자 비밀번호
host = '127.0.0.1'          # 데이터베이스 서버의 주소 (127.0.0.1은 로컬호스트를 의미)
port = '5432'               # 데이터베이스 서버의 포트 번호 (PostgreSQL의 기본 포트)
database = 'postgres'       # 연결할 데이터베이스 이름

# 데이터베이스 연결 엔진 생성
db = create_engine(f'{engine_name}://{user_id}:{user_pw}@{host}:{port}/{database}')
# 위 코드에서 create_engine() 함수는 주어진 연결 정보를 통해 데이터베이스 엔진 객체(db)를 생성함
# 이 엔진 객체(db)는 데이터베이스와의 상호작용에 사용됨

# 데이터베이스 연결 생성
cursor = db.connect()       # 데이터베이스에 연결하고, SQL 쿼리를 직접 실행할 수 있는 cursor 객체 생성

# 세션 생성기 준비
Session = sessionmaker(bind=db)  # sessionmaker 함수를 사용하여 세션을 생성하는 클래스를 만듦. 여기서 bind=db로 엔진을 바인딩함
session = Session()              # session 객체 생성, 이 세션을 통해 ORM 기능을 이용하여 데이터베이스와 상호작용을 수행할 수 있음

 

② 테이블을 클래스 형태로 정의 → 데이터베이스에 테이블 생성

[tip] lecture(Base)의 의미
- declarative_base(): SQLAlchemy에서 ORM 클래스의 기본 클래스를 생성하는 함수
- Base 클래스 : declarative_base() 를 호출해서 생성된 기본 클래스. SQLAlchemy에서 ORM 클래스를 만들 때, 이 Base 상속하여 데이터베이스 테이블과 매핑할 클래스를 정의함
- lecture 클래스: Base 클래스를 상속받아 생성된 ORM 클래스. 모든 ORM 클래스는 Base 클래스를 상속받아 정의해야 함(데이터베이스와 연결되는 SQLAlchemy ORM 클래스가 됨)
from sqlalchemy import Column, INTEGER, VARCHAR  # SQLAlchemy에서 열(Column)과 데이터 타입(INTEGER, VARCHAR)을 임포트
from sqlalchemy.orm import declarative_base       # SQLAlchemy ORM에서 기본 클래스를 정의하기 위한 declarative_base 임포트

# 기본 클래스 생성
Base = declarative_base()

# lecture 테이블을 나타내는 ORM 클래스 정의
class lecture(Base):     
    __tablename__ = 'lecture'  # 데이터베이스 테이블 이름을 'lecture'로 설정

    # 각 컬럼 정의
    id = Column('id', INTEGER, primary_key=True, autoincrement=True)  # 좌변의 id는 클래스의 속성명, 우변의 'id'는 데이터베이스 테이블의 컬럼명 지정(두 이름은 통일하지 않아도 됨)
    # 'id' 열을 INTEGER 타입으로 설정하고, 기본 키(primary_key)로 지정하여 고유하게 만듦. autoincrement로 자동 증가 설정

    name = Column('name', VARCHAR(10), nullable=False)
    # 'name' 열을 VARCHAR(10) 타입으로 설정. 길이 제한을 10으로 지정하며, NULL 값을 허용하지 않음 (nullable=False)

    year = Column('year', INTEGER, nullable=False)
    # 'year' 열을 INTEGER 타입으로 설정하고, NULL 값을 허용하지 않음

    gender = Column('gender', VARCHAR(10), nullable=False)
    # 'gender' 열을 VARCHAR(10) 타입으로 설정하고, NULL 값을 허용하지 않음

    count = Column('count', INTEGER, nullable=False)
    # 'count' 열을 INTEGER 타입으로 설정하고, NULL 값을 허용하지 않음

Base.metadata.create_all(db)
# Base.metadata: Base를 상속받는 모든 ORM 클래스(lecture, ...)의 테이블 정보(테이블 이름, 컬럼, 제약 조건 등)을 포함
# create_all(db): 현재 메타데이터에 정의된 테이블들을 데이터베이스에 실제로 생성. SQLAlchemy 엔진 객체인 db을 통해 지정된 데이터베이스에 테이블을 생성
# 테이블이 이미 존재하지 않는 경우에만 생성되며, 존재하면 생성되지 않음
[tip] 클래스 변수? 인스턴스 변수? 메서드? 클래스 속성?
- 클래스 변수: 클래스 자체에 속한 변수로, 모든 인스턴스가 공유함
- 인스턴스 변수: 개별 인스턴스에 속한 변수로, 각 인스턴스마다 별도로 존재
- 메서드: 클래스 안에 정의된 함수로, 클래스 또는 인스턴스와 관련된 작업을 수행
- 클래스 속성: 클래스 안에 정의된 모든 변수와 메서드를 포함하는 개념
class Car:
    wheels = 4  # 클래스 변수

    def __init__(self, color):
        self.color = color  # 인스턴스 변수

    def describe(self):  # 메서드
        return f"This car is {self.color} and has {self.wheels} wheels."

# 클래스 속성: 위의 모든 변수, 메서드를 통칭​

 

③ CRUD 실행

  •  INSERT: 데이터 추가
datal = lecture(id=3, name='chunjae', year=1990, gender='M', count=1234)
# lecture 테이블에 삽입할 데이터를 가진 lecture 인스턴스를 생성. id, name, year, gender, count 값을 설정

session.add(datal)  # 세션을 통해 새로운 데이터를 추가
session.commit()     # 변경사항을 커밋하여 데이터베이스에 반영
  • UPDATE: 특정 조건의 데이터 수정
session.query(lecture).filter(lecture.name == 'chunjae').update({'gender': 'F', 'count': 1500})
# lecture 테이블에서 name이 'chunjae'인 행을 찾아 gender를 'F', count를 1500으로 업데이트

session.commit()
  • SELECT ONE: 테이블에서 첫 번째 행 조회
res = session.query(lecture).first()
# session.query(lecture): SQLAlchemy의 session 객체를 통해 lecture 테이블에서 데이터를 조회하는 쿼리 객체(res)를 생성
# 여기서 lecture는 Base를 상속받아 정의된 ORM 클래스. 데이터베이스의 lecture 테이블과 매핑되어 있음
# 추후, res.id, res.name 처럼 res 객체의 속성을 통해 데이터에 접근할 수 있음
# .first(): lecture 테이블에 존재하는 데이터 중 첫 번째 행을 가져옴. 만약 테이블에 데이터가 없으면 None을 반환 

# 조회된 결과 출력
print(res.id, res.name, res.year, res.gender, res.count)  # 만약 테이블에 데이터가 없으면 오류 발생
  • SELECT ALL: 모든 데이터 조회
all_data = session.query(lecture).all()  # lecture 테이블의 모든 데이터를 조회
for data in all_data:
    print(data.id, data.name, data.year, data.gender, data.count)
# 모든 데이터를 출력. 각 열의 값을 반복문으로 순서대로 출력
  • DELETE: 특정 조건의 데이터 삭제  
session.query(lecture).filter(lecture.id == 3).delete()  # lecture 테이블에서 id가 3인 행을 삭제

session.commit()

 

2) SQLAlchemy Core 방식

  • SQLAlchemy Core 방식: SQLAlchemy의 저수준 접근 방식으로, SQL 쿼리를 직접 작성하고 실행할 수 있는 기능을 제공. ORM처럼 클래스를 사용해 테이블을 정의하는 대신, Table 객체와 SQLAlchemy Core의 메타데이터 객체를 사용하여 테이블을 정의하고 데이터베이스와 상호작용함
    • Core 방식의 장점
      • 세밀한 SQL 제어: SQL을 직접 작성할 수 있어 데이터베이스에 대한 세부적 제어가 가능함
      • 효율성: ORM 방식에 비해 더 가볍고, 성능 측면에서 효율적일 수 있음
      • 유연한 데이터베이스 조작: ORM 매핑 없이도 테이블 생성, 삭제, 데이터 삽입, 조회, 수정, 삭제 등의 작업을 수행할 수 있음
  • SQLAlchemy Core의 주요 기능
    • SQL 표현 언어: SQLAlchemy Core는 SQL 표현 언어(SQL Expression Language)를 통해 Python 코드로 SQL 쿼리를 작성할 수 있게 함. select(), insert(), update(), delete() 메서드를 사용해 다양한 SQL 쿼리 실행 가능
    • Table 객체와 MetaData 객체: Core 방식에서는 Table 객체를 사용하여 테이블과 열을 정의하고, MetaData 객체는 테이블 메타데이터를 관리함. 이를 통해 데이터베이스와의 매핑을 수행함
    • 직접적인 커넥션 관리: Core 방식에서는 connect() 메서드를 통해 커넥션 객체를 생성하여 SQL 쿼리를 직접 실행 가능. ORM과 달리 커넥션을 명시적으로 생성하고, 필요 시 연결을 해제하는 방식으로 데이터베이스와의 상호작용을 제어함
[tip] Core 방식 vs ORM 방식
- 코드 스타일: Core는 SQL에 가깝게 작성하고 직접 제어할 수 있으며, ORM은 클래스와 객체를 사용하여 SQL 쿼리 없이도 데이터 조작 가능
- 유연성: Core 방식은 SQL 쿼리를 직접 다루기 때문에 더 높은 수준의 유연성을 제공하지만, 객체 지향적 접근에서는 ORM 방식이 더 적합

"Core 방식은 SQLAlchemy의 SQL 표현 언어와 Table, MetaData 객체를 사용하여 SQL을 직접 다루고, SQLAlchemy의 고성능 기능을 활용할 수 있는 유연한 방식"

 

① 데이터베이스 Connection

from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData  # SQLAlchemy에서 데이터베이스 연결과 테이블, 컬럼, 데이터 타입을 정의하는 모듈 임포트
from sqlalchemy.orm import sessionmaker  # 세션 생성을 위한 sessionmaker 함수 임포트

# 데이터베이스 연결 설정
db = create_engine(f'{engine_name}://{user_id}:{user_pw}@{host}:{port}/{database}')
# 데이터베이스 연결 엔진 생성. 여기서 설정된 엔진(db)은 지정된 데이터베이스에 연결할 수 있도록 함

# 세션 생성기 준비
Session = sessionmaker(db)  # sessionmaker 함수를 사용하여 세션을 생성하는 클래스 만듦
session = Session()         # 세션 인스턴스를 생성하여 데이터베이스와의 상호작용에 사용

# 메타데이터 객체 생성
meta = MetaData()  # 테이블과 열의 정보를 포함하여 데이터베이스 구조를 관리하는 메타데이터 객체 생성

 

② 테이블 생성

[tip] Table 객체
- SQLAlchemy의 Core API를 사용하여 테이블을 정의할 때 사용
- 이 Table 객체를 임의로 지정한 변수 core_table에 할당하여, 이후 SQL 쿼리 등을 작성할 때 core_table 을 통해 이 테이블을 참조할 수 있음 
# 테이블 정의
core_table = Table(  # core_table 변수에 Table 객체를 할당; 데이터베이스의 특정 테이블을 파이썬 코드에서 참조할 수 있게 함 
    'first', meta,  # 'first'라는 테이블을 meta 객체와 함께 정의
    Column('id', Integer, primary_key=True, autoincrement=True),  # 'id' 열 정의. Integer 타입, 기본 키이며 자동 증가 설정
    Column('name', String),  # 'name' 열 정의. String 타입
    Column('year', Integer),  # 'year' 열 정의. Integer 타입
    Column('gender', String),  # 'gender' 열 정의. String 타입
    Column('count', Integer)  # 'count' 열 정의. Integer 타입
)

# 테이블 생성
meta.create_all(db)  # 데이터베이스에 'first' 테이블 생성. 테이블이 이미 존재하지 않는 경우에만 생성됨

 

③ CRUD 실행

  • SELECT ALL: 모든 데이터 조회
from sqlalchemy import insert, update, delete  # SQLAlchemy에서 insert, update, delete 구문을 사용할 수 있도록 임포트

res = core_table.select()  # core_table이라는 SQLAlchemy 테이블 객체를 SELECT 쿼리 형태로 선택  # 결과적으로 res에는 core_table 테이블의 모든 데이터를 가져오는 SQL 쿼리가 담김  # 이 쿼리는 아직 실행되지 않은 상태로, 실제 실행은 세션이나 연결 객체(execute())를 통해 이루어짐
result = session.execute(res)  # 세션을 통해 생성한 쿼리를 실행하여 결과를 반환

# 조회 결과 출력
for row in result:
    print(row)  # 각 행의 데이터를 출력
  • INSERT ONE: 단일 데이터 삽입
[tip] 연결 객체 conn 의 역할
- 데이터베이스와 하나의 연결 세션을 유지함. 이를 통해 execute() 와 같은 메서드를 호출하여 쿼리를 데이터베이스에 전송하거나, commit() 을 호출하여 변경 사항을 확정하는 등의 작업을 할 수 있음
- 예를 들어, conn.execute(stmt) 는 conn 객체를 통해 데이터베이스에 stmt 쿼리를 전달하고 실행함
stmt = insert(core_table).values(name="hahaha", year=2023, gender="M", count=555)
# stmt 변수에 INSERT 쿼리가 담김  # 이 stmt를 실제로 실행하려면 SQLAlchemy 엔진의 execute() 메서드를 사용해야 함
# .values() 메서드: INSERT 쿼리에 삽입할 데이터 값들을 지정 

# with문을 사용하여 연결을 관리; 작업이 완료되면 자동으로 연결이 닫히게 됨
with db.connect() as conn:  # db.connect(): 데이터베이스와의 연결을 생성  # 데이터베이스에 연결할 때, db.connect()를 호출하면 conn이라는 연결 객체를 반환; 이 연결 객체는 데이터베이스와 상호작용할 수 있는 통로 역할. 이를 통해 SQL 쿼리를 실행하고 결과를 가져오는 작업을 수행
    result = conn.execute(stmt)  # 미리 정의한 INSERT 쿼리를 실행하여 데이터를 테이블에 삽입  # result에 실행 결과 객체가 저장됨
    conn.commit()  # 실행한 INSERT 작업을 데이터베이스에 영구적으로 반영. 커밋을 호출하지 않으면 트랜잭션이 완료되지 않아, 작업이 확정되지 않고 취소될 수 있음
  • INSERT MULTI: 여러 데이터 삽입
stmt = insert(core_table)  # core_table에 데이터를 삽입할 insert 구문을 생성
data_list = [
    {"name": "hwang", "year": 1994, "gender": "M", "count": 12},
    {"name": "jeong", "year": 1996, "gender": "F", "count": 32}
]
# 여러 개의 딕셔너리를 포함한 리스트를 생성하여 삽입할 데이터를 준비

with db.connect() as conn:
    result = conn.execute(stmt, data_list)  # 여러 데이터를 한 번에 삽입
    conn.commit()  # 삽입된 내용을 커밋하여 데이터베이스에 반영
  • UPDATE: 데이터 수정
stmt = update(core_table).where(core_table.c.name == "jeong").values(year=1000)  # core_table.c.name: core_table의 모든 컬럼(.c) 중 name 이라는 컬럼(.name)을 참조
# core_table에서 name이 "jeong"인 행의 year 값을 1000으로 업데이트하는 구문 생성

with db.connect() as conn:
    result = conn.execute(stmt)  # 업데이트 구문을 실행하여 해당 행의 데이터를 수정
    conn.commit()  # 변경 사항을 커밋하여 데이터베이스에 반영
  • DELETE: 데이터 삭제
# Delete : 데이터 삭제
stmt = delete(core_table).where(core_table.c.name == "jeong")
# core_table에서 name이 "jeong"인 행을 삭제하는 delete 구문 생성

with db.connect() as conn:
    result = conn.execute(stmt)  # 삭제 구문을 실행하여 해당 행을 삭제
    conn.commit()  # 변경 사항을 커밋하여 데이터베이스에 반영

 

 

[파이썬 클래스, 객체를 통한 데이터베이스 관리]

1) 클래스(TempClass) 생성

① 데이터베이스 연결

import psycopg2

db = psycopg2.connect(
    dbname=database,
    user=user,
    password=password,
    port=port,
    host=host
)

cursor=db.cursor()

 

② 클래스 정의

[tip] 타입 힌트(Type Hint)
- 파이썬에서는 타입 힌트를 사용하여, 클래스에서 인스턴스 변수의 데이터 타입을 미리 지정
- 아래 예시에서, name: str, age: int, count: int 와 같이 타입을 지정한 부분을 말함
class Temp_Class:
    def __init__(self, name: str, age: int, count: int):
        self.name = name
        self.age = age
        self.count = count​
[tip] __init__() 메서드
- 객체가 생성될 때 자동으로 호출되어, name, age, count 속성을 설정하고, addage() 메서드를 실행
- 객체를 생성할 때 addage()가 자동으로 실행되므로, new_age 속성이 이미 설정된 상태
# 객체 생성
temp_instance = TempClass(name="Alice", age=30, count=5)

# 객체 생성 시 `addage()`가 호출되어 `new_age` 속성이 생성됨
print(temp_instance.age)      # 출력: 30
print(temp_instance.new_age)  # 출력: 40​
# TempClass라는 클래스 정의
class TempClass:
    def __init__(self, name: str, age: int, count: int):
        # 인스턴스 초기화: name, age, count 속성 설정
        self.name = name
        self.age = age
        self.count = count
        self.addage()  # 객체 생성시 addage() 메서드 호출
        
    def printall(self):
        # 모든 속성을 출력하는 메서드
        print(self.name)
        print(self.age)
        print(self.count)

    def printself(self):
        # self 객체를 반환하는 메서드 (자기 자신을 반환)
        return self

    def addage(self):
        # 나이 속성에 10을 더해 new_age 속성 생성
        self.new_age = self.age + 10

 

③ 인스턴스 생성

# TempClass 클래스 인스턴스 생성
a1 = TempClass(age=2, count=3, name="chunjae")  # 인스턴스 생성 및 초기화
s1 = a1.printself()  # a1 인스턴스 자기자신을 반환; s1과 a1은 같은 객체를 가리키는 두 개의 변수가 됨 

# 인스턴스 a1과 s1 출력 (s1은 a1의 self를 가리킴)
print(a1)
print(s1)

 

④ 인스턴스(객체) 내부 속성 및 값 확인; 딕셔너리로 반환

[tip] __dict__ 속성
- 파이썬의 내장 속성으로, 객체가 가진 속성과 속성값의 매핑 정보를 딕셔너리 형태로 저장
s1.__dict__

"""
{'name': 'chunjae', 'age': 2, 'count': 3, 'new_age': 12}
"""

 

⑤ 조건을 만족하는 항목만 반환

[tip] dir(s1)
- s1 객체가 가진 모든 속성과 메서드의 이름을 리스트 형태로 반환
- 여기에는 더블 언더스코어(__)로 시작하는 매직 메서드와 속성도 포함됨
[tip] 리스트 컴프리헨션(List Comprehension)
- 리스트를 간결하고 효율적으로 생성하는 방법
- 형식: [표현식 for 요소 in 반복 가능한 객체 if 조건]
- 예시
[i for i in dir(s1) if '__' not in i]
# dir(s1) 에서 반환된 이름 중 __ 가 포함되지 않은 것만 필터링하여 리스트로 만듦
# dir()을 사용하여 객체의 메서드와 속성 목록에서 __로 시작하지 않는 항목만 가져옴
[i for i in dir(s1) if '__' not in i]

"""
['addage', 'age', 'count', 'name', 'new_age', 'printall', 'printself']
"""

 

⑥ 클래스(EasyDict)로 부터 객체(self) 생성

[tip] EasyDict(dict()) vs EasyDict()
- EasyDict: 딕셔너리 형태로 속성에 접근할 수 있는 클래스. 보통 기존 딕셔너리를 넘겨 받아 객체를 생성
- EasyDict(dict()): 빈 딕셔너리 dict()를 EasyDict 객체의 초기값으로 지정. 이후 속성을 자유롭게 추가하 수 있게 함
- EasyDict(): 이렇게 클래스를 호출해도 빈 딕셔너리로 초기화된 객체를 생성함. 하지만 dict()를 추가해주는게 코드의 의도(객체가 빈 딕셔너리로 초기화됨)가 더 분명하게 보여서 가독성이 향상됨
# EasyDict 라이브러리에서 EasyDict 클래스 가져오기
from easydict import EasyDict

# EasyDict 객체 self 생성
# EasyDict(dict())는 비어 있는 딕셔너리로 초기화된 EasyDict 객체를 생성함
self = EasyDict(dict())

# EasyDict 객체 self에 속성 추가
self["name"] = "jeongseok"  # name이라는 키에 "jeongseok" 값 설정
self["age"] = 3             # age라는 키에 3 값 설정
self["count"] = 1           # count라는 키에 1 값 설정

# s2 변수에 EasyDict 객체 self를 할당
# 여기서 s2와 self는 같은 EasyDict 객체를 참조 (같은 값을 공유함)
s2 = self

# s2를 출력하면 self와 같은 내용이 출력됨 (EasyDict 객체 s2의 현재 상태를 확인)
s2

# EasyDict 객체 self에 새로운 속성 hoho 추가
# 객체처럼 . 표기법을 통해 속성 추가 및 값 설정.
self.hoho = "ho"  # hoho라는 새로운 속성을 추가하고 값으로 "ho" 설정

# self를 출력하면 hoho 속성이 추가된 상태로 출력됨
self

 

2) 클래스(DBConnector) 생성

① 클래스 정의

  • Context Manager (__enter__, __exit__) 
    • Context Manager(컨텍스트 관리자): 자원 관리나 특정 작업의 시작과 끝을 자동으로 처리하는 구조
    • 데이터베이스 연결 시 with문을 사용하여 안전하게 DB 연결을 종료할 수 있도록 함
      • __enter__:  with문에 진입할 때 실행됨
      • __exit__: with문이 끝날 때 실행되어 데이터베이스 연결을 자동으로 닫음(close 메서드를 호출할 필요X)
import psycopg2

class DBConnector:
    def __init__(self, host, database, user, password, port):
        # 초기화 메서드: DB 연결 정보 초기화
        print("Start! Class")
        # self.conn_params: 데이터베이스 연결에 필요한 매개변수들을 딕셔너리 형태로 저장한 속성. 필요시 참조 가능
        self.conn_params = dict(  
            host=host,
            dbname=database,
            user=user,
            password=password,
            port=port
        )
        # 연결 정보 저장
        self.host = host
        self.database = database
        self.user = user
        self.password = password
        self.port = port

    def postgres_connect(self):
        # PostgreSQL 연결 메서드
        print("<<< postgres_connect >>>")
        # psycopg2.connect() : 데이터 베이스 연결을 생성하고, 데이터베이스 연결 객체인 self.conn이 정의됨
        self.conn = psycopg2.connect(  
            host=self.host,
            dbname=self.database,
            user=self.user,
            password=self.password,
            port=self.port
        )
        return self  # self를 반환하여 체이닝이 가능하도록 설정
        
    def print_conn_params(self):
        # conn_params를 출력하는 메서드
        print("Connection Parameters:", self.conn_params)
           
    def __enter__(self):
        # with문 진입 시 호출되어, 객체 반환
        print("Enter")
        return self
        
    # 데이터베이스 연결 종료; postgres_connect(self)에서 생성된 연결 객체 self.conn을 참조하여 데이터베이스 연결을 종료
    def __exit__(self, exc_type, exc_value, traceback):
        # with문 종료 시 호출되어, DB 연결을 닫음
        self.conn.close()
        print("Exit")

 

③ 인스턴스(db_object) 생성

# DB 연결 인스턴스 생성
db_object = DBConnector(
    host="127.0.0.1", 
    database="postgres", 
    user="postgres", 
    password="1234", 
    port="5432"
)

 

④ 데이터베이스 연결 테스트

# 데이터베이스 연결
db_object.postgres_connect()  # self.conn 생성

# with문을 사용하여 안전하게 연결 관리
with db_object as connected:
    db_conn = connected.conn  # 연결된 DB 객체 가져오기
    cursor = db_conn.cursor()  # 커서 객체 생성

    # 데이터베이스 조회 테스트
    cursor.execute("SELECT * FROM lecture LIMIT 10")
    print(cursor.fetchall())  # 조회 결과 출력

 

⑤ Context Manager 예제

  • ContextManager 클래스 생성: with 문을 사용한 리소스 관리 예시
  • __enter__, __exit__ 메서드 구현: with 문 내부와 외부에서 리소스 관리가 자동으로 이루어짐
[tip] return self 
- return self: 추후 with 문 안에서 __enter__ 메서드가 호출될 때 self가 반환되어, with 문 내부에서 해당 객체(self)에 접근할 수 있음
- return self가 없다면 cm 변수는 None이 되어 객체에 접근할 수 없음
class ContextManager:
    def __init__(self, age):
        print("Start! Class")
        self.age = age
    
    def __enter__(self):
        print("Enter")
        return self  # `self`를 반환하여 `with` 문에서 사용할 수 있게 함

    def __exit__(self, exc_type, exc_value, traceback):
        print("Exit")

# with 문을 사용하여 ContextManager 객체 생성 및 사용
with ContextManager(age=25) as cm:
    print("Inside with statement, age:", cm.age)
# with 문을 시작할 때 __enter__ 메서드가 호출되고, 이 메서드가 반환한 값이 as 구문에서 지정한 변수에 할당됨. 이렇게 할당된 변수는 with 문 블록 내에서 바로 사용할 수 있게 됨
# cm: __enter__ 메서드의 return self에 의해 반환된 ContextManager 객체
# with 문 안에서 cm을 통해 ContextManager 객체의 속성과 메서드를 바로 사용할 수 있음
# 즉, with 문에서 as 구문을 사용하면 객체를 바로 변수로 받아 with 블록 안에서 편리하게 접근할 수 있게 됨​
class ContextManager:
    def __init__(self, age):
        print("Start! Class")
        self.age = age

    def __enter__(self):
        # with문 진입 시 호출
        print("Enter")
        return self
    
    def __exit__(self, exc_type, exc_value, traceback):
        # with문 종료 시 호출
        print("Exit")

    def show(self):
        # age를 출력하는 메서드
        print(self.age)
        return self.__dict__

# ContextManager 인스턴스 생성 및 with문 사용 예제
context = ContextManager(age=10)
with context:
    print(context.show())

 

[데이터베이스에 데이터 Bulk Insert]

1) for문을 통해 Bulk Insert

# 최종 목적: for문을 사용하여 데이터프레임의 데이터를 bulk insert하는 코드

# psycopg2를 불러와 PostgreSQL과 연결하는 라이브러리
import psycopg2 as pgsql

# PostgreSQL 데이터베이스 연결 설정
db = pgsql.connect(
    host='127.0.0.1',        # 로컬호스트 IP
    dbname='postgres',       # 사용할 데이터베이스 이름
    user='postgres',         # 데이터베이스 사용자 이름
    password='1234',         # 데이터베이스 비밀번호
    port=5432                # PostgreSQL 기본 포트
)

# 커서 생성
cursor = db.cursor()

# 기존 lecture 테이블 삭제
drop_query = "DROP TABLE lecture"
cursor.execute(drop_query)
db.commit()  # 변경사항을 데이터베이스에 반영

# pandas를 불러와 CSV 파일을 데이터프레임으로 읽어오기
import pandas as pd

# CSV 파일을 읽어와 데이터프레임 생성
df = pd.read_csv('./dataset/data-01/names.csv')
df.head(10)  # 데이터프레임의 첫 10개의 행 출력 (테스트 용도)

# 데이터 타입 확인
df.dtypes
# pandas는 SQL의 VARCHAR 타입을 인식하지 않으며, integer 타입을 구분하지 않음

# lecture 테이블의 모든 데이터 삭제 (데이터 삽입 전 초기화)
delete_query = "DELETE FROM lecture"
cursor.execute(delete_query)
db.commit()  # 변경사항을 데이터베이스에 반영

# 데이터프레임의 각 행을 튜플로 변환하여 lecture 테이블에 데이터 삽입
for data in range(len(df)):  # df의 행 수만큼 반복문을 실행; 각 행을 반복적으로 처리
    datas = tuple(df.iloc[data])  # 데이터프레임의 행을 튜플로 변환; INSERT INTO 쿼리에 사용할 값을 튜플 형태로 만들기 위함
    query = f"INSERT INTO lecture VALUES{datas}"  # INSERT 쿼리 생성(f-string)
    cursor.execute(query)  # 쿼리 실행; 현재 행을 lecture 테이블에 삽입

db.commit()  # 모든 데이터를 삽입 후 변경사항을 데이터베이스에 반영

# lecture 테이블에서 상위 10개 행 선택
cursor.execute("SELECT * FROM lecture LIMIT 10")
cursor.fetchall()  # 상위 10개 행을 가져와 출력

 

2) to_sql() 을 통해 Bulk Insert

# SQLAlchemy 라이브러리에서 create_engine을 불러와 데이터베이스와 연결 설정
from sqlalchemy import create_engine

# 데이터베이스 연결 정보 선언
engine = 'postgresql'       # 사용할 데이터베이스 엔진 (PostgreSQL)
user = 'postgres'           # 데이터베이스 사용자 이름
password = '1234'           # 데이터베이스 비밀번호
host = '127.0.0.1'          # 로컬호스트 IP 주소
port = '5432'               # PostgreSQL 기본 포트
database = 'postgres'       # 데이터베이스 이름

# SQLAlchemy 엔진 생성, 연결 문자열 형식으로 데이터베이스에 연결
db = create_engine(f'{engine}://{user}:{password}@{host}:{port}/{database}')

# pandas 불러오기
import pandas as pd

# CSV 파일을 읽어와 데이터프레임 생성
df = pd.read_csv('./dataset/data-01/names.csv')

# 특정 행 선택 (테스트용)
df.iloc[1]  # DataFrame에서 인덱스 1에 해당하는 행 선택
df.iloc[1:]  # 인덱스 1 이후의 모든 행을 선택

# 데이터프레임을 PostgreSQL 테이블에 BULK INSERT
df.to_sql(name="lecture", con=db, if_exists="replace")
# 'lecture'라는 테이블 이름으로 데이터프레임을 데이터베이스에 저장
# 기존 테이블이 존재할 경우 대체 (replace)

 

3) execute_values() 를 통해 Bulk Insert

# psycopg2 라이브러리를 불러와 PostgreSQL에 연결
import psycopg2

# PostgreSQL 데이터베이스 연결 설정
db = psycopg2.connect(
    host='127.0.0.1',        # 로컬 IP 주소
    dbname='postgres',       # 데이터베이스 이름
    user='postgres',         # 데이터베이스 사용자 이름
    password='1234',      # 데이터베이스 비밀번호
    port=5433                # PostgreSQL 포트 (기본 5432가 아닌 5433으로 설정)
)

# 커서 생성
cursor = db.cursor()

# lecture 테이블의 모든 데이터를 삭제하는 쿼리
delete_query = "DELETE FROM lecture;"
cursor.execute(delete_query)  # 쿼리 실행
db.commit()  # 변경사항을 데이터베이스에 반영

# lecture 테이블에서 모든 데이터를 선택하는 쿼리
cursor.execute("SELECT * FROM lecture")
cursor.fetchall()  # 선택된 데이터를 가져옴 (테스트 용도)

# pandas 라이브러리를 불러와 CSV 파일을 데이터프레임으로 읽기
import pandas as pd

# CSV 파일 읽어오기
df = pd.read_csv('./dataset/data-01/names.csv')

# 데이터형 변환 (id, year, count 열을 float 타입으로 변환)
df['id'] = df['id'].astype(float)
df['year'] = df['year'].astype(float)
df['count'] = df['count'].astype(float)

# 데이터 삽입을 위한 리스트 생성
insert_list = list()

# DataFrame에서 인덱스 5부터 49까지의 행을 tuple로 변환하여 insert_list에 추가
for i in range(5, 50):
    insert_list.append(tuple(df.iloc[i]))

# 데이터 타입 확인
df.dtypes  # 각 열의 데이터 타입 출력

# numpy에서 psycopg2로 데이터 전송 시 호환성을 위해 np.int64 데이터 타입을 SQL 타입으로 변환
import numpy as np
from psycopg2.extensions import register_adapter, AsIs
register_adapter(np.int64, AsIs)  # np.int64 타입을 SQL에서 처리 가능한 타입으로 등록

# psycopg2의 execute_values를 사용하여 효율적인 Bulk Insert 수행
from psycopg2.extras import execute_values

# INSERT 쿼리를 생성하고, insert_list에 있는 데이터를 일괄 삽입
sql = f"INSERT INTO lecture VALUES %s;"
execute_values(cursor, sql, insert_list)

# 변경사항을 데이터베이스에 반영
db.commit()

 

[파이썬에서 민감 정보 관리]

1) dotenv를 활용한 환경 변수 관리 및 보안 강화

  • dotenv : 파이썬에서 민감한 정보를 코드에 하드코딩하지 않고, .env 파일을 통해 관리할 수 있도록 도와주는 라이브러리
  • 데이터베이스 정보, API 키, 비밀번호 등의 민감 정보를 코드에 직접 작성하지X → .env 파일에서 읽어와 코드에서 사용할 수 있음

① .env 파일 생성

  • 보안이 필요한 환경 변수들을 저장
# .env
# 변동이 없을 변수는 대문자로 작성하고, 변수의 용도를 명확히 하기 위해 prefix를 붙임
# 예: 전역 변수에는 GLOBAL_ 같은 prefix를, 특정 모듈이나 클래스에서만 사용되는 상수에는 MODULE_, CLASS_ 같은 prefix를 사용 가능
# 여기서는 PostgreSQL 관련 상수에 POSTGRES_라는 prefix를 붙임

POSTGRES_ENGINE = 'postgresql'  # 데이터베이스 엔진 이름 설정 (여기서는 PostgreSQL 사용)
POSTGRES_USER = 'postgres'       # 데이터베이스 사용자 이름
POSTGRES_PASSWORD = '1234'       # 데이터베이스 비밀번호
POSTGRES_HOST = '127.0.0.1'      # 데이터베이스 호스트 주소 (로컬 호스트 IP)  # 'localhost' 대신 직접 IP 주소를 적는 이유: 'localhost'를 해석하는 과정에서 문제가 발생할 수 있으므로 명확한 IP 주소를 사용하여 에러 발생 가능성을 줄임
POSTGRES_PORT = '5432'           # PostgreSQL 기본 포트 번호
POSTGRES_DB = 'postgres'         # 데이터베이스 이름

 

② find_dotenv(): .env 파일 경로 찾기

# dotenv 라이브러리를 불러오기
import dotenv

# .env 파일의 경로 찾기
env_path = dotenv.find_dotenv()  # 현재 디렉토리나 상위 디렉토리에서 .env 파일을 찾음
print(env_path)  # 찾은 경로를 출력하여 확인

 

③ load_dotenv(): 환경 변수 로드

  • .env 파일에서 읽은 값을 시스템 환경 변수로 설정함; 추후 코드에서 os.getenv() 로 값을 사용할 수 있게 됨
# .env 파일에 저장된 환경 변수를 불러옴
dotenv.load_dotenv(env_path)

 

④ dotenv_values(): 환경 변수 값 확인

  • .env 파일의 내용을 딕셔너리 형태로 반환하여 내용 확인
# .env 파일의 내용을 딕셔너리 형태로 불러오기
dotenv.dotenv_values(env_path)  # .env 파일의 내용을 딕셔너리로 반환하여 확인

 

⑤ set_key(): 환경 변수 값 변경

  • .env 파일에서 특정 키의 값을 업데이트
# .env 파일의 환경 변수 값 변경하기
dotenv.set_key(env_path, 'HOST', '876543')  # .env 파일에서 'HOST'의 값을 '876543'으로 설정

 

⑥ os.getenv(): 환경 변수에서 값 불러오기

import os
from dotenv import load_dotenv

load_dotenv()

# 환경 변수에서 불러온 값을 변수에 저장
engine   = os.getenv("POSTGRES_ENGINE")
user     = os.getenv("POSTGRES_USER")
password = os.getenv("POSTGRES_PASSWORD")
host     = os.getenv("POSTGRES_HOST")
port     = os.getenv("POSTGRES_PORT")
database = os.getenv("POSTGRES_DB")

# 각 환경 변수 값 출력 확인
(engine, user, password, host, port, database)

 

⑦ DB 설정 딕셔너리 생성

# DB 설정 딕셔너리 생성
DB_SETTINGS = {  # DB_SETTINGS: 데이터베이스 설정 정보를 저장하는 딕셔너리
    "POSTGRES": {  # POSTGRES: PostgreSQL 데이터베이스와 관련된 설정 정보를 저장하는 키 
        'host': os.getenv("POSTGRES_HOST"),  # host, database, user,...: PostgreSQL에 연결할 때 필요한 환경 변수
        'database': os.getenv("POSTGRES_DB"),  # os.getenv(): .env 파일에서 로드된 환경 변수 값을 가져오는 함수. POSTGRES_HOST와 같은 키의 값을 불러옴
        'user': os.getenv("POSTGRES_USER"),
        'password': os.getenv("POSTGRES_PASSWORD"),
        'port': os.getenv("POSTGRES_PORT")
    }
}

# POSTGRES 설정 확인
DB_SETTINGS['POSTGRES']

 

⑧ DBConnector 인스턴스 생성

  • DBConnector 클래스의 객체 db_object 생성  
    • DB_SETTINGS['POSTGRES']에 저장된 PostgreSQL 설정을 **를 사용해 인자로 전달
    • 연결 정보가 포함된 딕셔너리를 가변 키워드 인자로 전달하여, PostgreSQL 데이터베이스에 연결하는 데 필요한 정보를 설정 
# DBConnector 클래스에 POSTGRES 설정을 unpacking 하여 전달
db_object = DBConnector(**DB_SETTINGS['POSTGRES'])

 

⑨ lecture 테이블의 데이터 조회

# 데이터베이스에 안전하게 연결 및 쿼리 실행
with db_object as connected:
     db_conn = connected.conn
     cursor = db_conn.cursor()
     cursor.execute("SELECT * FROM lecture LIMIT 5")
     print(cursor.fetchall())

 

➉ 다중 데이터베이스 설정

  • 다중 DB 설정을 DB_SETTINGS 딕셔너리로 관리; 여러 데이터베이스 전환이 가능
    • DB_SETTINGS 딕셔너리에 두 개의 설정(POSTGRES, KDT9)을 정의
    • 여러 데이터베이스를 쉽게 전환하거나, 연결할 수 있음
# 다중 DB 설정 추가
DB_SETTINGS = {
    "POSTGRES": {
        'host': os.getenv("POSTGRES_HOST"),
        'database': os.getenv("POSTGRES_DB"),
        'user': os.getenv("POSTGRES_USER"),
        'password': os.getenv("POSTGRES_PASSWORD"),
        'port': os.getenv("POSTGRES_PORT")
    },
    "KDT9": {
        'host': os.getenv("POSTGRES_HOST"),
        'database': os.getenv("POSTGRES_DB_2"),
        'user': os.getenv("POSTGRES_USER"),
        'password': os.getenv("POSTGRES_PASSWORD"),
        'port': os.getenv("POSTGRES_PORT")
    }
}

 

⑪ INFORMATION_SCHEMA.TABLES: 테이블 정보 조회

  • with 문 사용; __exit__ 메서드가 자동 호출되어 연결이 안전하게 종료됨
# POSTGRES 설정을 사용하여 DBConnector 인스턴스 생성 및 연결
db_object = DBConnector(**DB_SETTINGS['POSTGRES'])

# with문을 사용해 데이터베이스 연결, 쿼리 실행 후 연결 자동 종료
with db_object as connected:
    db_conn = connected.conn  # 연결된 DB 객체 가져오기
    cursor = db_conn.cursor()  # 커서 생성

    # 데이터베이스 내 테이블 정보 조회
    cursor.execute("SELECT * FROM INFORMATION_SCHEMA.TABLES")
    print(cursor.fetchall())  # 조회 결과 출력

 

[Asterisk(*)]

  • 애스터리스크 : 리스트, 딕셔너리 요소를 함수의 인자로 유연하게 전달할 수 있음

1) *args: 위치 인자를 가변적으로 전달

[tip] 위치 인자: 리스트, 튜플, 문자열, 셋, 숫자형 데이터
def print_args(*args):
    for arg in args:
        print(arg)

# 호출 예제
print_args([1, 2, 3], (4, 5, 6), "hello", {"apple", "banana"}, 42)

"""
[1, 2, 3]
(4, 5, 6)
hello
{'apple', 'banana'}
42
"""​
def func(*args):
    print(args)

func(1, 2, 3)  # 출력: (1, 2, 3)

my_list = [1, 2, 3]
func(*my_list)  # 리스트 요소들이 위치 인자로 전달되어 (1, 2, 3) 출력

 

2) **kwargs: 키워드 인자를 가변적으로 전달

[tip] 키워드 인자: 딕셔너리, 기본 자료형(숫자, 문자열, 불리언), 리스트, 튜플, 셋
def print_kwargs(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

# 호출 예제
print_kwargs(
    name="Alice",
    age=25,
    hobbies=["reading", "swimming"],
    coordinates=(10.0, 20.0),
    favorite_fruits={"apple", "banana"},
    address={"city": "Seoul", "country": "Korea"}
)

"""
name: Alice
age: 25
hobbies: ['reading', 'swimming']
coordinates: (10.0, 20.0)
favorite_fruits: {'apple', 'banana'}
address: {'city': 'Seoul', 'country': 'Korea'}
"""​
def func(**kwargs):
    print(kwargs)

func(one=1, two=2)  # 출력: {'one': 1, 'two': 2}

my_dict = {'host': 'localhost', 'port': 5432}
func(**my_dict)  # 키워드 인자로 전달되어 {'host': 'localhost', 'port': 5432} 출력

 

3) 응용

# 함수에 일반 인자를 전달하는 예시
def func(one, two):
    # 전달된 인자를 각각 출력
    print("one = ", one)
    print("two = ", two)

# 인자 1과 2를 전달하여 호출
func(1, 2)

# *args 예제 - 여러 개의 위치 인자를 가변적으로 처리
# *args는 여러 개의 위치 인자(순서가 있는 인자)를 전달할 때 사용
def func(*args):
    # args는 튜플 형태로 인식
    print("*args = ", args)
    # 전달된 인자의 합을 계산하여 출력
    print(sum(args))

# 위치 인자 1, 2, 3을 가변 인자로 전달
func(1, 2, 3)

# **kwargs 예제 - 여러 개의 키워드 인자를 가변적으로 처리
# **kwargs는 여러 개의 키워드 인자(이름이 있는 인자)를 딕셔너리 형태로 전달받을 때 사용
def func(**kwargs):
    # kwargs는 딕셔너리 형태로 인식
    print("**kwargs = ", kwargs)

# 키워드 인자 one=1, two=2를 전달
func(one=1, two=2)

# DBConnector 클래스 인스턴스 생성
# 인자를 직접 지정하여 인스턴스 생성
test = DBConnector(1, 2, 3, 4, 5)

# conn_params 확인 (인스턴스에 저장된 연결 정보 확인)
test.conn_params

# LIST 예제 - *args 사용
args = [1, 2, 3, 4, 5]  # 위치 인자 목록을 리스트로 정의

# *args를 사용하여 리스트의 요소를 위치 인자로 전달
list_params = DBConnector(*args)  # DBConnector(1, 2, 3, 4, 5)와 동일한 효과

# list_params의 conn_params 확인
list_params.conn_params

# DICT 예제 - **kwargs 사용
kwargs = dict(
    host=1,
    database=2,
    user=3,
    password=4,
    port=5
)

# **kwargs를 사용하여 딕셔너리의 키-값 쌍을 키워드 인자로 전달
dict_params = DBConnector(**kwargs)  # DBConnector(host=1, database=2, user=3, password=4, port=5)와 동일한 효과

# dict_params의 conn_params 확인
dict_params.conn_params

 

 

 

📙 내일 일정

  • SQLAlchemy ORM 실습


 
 
 

 

'TIL _Today I Learned > 2024.10' 카테고리의 다른 글

[DAY 70] SQLAlchemy ORM  (1) 2024.10.29
[DAY 69] SQLAlchemy ORM  (1) 2024.10.28
[DAY 67] AWS 아키텍처 그리기  (0) 2024.10.24
[DAY 66] AWS 클라우드 환경 이해 및 실습  (0) 2024.10.23
[DAY 65] AWS, Linux 란?  (2) 2024.10.22