
📌 Paradigm은 '본보기/틀/사고방식' 같은 의미.
프로그래밍에서 패러다임은 문제를 바라보고 해결하는 기본 관점이다.
[같은 퍼즐, 다른 전략] - A: 가장자리부터 맞춤 - B: 색깔별로 분류해서 맞춤 둘 다 퍼즐은 완성하지만 "접근 방식(패러다임)"이 다름
💡 즉, 패러다임은 "정답"이 아니라 사고의 프레임.
그런데 OOP와 RDB는 프레임이 아예 다르다 보니, 둘을 붙이면 여기저기서 삐걱거림이 생긴다.
🧠 OOP에서는 사용자를 객체로 만든 다음, 그 객체가 스스로 행동하게 만든다.
class User:
def __init__(self, user_id, name, email):
self.user_id = user_id # 속성
self.name = name # 속성
self.email = email # 속성
def change_email(self, new_email):
self.email = new_email
print(f"{self.name} 이메일 변경 완료: {new_email}")
# 객체 생성 + 행동 수행
jay = User(1, "kjjedd", "jay@gmail.com")
jay.change_email("new_jay@gmail.com")
✅ 핵심: 객체는 자기 상태(state)를 가지고, 자기 행동(behavior)으로 상태를 바꾼다
🗃️ RDB는 사용자를 "행(row)"으로 저장하고, 수정은 UPDATE 같은 명령(SQL)로 한다.
CREATE TABLE users (
user_id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
-- 데이터 삽입
INSERT INTO users VALUES (1, 'kjjedd', 'jay@gmail.com');
-- 이메일 변경 (외부에서 조작)
UPDATE users
SET email = 'new_jay@gmail.com'
WHERE user_id = 1;
✅ 핵심: DB는 데이터 중심이고, "행동"은 SQL로 외부에서 명령하는 구조.
💥 OOP와 RDB는 서로 다른 철학을 갖고 있어서, 데이터가 오갈 때 매번 변환/타협이 필요
[관점 차이 요약] OOP : "세상은 객체들의 네트워크다" → 참조/행동/캡슐화/그래프 탐색 RDB : "세상은 테이블들의 집합이다" → 행/컬럼/정규화/조인(JOIN)
그래서 문제가 생기는 대표 포인트들이 있고, 그중 첫 번째가 바로 정체성(Identity) 문제이다.
✅ OOP에서는 "메모리에서의 객체"가 정체성
✅ DB에서는 "Primary Key(주키)"가 정체성
OOP에서 아래 두 객체는 데이터가 같아도 기본적으로 다른 객체
class User:
def __init__(self, user_id, name, email):
self.user_id = user_id
self.name = name
self.email = email
user1 = User(1, "kjjedd", "jay@gmail.com")
user2 = User(1, "kjjedd", "jay@gmail.com")
print(user1 == user2) # 보통 False (동등성 비교를 구현 안 하면)
print(user1 is user2) # 무조건 False (메모리 주소가 다름)
🧠 여기서 중요한 포인트:
__eq__ 구현 여부에 따라 달라짐)💡 즉, OOP의 기본 정체성은 "PK"가 아니라 인스턴스 자체
DB에서는 user_id=1인 행은 원칙적으로 하나만 존재 가능.
SELECT *
FROM users
WHERE user_id = 1;
✅ DB 관점에서 정체성은 "PK가 뭐냐"로 결정됨
[상황] - DB에는 user_id=1 행이 딱 1개 있음 - 그런데 앱에서 조회를 2번 하면, 메모리에는 객체가 2개 생길 수 있음 DB (1 row) APP (2 objects) +------------------+ +---------------------+ | users | | userA (User #1) | | user_id = 1 |<---->| userB (User #1) | | email = ... | +---------------------+ +------------------+
😵 여기서 질문:
"수정은 누구를 기준으로 DB에 반영해야 하지?"
예를 들어 이런 일이 가능해진다.
- userA.email = "a@a.com" 으로 바꿈 - userB.email = "b@b.com" 으로 바꿈
둘 다 user_id=1인데 값이 다름
그럼 DB 최종값은 뭐가 돼야 함?
✅ 이게 바로 정체성 불일치에서 파생되는 대표적인 혼란.
🧩 "같은 요청/같은 작업 단위 안에서는, 같은 PK는 같은 객체(인스턴스)로 유지하는 게 편하다"
그래서 등장하는 아이디어가 이런 것들:
(이런 개념들을 자동으로 도와주는 대표 기술이 나중에 나오는 ORM 쪽으로 이어짐)
| 구분 | 객체지향(OOP) | 관계형 DB(RDB) | 충돌 포인트 |
|---|---|---|---|
| 정체성(Identity) | 메모리의 객체 인스턴스 | Primary Key(주키) | DB는 1개 행인데 앱은 객체가 여러 개 생김 |
객체지향은 현실을 잘게 쪼개 조립하는 방식이고, 관계형 DB는 테이블을 정규화해서 연결하는 방식이다.
이 둘이 만나면 “구조를 표현하는 방법”에서 충돌이 난다.
class Address:
def __init__(self, city: str, street: str, zipcode: str):
self.city = city
self.street = street
self.zipcode = zipcode
class User:
def __init__(self, user_id: int, name: str, address: Address):
self.user_id = user_id
self.name = name
self.address = address
home = Address("서울", "강남대로 123", "06000")
jay = User(1, "kjjedd", home)
print(jay.address.city) # "서울"
객체지향에서는 점(.)으로 그래프를 타고 내려가면 끝이다.
즉, “구조(객체의 포함 관계)”가 코드에 그대로 드러난다.
DB에서는 주소를 어떻게 저장해야 할까? 크게 2가지 선택지가 나온다.
CREATE TABLE users (
user_id INT PRIMARY KEY,
name VARCHAR(100),
city VARCHAR(100),
street VARCHAR(200),
zipcode VARCHAR(10)
);
CREATE TABLE addresses (
address_id INT PRIMARY KEY,
city VARCHAR(100),
street VARCHAR(200),
zipcode VARCHAR(10)
);
CREATE TABLE users (
user_id INT PRIMARY KEY,
name VARCHAR(100),
address_id INT REFERENCES addresses(address_id)
);
SELECT u.name, a.city
FROM users u
JOIN addresses a ON u.address_id = a.address_id
WHERE u.user_id = 1;
객체지향의 구조는 중첩(포함)이고, DB의 구조는 분해(정규화) + 연결(JOIN)이다.
둘의 철학이 다르니 변환 비용이 발생한다.
객체지향(중첩 구조) 관계형 DB(분해 + 연결)
-------------------------------- --------------------------------
User users
├─ user_id ├─ user_id (PK)
├─ name ├─ name
└─ address └─ address_id (FK)
├─ city
├─ street addresses
└─ zipcode ├─ address_id (PK)
├─ city
├─ street
└─ zipcode
핵심: 객체는 “안으로 품고”, DB는 “쪼개서 잇는다”. 구조가 다르니 매번 매핑이 필요하다.
객체지향은 상속으로 모델을 확장한다.
하지만 관계형 DB는 “상속”이라는 개념이 없다. 그래서 상속 구조를 테이블로 옮기려면 전략 선택이 필요하다.
class Member:
def __init__(self, member_id: int, name: str):
self.member_id = member_id
self.name = name
class RegularMember(Member):
def __init__(self, member_id: int, name: str, point: int):
super().__init__(member_id, name)
self.point = point
class VipMember(Member):
def __init__(self, member_id: int, name: str, discount_rate: float):
super().__init__(member_id, name)
self.discount_rate = discount_rate
| 전략 | 테이블 구성 | 장점 | 단점 |
|---|---|---|---|
| Single Table | members 한 장에 전부 | 조회 쉬움 | NULL 폭탄, 컬럼 혼란 |
| Class Table | 부모/자식 테이블 분리 | 정규화, 역할 분리 | JOIN 증가, 복잡 |
| Concrete Table | 자식마다 완전 별도 | 테이블 단순 | 전체 조회 어려움(UNION) |
CREATE TABLE members (
member_id INT PRIMARY KEY,
name VARCHAR(100),
member_type VARCHAR(20), -- 'REGULAR' or 'VIP'
point INT, -- Regular 전용 (VIP는 NULL)
discount_rate FLOAT -- VIP 전용 (Regular는 NULL)
);
문제는 NULL이 많아지고, “이 회원은 어떤 컬럼을 봐야 하는지”가 헷갈린다는 점이다.
CREATE TABLE members (
member_id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE regular_members (
member_id INT PRIMARY KEY REFERENCES members(member_id),
point INT
);
CREATE TABLE vip_members (
member_id INT PRIMARY KEY REFERENCES members(member_id),
discount_rate FLOAT
);
정규화는 좋아지지만 조회 시 JOIN이 늘어난다.
추가 핵심 개념: “상속”은 객체지향의 코드 재사용 방식인데, DB는 데이터 저장/정합성 중심이라 상속이 필요 없도록 설계됐다. 그래서 매핑은 늘 ‘타협’이 된다.
객체는 참조(Reference)로 연결되고, 참조에는 방향성이 있다.
DB는 외래키(FK) 하나로 양방향 JOIN이 가능하다. 철학이 다르다.
class Team:
def __init__(self, team_id: int, name: str):
self.team_id = team_id
self.name = name
class Member:
def __init__(self, member_id: int, name: str, team: Team):
self.member_id = member_id
self.name = name
self.team = team # Member -> Team 단방향 참조
여기서 Team에서 members로 역방향 탐색을 하려면? 직접 관리해야 한다.
class Team:
def __init__(self, team_id: int, name: str):
self.team_id = team_id
self.name = name
self.members = [] # Team -> Member 참조 추가
class Member:
def __init__(self, member_id: int, name: str, team: Team):
self.member_id = member_id
self.name = name
self.team = team
dev_team = Team(1, "개발팀")
jay = Member(1, "kjjedd", dev_team)
dev_team.members.append(jeff) # 양방향 동기화는 개발자 책임
양방향은 “편해 보이지만” 동기화 비용이 숨겨져 있다.
한쪽만 수정하면 다른 쪽이 깨진다. 즉, 일관성 유지 책임이 코드로 넘어온다.
CREATE TABLE teams (
team_id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE members (
member_id INT PRIMARY KEY,
name VARCHAR(100),
team_id INT REFERENCES teams(team_id)
);
-- Member -> Team
SELECT m.name, t.name
FROM members m
JOIN teams t ON m.team_id = t.team_id;
-- Team -> Members
SELECT t.name, m.name
FROM teams t
JOIN members m ON t.team_id = m.team_id;
객체: 참조 방향이 코드에 박힌다
DB : FK만 있으면 JOIN으로 양쪽 다 된다
객체지향은 객체 그래프를 마음껏 탐색한다.<
DB는 탐색할 때마다 “쿼리”를 설계해야 한다. 여기서 쿼리 수가 폭발할 씨앗이 생긴다.
order = get_order(100)
print(order.customer.name)
print(order.customer.address.city)
print(order.items[0].product.category.name)
코드만 보면 “그냥 접근”인데, 실제로 DB가 연결되어 있다면?
이 탐색이 쿼리 호출로 바뀔 수 있다.
-- 주문만
SELECT * FROM orders WHERE order_id = 100;
-- 고객도 필요하면 JOIN 추가
SELECT o.*, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_id = 100;
-- 주소까지 필요하면 JOIN 추가
SELECT o.*, c.name, a.city
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
JOIN addresses a ON c.address_id = a.address_id
WHERE o.order_id = 100;
members = get_members() # 쿼리 1번
for m in members:
print(m.team.name) # 멤버마다 팀을 조회 -> 멤버 수만큼 쿼리 추가
멤버가 100명이면?
- members 조회: 1번
- team 조회: 100번
총 101번 쿼리 = 1 + N
추가 핵심 개념: “탐색이 편한 코드”일수록 DB에서는 “조회 계획(쿼리)”이 숨어있을 수 있다. 객체지향의 탐색 방식이 그대로 DB 효율로 이어지지 않는다.
| 영역 | 객체지향 | 관계형 DB | 충돌 포인트 |
|---|---|---|---|
| 정체성 | 메모리의 객체 | PK로 식별되는 row | 같은 row가 여러 객체로 생길 수 있음 |
| 구조 | 포함/중첩(Composition) | 정규화 + JOIN | 객체 구조 ↔ 테이블 구조 변환 필요 |
| 상속 | 언어 차원 지원 | 개념 없음 | 매핑 전략 선택이 강제됨 |
| 연관 | 참조(방향성) | FK + JOIN(양방향 가능) | 양방향 동기화 책임이 개발자에게 옴 |
| 탐색 | 객체 그래프 탐색 | 쿼리 설계 | 쿼리 폭증(N+1) 가능 |
결국 애플리케이션은 DB에서 row를 가져와서 객체로 바꿔야 한다.
반대로 객체를 저장하려면 row로 쪼개 SQL로 넣어야 한다.
이걸 직접 하면 “수동 매핑 지옥”이 열린다.
def get_user_by_id(cursor, user_id: int):
cursor.execute(
"SELECT user_id, name, email FROM users WHERE user_id = %s",
(user_id,)
)
row = cursor.fetchone()
if not row:
return None
user = User(
user_id=row[0],
name=row[1],
email=row[2]
)
return user
def save_user(cursor, user: "User"):
cursor.execute(
"INSERT INTO users (user_id, name, email) VALUES (%s, %s, %s)",
(user.user_id, user.name, user.email)
)
추가 핵심 개념: 여기서 자연스럽게 “변환을 자동화하는 계층”이 필요해진다. 그게 ORM이 등장한 이유다.
객체지향과 관계형 DB는 태생이 다르다.
이 둘을 연결하려면 “매핑(변환)”이 필수다.
그리고 그 매핑을 사람이 직접 하면 유지보수 비용이 폭발한다.
객체는 탐색이 편하고,
DB는 쿼리가 강하다.
둘을 연결하려면 매핑이 필요하고,
매핑을 자동화하려고 ORM이 나온다.