Ver3.0 ๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋๊ณผ ORM ํ์ฉ์ ํตํ ํจ์จ์ ๋ฐ์ดํฐ ๊ด๋ฆฌ

๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋๊ณผ ORM ํ์ฉ์ ํตํ ํจ์จ์ ๋ฐ์ดํฐ ๊ด๋ฆฌ
์น๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋์ ์์์ผ ํ ๋ฐ์ดํฐ ๊ด๋ฆฌ์ ๋ชจ๋ ๊ฒ
ํน์ ์น์ฌ์ดํธ๋ฅผ ๋ง๋ค๋ค๊ฐ "์ฌ์ฉ์ ์ ๋ณด๋ ์ด๋์ ์ ์ฅํ์ง?", "๊ฒ์๊ธ ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ๊ด๋ฆฌํ์ง?" ๊ฐ์ ๊ณ ๋ฏผ ํด๋ณธ ์ ์์ด? ๋ฐ๋ก ์ด๋ด ๋ ํ์ํ ๊ฒ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ผ. ๊ทธ๋ฆฌ๊ณ ์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋ ์ฝ๊ณ ํจ์จ์ ์ผ๋ก ๋ค๋ฃจ๊ธฐ ์ํ ๋ง๋ฒ ๊ฐ์ ๋๊ตฌ๊ฐ ๋ฐ๋ก ORM(Object-Relational Mapping)์ด์ง!
์ด ๊ธ์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋์ ๊ธฐ์ด๋ถํฐ ORM ํ์ฉ๊น์ง, ์ค๋ฌด์์ ๋ฐ๋ก ์จ๋จน์ ์ ์๋ ์ง์๋ค์ ์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ์ค๋ช ํด์ค๊ฒ. ์ปคํผ ํ ์ ๋ค๊ณ ํธํ๊ฒ ์ฝ์ด๋ด! โ
๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค, ์ ํ์ํ ๊ฑธ๊น?
์ด๋ณด ๊ฐ๋ฐ์๋ค์ด ๊ฐ๋ ํ๋ ์ค์๊ฐ ์์ด. "๊ทธ๋ฅ ํ ์คํธ ํ์ผ์ ์ ์ฅํ๋ฉด ๋์ง ์๋์?" ๋ผ๊ณ ์๊ฐํ๋ ๊ฑฐ์ผ. ๋ฌผ๋ก ๊ฐ๋จํ ๋ฐ์ดํฐ๋ ํ์ผ๋ก๋ ๊ด๋ฆฌํ ์ ์์ด. ํ์ง๋ง ์ฌ์ฉ์๊ฐ 100๋ช , 1000๋ช , 10000๋ช ์ผ๋ก ๋์ด๋๋ฉด? ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํ๊ณ , ์์ ํ๊ณ , ์ญ์ ํ๋ ์์ ์ด ์์ฒญ๋๊ฒ ๋ณต์กํด์ง์ง.
โข ๋์ ์ ์ ์ฒ๋ฆฌ: ์ฌ๋ฌ ์ฌ์ฉ์๊ฐ ๋์์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ณ ์ธ ์ ์์ด
โข ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ: ์๋ชป๋ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋๋ ๊ฑธ ๋ฐฉ์ง
โข ๋ณด์: ๊ถํ ๊ด๋ฆฌ์ ์ํธํ๋ก ๋ฐ์ดํฐ ๋ณดํธ
โข ๋ฐฑ์ ๊ณผ ๋ณต๊ตฌ: ๋ฐ์ดํฐ ์์ค ๊ฑฑ์ ์์ด ์์ ํ๊ฒ ๊ด๋ฆฌ
๐๏ธ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ข ๋ฅ์ ํน์ง
๐ท ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค (RDBMS)
๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ๋ฐ์ดํฐ๋ฅผ ํ(ํ ์ด๋ธ) ํํ๋ก ์ ์ฅํด. ์์ ์ํธ๋ฅผ ๋ ์ฌ๋ฆฌ๋ฉด ์ดํดํ๊ธฐ ์ฌ์! ๊ฐ ํ ์ด๋ธ์ ํ(row)๊ณผ ์ด(column)๋ก ๊ตฌ์ฑ๋์ด ์๊ณ , ํ ์ด๋ธ ๊ฐ์ ๊ด๊ณ๋ฅผ ์ ์ํ ์ ์์ด.โข ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ์คํ์์ค ๋ฐ์ดํฐ๋ฒ ์ด์ค
โข ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋๋ฆฌ ์ฌ์ฉ
โข ๋น ๋ฅธ ์๋์ ์์ ์ฑ
โข ๋ฌด๋ฃ๋ก ์ฌ์ฉ ๊ฐ๋ฅ (MySQL Community Edition)
2. PostgreSQL
โข ๊ฐ๋ ฅํ ๊ธฐ๋ฅ๊ณผ ํ์ฅ์ฑ
โข ๋ณต์กํ ์ฟผ๋ฆฌ์ ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ์ ๊ฐํจ
โข JSON ๋ฐ์ดํฐ ํ์ ์ง์
โข ์คํ์์ค์ด๋ฉด์ ์ํฐํ๋ผ์ด์ฆ๊ธ ์ฑ๋ฅ
3. Oracle Database
โข ๋๊ธฐ์ ์์ ์ฃผ๋ก ์ฌ์ฉ
โข ์ต๊ณ ์์ค์ ์ฑ๋ฅ๊ณผ ๋ณด์
โข ๋น์ฉ์ด ๋์ง๋ง ๊ฐ๋ ฅํ ๊ธฐ๋ฅ ์ ๊ณต
4. Microsoft SQL Server
โข Windows ํ๊ฒฝ์ ์ต์ ํ
โข .NET ํ๋ ์์ํฌ์ ๊ถํฉ์ด ์ข์
โข ๊ฐ๋ ฅํ ๊ด๋ฆฌ ๋๊ตฌ ์ ๊ณต
5. SQLite
โข ํ์ผ ๊ธฐ๋ฐ์ ๊ฒฝ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค
โข ๋ณ๋ ์๋ฒ ์ค์น ๋ถํ์
โข ๋ชจ๋ฐ์ผ ์ฑ์ด๋ ์๊ท๋ชจ ํ๋ก์ ํธ์ ์ ํฉ
๐ถ NoSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค
NoSQL์ "Not Only SQL"์ ์ฝ์๋ก, ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฝ์์ ๋ฒ์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ผ. ํ ์ด๋ธ ๊ตฌ์กฐ๊ฐ ์๋ ๋ค์ํ ํํ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ ์ ์์ด. ๐ฆ์์: MongoDB, CouchDB
JSON ํํ๋ก ๋ฐ์ดํฐ ์ ์ฅ, ์ ์ฐํ ์คํค๋ง
โข Key-Value Store (ํค-๊ฐ ์ ์ฅ์)
์์: Redis, DynamoDB
๋น ๋ฅธ ์ฝ๊ธฐ/์ฐ๊ธฐ ์๋, ์บ์ฑ์ ์ ํฉ
โข Column-Family Store (์ปฌ๋ผํ)
์์: Cassandra, HBase
๋์ฉ๋ ๋ฐ์ดํฐ ๋ถ์ฐ ์ฒ๋ฆฌ์ ๊ฐํจ
โข Graph DB (๊ทธ๋ํํ)
์์: Neo4j, ArangoDB
๊ด๊ณ ์ค์ฌ ๋ฐ์ดํฐ ๋ชจ๋ธ๋ง์ ์ต์
โข ๋ฐ์ดํฐ ๊ฐ์ ๊ด๊ณ๊ฐ ๋ช ํํ๊ณ ์ค์ํ ๋
โข ํธ๋์ญ์ ์ฒ๋ฆฌ๊ฐ ์ค์ํ ๋ (์: ๊ธ์ต ์์คํ )
โข ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ด ์ต์ฐ์ ์ผ ๋
โข ๋ณต์กํ ์ฟผ๋ฆฌ๊ฐ ํ์ํ ๋
NoSQL์ ์ ํํ๋ ๊ฒฝ์ฐ:
โข ๋ฐ์ดํฐ ๊ตฌ์กฐ๊ฐ ์์ฃผ ๋ณ๊ฒฝ๋ ๋
โข ๋์ฉ๋ ๋ฐ์ดํฐ๋ฅผ ๋น ๋ฅด๊ฒ ์ฒ๋ฆฌํด์ผ ํ ๋
โข ์ํ์ ํ์ฅ(์ค์ผ์ผ ์์)์ด ํ์ํ ๋
โข ๋น์ ํ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃฐ ๋
๐ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋์ ๊ธฐ์ด
๐ฏ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋ ํ๋ก์ธ์ค
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋์ ํฌ๊ฒ 4๋จ๊ณ๋ก ์ด๋ฃจ์ด์ ธ:2๋จ๊ณ: ์ฟผ๋ฆฌ ์คํ (Query Execution)
SQL ๋ฌธ์ ์์ฑํ๊ณ ์คํํ๋ ๋จ๊ณ. SELECT, INSERT, UPDATE, DELETE ๋ฑ์ ๋ช ๋ น์ ์ฌ์ฉํด.
3๋จ๊ณ: ๊ฒฐ๊ณผ ์ฒ๋ฆฌ (Result Handling)
์ฟผ๋ฆฌ ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฉํ ์ ์๋ ํํ๋ก ๋ณํํด.
4๋จ๊ณ: ์ฐ๊ฒฐ ์ข ๋ฃ (Connection Close)
์์ ์ด ๋๋๋ฉด ์ฐ๊ฒฐ์ ๋ซ์์ ๋ฆฌ์์ค๋ฅผ ํด์ ํด์ผ ํด. ์ด๊ฑฐ ์ ํ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ๋ฐ์ํ ์ ์์ด! โ ๏ธ
๐ป ์ธ์ด๋ณ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋ ๋ฐฉ๋ฒ
๊ฐ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด๋ง๋ค ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฐ๋ํ๋ ๋ฐฉ๋ฒ์ด ์กฐ๊ธ์ฉ ๋ฌ๋ผ. ์ฃผ์ ์ธ์ด๋ค์ ์์๋ฅผ ์ดํด๋ณผ๊ฒ!๐ Python + MySQL ์ฐ๋
Python์์๋ ์ฃผ๋ก mysql-connector-python์ด๋ PyMySQL ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด.
import mysql.connector
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ
connection = mysql.connector.connect(
host='localhost',
user='your_username',
password='your_password',
database='your_database'
)
# ์ปค์ ์์ฑ
cursor = connection.cursor()
# ์ฟผ๋ฆฌ ์คํ
cursor.execute("SELECT * FROM users WHERE age > 20")
# ๊ฒฐ๊ณผ ๊ฐ์ ธ์ค๊ธฐ
results = cursor.fetchall()
for row in results:
print(row)
# ์ฐ๊ฒฐ ์ข
๋ฃ
cursor.close()
connection.close()
โ Java + MySQL ์ฐ๋Java์์๋ JDBC(Java Database Connectivity)๋ฅผ ์ฌ์ฉํด. JDBC๋ ์๋ฐ ํ์ค API๋ก, ๋ค์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฐ๋ํ ์ ์์ด.
import java.sql.*;
public class DatabaseExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/your_database";
String user = "your_username";
String password = "your_password";
try {
// ๋๋ผ์ด๋ฒ ๋ก๋
Class.forName("com.mysql.cj.jdbc.Driver");
// ์ฐ๊ฒฐ ์์ฑ
Connection conn = DriverManager.getConnection(url, user, password);
// Statement ์์ฑ
Statement stmt = conn.createStatement();
// ์ฟผ๋ฆฌ ์คํ
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
while(rs.next()) {
System.out.println(rs.getString("name"));
}
// ๋ฆฌ์์ค ํด์
rs.close();
stmt.close();
conn.close();
} catch(Exception e) {
e.printStackTrace();
}
}
}
๐จ JavaScript (Node.js) + MySQL ์ฐ๋Node.js์์๋ mysql์ด๋ mysql2 ํจํค์ง๋ฅผ ๋ง์ด ์ฌ์ฉํด.
const mysql = require('mysql2');
// ์ฐ๊ฒฐ ํ ์์ฑ (์ฌ๋ฌ ์ฐ๊ฒฐ์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌ)
const pool = mysql.createPool({
host: 'localhost',
user: 'your_username',
password: 'your_password',
database: 'your_database',
waitForConnections: true,
connectionLimit: 10
});
// Promise ๊ธฐ๋ฐ ์ฌ์ฉ
const promisePool = pool.promise();
async function getUsers() {
try {
const [rows] = await promisePool.query('SELECT * FROM users');
console.log(rows);
} catch(error) {
console.error(error);
}
}
getUsers();
๐ Ruby + PostgreSQL ์ฐ๋Ruby์์๋ pg gem์ ์ฌ์ฉํด์ PostgreSQL๊ณผ ์ฐ๋ํ ์ ์์ด.
require 'pg'
# ์ฐ๊ฒฐ ์์ฑ
conn = PG.connect(
host: 'localhost',
dbname: 'your_database',
user: 'your_username',
password: 'your_password'
)
# ์ฟผ๋ฆฌ ์คํ
result = conn.exec('SELECT * FROM users')
# ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
result.each do |row|
puts row['name']
end
# ์ฐ๊ฒฐ ์ข
๋ฃ
conn.close
โข ํ๊ฒฝ ๋ณ์ ์ฌ์ฉ: .env ํ์ผ์ ์ ์ฅํ๊ณ ํ๊ฒฝ ๋ณ์๋ก ๋ถ๋ฌ์ค๊ธฐ
โข ์ค์ ํ์ผ ๋ถ๋ฆฌ: config ํ์ผ์ ๋ง๋ค๊ณ .gitignore์ ์ถ๊ฐ
โข ์ํฌ๋ฆฟ ๊ด๋ฆฌ ๋๊ตฌ: AWS Secrets Manager, HashiCorp Vault ๋ฑ ํ์ฉ
โข ์ต์ ๊ถํ ์์น: ์ ํ๋ฆฌ์ผ์ด์ ์๋ ํ์ํ ์ต์ํ์ ๊ถํ๋ง ๋ถ์ฌ
๐ญ SQL ์ฟผ๋ฆฌ์ ๊ธฐ์ด
๐ ๊ธฐ๋ณธ SQL ๋ช ๋ น์ด
๊ฐ์ฅ ๋ง์ด ์ฌ์ฉํ๋ ๋ช ๋ น์ด์ผ. ๋ฐ์ดํฐ๋ฅผ ์ฝ์ด์ฌ ๋ ์ฌ์ฉํด.
-- ๋ชจ๋ ์ปฌ๋ผ ์กฐํ
SELECT * FROM users;
-- ํน์ ์ปฌ๋ผ๋ง ์กฐํ
SELECT name, email FROM users;
-- ์กฐ๊ฑด์ ์ถ๊ฐํ ์กฐํ
SELECT * FROM users WHERE age >= 20;
-- ์ ๋ ฌํด์ ์กฐํ
SELECT * FROM users ORDER BY created_at DESC;
-- ๊ฐ์ ์ ํ
SELECT * FROM users LIMIT 10;
-- ์ฌ๋ฌ ์กฐ๊ฑด ์กฐํฉ
SELECT name, email
FROM users
WHERE age >= 20 AND status = 'active'
ORDER BY name ASC
LIMIT 50;
์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ ๋ ์ฌ์ฉํด.
-- ๋จ์ผ ํ ์ฝ์
INSERT INTO users (name, email, age)
VALUES ('ํ๊ธธ๋', 'hong@example.com', 25);
-- ์ฌ๋ฌ ํ ๋์ ์ฝ์
INSERT INTO users (name, email, age)
VALUES
('๊น์ฒ ์', 'kim@example.com', 30),
('์ด์ํฌ', 'lee@example.com', 28),
('๋ฐ๋ฏผ์', 'park@example.com', 35);
๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ ๋ ์ฌ์ฉํด. WHERE ์ ์ ๊ผญ ์จ์ผ ํด! ์ ๊ทธ๋ฌ๋ฉด ๋ชจ๋ ๋ฐ์ดํฐ๊ฐ ๋ฐ๋์ด๋ฒ๋ ค! ๐ฑ
-- ํน์ ์ฌ์ฉ์์ ์ด๋ฉ์ผ ๋ณ๊ฒฝ
UPDATE users
SET email = 'newemail@example.com'
WHERE id = 1;
-- ์ฌ๋ฌ ์ปฌ๋ผ ๋์ ์์
UPDATE users
SET age = 26, status = 'active'
WHERE name = 'ํ๊ธธ๋';
-- ์กฐ๊ฑด์ ๋ง๋ ์ฌ๋ฌ ํ ์์
UPDATE users
SET status = 'inactive'
WHERE last_login < '2023-01-01';
๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ ๋ ์ฌ์ฉํด. ์ด๊ฒ๋ WHERE ์ ํ์! โ ๏ธ
-- ํน์ ์ฌ์ฉ์ ์ญ์
DELETE FROM users WHERE id = 1;
-- ์กฐ๊ฑด์ ๋ง๋ ์ฌ๋ฌ ํ ์ญ์
DELETE FROM users WHERE status = 'deleted';
-- ๋ชจ๋ ๋ฐ์ดํฐ ์ญ์ (์ํ!)
DELETE FROM users;
๐ JOIN - ํ ์ด๋ธ ๊ฒฐํฉ
์ค๋ฌด์์๋ ์ฌ๋ฌ ํ ์ด๋ธ์ ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ์กฐํํด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์. ์ด๋ JOIN์ ์ฌ์ฉํด!โข LEFT JOIN: ์ผ์ชฝ ํ ์ด๋ธ์ ๋ชจ๋ ๋ฐ์ดํฐ์ ๋งค์นญ๋๋ ์ค๋ฅธ์ชฝ ๋ฐ์ดํฐ ์กฐํ
โข RIGHT JOIN: ์ค๋ฅธ์ชฝ ํ ์ด๋ธ์ ๋ชจ๋ ๋ฐ์ดํฐ์ ๋งค์นญ๋๋ ์ผ์ชฝ ๋ฐ์ดํฐ ์กฐํ
โข FULL OUTER JOIN: ์์ชฝ ํ ์ด๋ธ์ ๋ชจ๋ ๋ฐ์ดํฐ ์กฐํ
-- INNER JOIN ์์
-- ์ฌ์ฉ์์ ๊ทธ๋ค์ ์ฃผ๋ฌธ ์ ๋ณด๋ฅผ ํจ๊ป ์กฐํ
SELECT users.name, orders.order_date, orders.total_amount
FROM users
INNER JOIN orders ON users.id = orders.user_id;
-- LEFT JOIN ์์
-- ๋ชจ๋ ์ฌ์ฉ์์ ์ฃผ๋ฌธ ์ ๋ณด ์กฐํ (์ฃผ๋ฌธ์ด ์๋ ์ฌ์ฉ์๋ ํฌํจ)
SELECT users.name, orders.order_date
FROM users
LEFT JOIN orders ON users.id = orders.user_id;
-- ์ฌ๋ฌ ํ
์ด๋ธ JOIN
SELECT
users.name,
orders.order_date,
products.product_name,
order_items.quantity
FROM users
INNER JOIN orders ON users.id = orders.user_id
INNER JOIN order_items ON orders.id = order_items.order_id
INNER JOIN products ON order_items.product_id = products.id;
๐ ์ง๊ณ ํจ์
๋ฐ์ดํฐ๋ฅผ ๋ถ์ํ ๋ ์ ์ฉํ ์ง๊ณ ํจ์๋ค์ด์ผ.-- COUNT: ๊ฐ์ ์ธ๊ธฐ
SELECT COUNT(*) FROM users;
SELECT COUNT(DISTINCT email) FROM users;
-- SUM: ํฉ๊ณ
SELECT SUM(total_amount) FROM orders;
-- AVG: ํ๊ท
SELECT AVG(age) FROM users;
-- MAX, MIN: ์ต๋๊ฐ, ์ต์๊ฐ
SELECT MAX(total_amount), MIN(total_amount) FROM orders;
-- GROUP BY: ๊ทธ๋ฃน๋ณ ์ง๊ณ
SELECT status, COUNT(*) as count
FROM users
GROUP BY status;
-- HAVING: ๊ทธ๋ฃน ์กฐ๊ฑด
SELECT status, COUNT(*) as count
FROM users
GROUP BY status
HAVING count > 10;
โข SELECT * ์ง์: ํ์ํ ์ปฌ๋ผ๋ง ๋ช ์์ ์ผ๋ก ์ ํ
โข WHERE ์ ์ต์ ํ: ์ธ๋ฑ์ค๋ฅผ ํ์ฉํ ์ ์๋ ์กฐ๊ฑด ์์ฑ
โข LIMIT ์ฌ์ฉ: ํ์ํ ๋งํผ๋ง ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
โข ์๋ธ์ฟผ๋ฆฌ๋ณด๋ค JOIN: ๊ฐ๋ฅํ๋ฉด JOIN ์ฌ์ฉ์ด ๋ ํจ์จ์
โข ์ฟผ๋ฆฌ ์คํ ๊ณํ ํ์ธ: EXPLAIN ๋ช ๋ น์ด๋ก ์ฟผ๋ฆฌ ๋ถ์
๐จ ORM์ด๋ ๋ฌด์์ธ๊ฐ?
ORM(Object-Relational Mapping)์ ๊ฐ์ฒด ์งํฅ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ด์ ๋ค๋ฆฌ ์ญํ ์ ํด. ์ฝ๊ฒ ๋งํ๋ฉด, SQL์ ์ง์ ์์ฑํ์ง ์๊ณ ๋ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์ ๊ฐ์ฒด๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋ค๋ฃฐ ์ ์๊ฒ ํด์ฃผ๋ ๊ธฐ์ ์ด์ผ! ๐
๐ค ์ ORM์ ์ฌ์ฉํ ๊น?
SQL์ ์ง์ ์์ฑํ๋ ๊ฒ๋ ๋์์ง ์์๋ฐ, ์ ORM์ ์ฌ์ฉํ๋ ๊ฑธ๊น? ์ฌ๋ฌ ์ด์ ๊ฐ ์์ด:2. ์ ์ง๋ณด์ ์ฉ์ด ๐ง
๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๊ฐ ๋ณ๊ฒฝ๋์ด๋ ORM ๋ชจ๋ธ๋ง ์์ ํ๋ฉด ๋ผ. SQL ์ฟผ๋ฆฌ๋ฅผ ์ผ์ผ์ด ์ฐพ์์ ์์ ํ ํ์๊ฐ ์์ด.
3. ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ ๋ฆฝ์ฑ ๐
MySQL์์ PostgreSQL๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋ณ๊ฒฝํด๋ ์ฝ๋๋ฅผ ๊ฑฐ์ ์์ ํ์ง ์์๋ ๋ผ. ORM์ด ์์์ ์ ์ ํ SQL์ ์์ฑํด์ฃผ๊ฑฐ๋ !
4. ๊ฐ์ฒด ์งํฅ์ ์ ๊ทผ ๐ฏ
๋ฐ์ดํฐ๋ฅผ ํ ์ด๋ธ์ด ์๋ ๊ฐ์ฒด๋ก ๋ค๋ฃจ๋๊น ๋ ์ง๊ด์ ์ด๊ณ ์ดํดํ๊ธฐ ์ฌ์.
5. SQL ์ธ์ ์ ๋ฐฉ์ด ๐ก๏ธ
ORM์ด ์๋์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๊ณ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฒ๋ฆฌํด์ SQL ์ธ์ ์ ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ ์ ์์ด.
โ๏ธ ORM์ ์ฅ๋จ์
๋ฌผ๋ก ORM๋ ๋ง๋ฅ์ ์๋์ผ. ์ฅ๋จ์ ์ ์ ํํ ์๊ณ ์ฌ์ฉํด์ผ ํด!โข ์ฝ๋ ์ฌ์ฌ์ฉ์ฑ์ด ๋์
โข ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณ๊ฒฝ์ด ์ฉ์ด
โข ๋น์ฆ๋์ค ๋ก์ง์ ์ง์ค ๊ฐ๋ฅ
โข ํ์ ์์ ์ฑ ์ ๊ณต (์ปดํ์ผ ํ์ ์๋ฌ ์ฒดํฌ)
โข ์๋ ๋ง์ด๊ทธ๋ ์ด์ ์ง์
โ ๋จ์
โข ๋ณต์กํ ์ฟผ๋ฆฌ๋ ์คํ๋ ค ์ด๋ ค์ธ ์ ์์
โข ์ฑ๋ฅ ์ค๋ฒํค๋ ๋ฐ์ ๊ฐ๋ฅ
โข ORM ์์ฒด๋ฅผ ํ์ตํด์ผ ํจ
โข ์์ฑ๋ SQL์ ์ ์ดํ๊ธฐ ์ด๋ ค์
โข ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ์ ๋นํจ์จ์ ์ผ ์ ์์
โข ์ผ๋ฐ์ ์ธ CRUD: ORM ์ฌ์ฉ
โข ๋ณต์กํ ์ง๊ณ/ํต๊ณ: Raw SQL ์ฌ์ฉ
โข ๋์ฉ๋ ๋ฐฐ์น ์์ : Raw SQL ๋๋ ๋ฒํฌ ์ฐ์ฐ
โข ์ฑ๋ฅ์ด ์ค์ํ ๋ถ๋ถ: ์ฟผ๋ฆฌ ์ต์ ํ ํ Raw SQL ๊ณ ๋ ค
๐ ์ฃผ์ ORM ํ๋ ์์ํฌ
๊ฐ ์ธ์ด๋ง๋ค ์ธ๊ธฐ ์๋ ORM ํ๋ ์์ํฌ๊ฐ ์์ด. ๋ํ์ ์ธ ๊ฒ๋ค์ ์ดํด๋ณผ๊ฒ!๐ Python
โข ๊ฐ๋ ฅํ๊ณ ์ฌ์ฉํ๊ธฐ ์ฌ์
โข ์๋ ๋ง์ด๊ทธ๋ ์ด์ ์ง์
โข Admin ํจ๋ ์๋ ์์ฑ
2. SQLAlchemy
โข ๊ฐ์ฅ ๊ฐ๋ ฅํ๊ณ ์ ์ฐํ Python ORM
โข Core์ ORM ๋ ๊ฐ์ง ๋ ๋ฒจ ์ ๊ณต
โข ๋ณต์กํ ์ฟผ๋ฆฌ ์์ฑ ๊ฐ๋ฅ
โข Flask, FastAPI ๋ฑ๊ณผ ์ ์ด์ธ๋ฆผ
3. Peewee
โข ๊ฒฝ๋ ORM
โข ๊ฐ๋จํ API
โข ์์ ํ๋ก์ ํธ์ ์ ํฉ
โข JPA(Java Persistence API) ๊ตฌํ์ฒด
โข ๊ฐ๋ ฅํ ์บ์ฑ ๊ธฐ๋ฅ
โข ์ํฐํ๋ผ์ด์ฆ๊ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉ
2. MyBatis
โข SQL ๋งคํผ ํ๋ ์์ํฌ
โข SQL์ ์ง์ ์์ฑํ๋ ๊ฐ์ฒด ๋งคํ ์๋ํ
โข ๋ณต์กํ ์ฟผ๋ฆฌ ์์ฑ์ ์ ๋ฆฌ
3. JPA (Spring Data JPA)
โข Spring Boot์ ์๋ฒฝํ ํตํฉ
โข Repository ํจํด ์ง์
โข ๋ฉ์๋ ์ด๋ฆ์ผ๋ก ์ฟผ๋ฆฌ ์๋ ์์ฑ
โข Promise ๊ธฐ๋ฐ
โข ๋ค์ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ง์
โข ๋ง์ด๊ทธ๋ ์ด์ ๋๊ตฌ ํฌํจ
2. TypeORM
โข TypeScript์ ์๋ฒฝํ ํธํ
โข ๋ฐ์ฝ๋ ์ดํฐ ๊ธฐ๋ฐ ๋ฌธ๋ฒ
โข Active Record์ Data Mapper ํจํด ์ง์
โข NestJS์ ์ ์ด์ธ๋ฆผ
3. Prisma
โข ์ฐจ์ธ๋ ORM
โข ํ์ ์์ ์ฑ์ด ๋ฐ์ด๋จ
โข ์ง๊ด์ ์ธ ์คํค๋ง ์ ์
โข ์๋ ์์ฑ ์ง์์ด ํ๋ฅญํจ
โข Convention over Configuration
โข ๋งค์ฐ ์ง๊ด์ ์ธ API
โข ๋ง์ด๊ทธ๋ ์ด์ ์์คํ ๋ด์ฅ
โข ์ฐ์ํ ๋ฌธ๋ฒ
โข ๊ด๊ณ ์ ์๊ฐ ์ฌ์
2. Doctrine
โข Symfony์ ๊ธฐ๋ณธ ORM
โข ๊ฐ๋ ฅํ๊ณ ์ ์ฐํจ
โข ์ํฐํ๋ผ์ด์ฆ๊ธ ๊ธฐ๋ฅ
๐ ORM ์ค์ ํ์ฉ - Django ORM
๐ ๋ชจ๋ธ ์ ์ํ๊ธฐ
Django์์๋ Python ํด๋์ค๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ ์ด๋ธ์ ์ ์ํด. ์ด๊ฑธ ๋ชจ๋ธ(Model)์ด๋ผ๊ณ ๋ถ๋ฌ.from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
"""์นดํ
๊ณ ๋ฆฌ ๋ชจ๋ธ"""
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name_plural = "Categories"
ordering = ['name']
def __str__(self):
return self.name
class Post(models.Model):
"""๊ฒ์๊ธ ๋ชจ๋ธ"""
STATUS_CHOICES = [
('draft', '์์์ ์ฅ'),
('published', '๋ฐํ๋จ'),
('archived', '๋ณด๊ด๋จ'),
]
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='posts')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
views = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', '-created_at']),
]
def __str__(self):
return self.title
class Comment(models.Model):
"""๋๊ธ ๋ชจ๋ธ"""
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f'{self.author.username}: {self.content[:50]}'
์ด๋ ๊ฒ ๋ชจ๋ธ์ ์ ์ํ๋ฉด, Django๊ฐ ์๋์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ
์ด๋ธ์ ์์ฑํด์ค! ๐๐ ๋ง์ด๊ทธ๋ ์ด์
๋ชจ๋ธ์ ์ ์ํ์ผ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ค์ ๋ก ํ ์ด๋ธ์ ๋ง๋ค์ด์ผ ํด. ์ด ๊ณผ์ ์ ๋ง์ด๊ทธ๋ ์ด์ ์ด๋ผ๊ณ ํด.# ๋ง์ด๊ทธ๋ ์ด์
ํ์ผ ์์ฑ
python manage.py makemigrations
# ๋ง์ด๊ทธ๋ ์ด์
์คํ (ํ
์ด๋ธ ์์ฑ)
python manage.py migrate
โ๏ธ ๋ฐ์ดํฐ ์์ฑ (Create)
# ๋ฐฉ๋ฒ 1: ๊ฐ์ฒด ์์ฑ ํ ์ ์ฅ
post = Post(
title='Django ORM ์๋ฒฝ ๊ฐ์ด๋',
content='ORM์ ์ฌ์ฉํ๋ฉด ์ ๋ง ํธ๋ฆฌํด์!',
author=user,
category=category,
status='published'
)
post.save()
# ๋ฐฉ๋ฒ 2: create() ๋ฉ์๋ ์ฌ์ฉ (ํ ๋ฒ์ ์์ฑํ๊ณ ์ ์ฅ)
post = Post.objects.create(
title='Python ์น ๊ฐ๋ฐ',
content='Django๋ก ์น์ฌ์ดํธ ๋ง๋ค๊ธฐ',
author=user,
category=category
)
# ์ฌ๋ฌ ๊ฐ ํ ๋ฒ์ ์์ฑ
Post.objects.bulk_create([
Post(title='์ฒซ ๋ฒ์งธ ๊ธ', content='๋ด์ฉ1', author=user),
Post(title='๋ ๋ฒ์งธ ๊ธ', content='๋ด์ฉ2', author=user),
Post(title='์ธ ๋ฒ์งธ ๊ธ', content='๋ด์ฉ3', author=user),
])
๐ ๋ฐ์ดํฐ ์กฐํ (Read)
Django ORM์ ์ง๊ฐ๋ ๋ฐ์ดํฐ ์กฐํ์์ ๋ํ๋! ์ ๋ง ๋ค์ํ ๋ฐฉ๋ฒ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด.# ๋ชจ๋ ๊ฒ์๊ธ ๊ฐ์ ธ์ค๊ธฐ
all_posts = Post.objects.all()
# ํน์ ์กฐ๊ฑด์ผ๋ก ํํฐ๋ง
published_posts = Post.objects.filter(status='published')
# ์ฌ๋ฌ ์กฐ๊ฑด ์กฐํฉ
recent_posts = Post.objects.filter(
status='published',
created_at__gte='2024-01-01'
).order_by('-created_at')
# ๋จ์ผ ๊ฐ์ฒด ๊ฐ์ ธ์ค๊ธฐ
post = Post.objects.get(id=1)
# ์กด์ฌํ์ง ์์ ์ ์๋ ๊ฒฝ์ฐ
try:
post = Post.objects.get(id=999)
except Post.DoesNotExist:
post = None
# ๋๋ get_or_404 ์ฌ์ฉ (Django shortcuts)
from django.shortcuts import get_object_or_404
post = get_object_or_404(Post, id=1)
# ์ฒซ ๋ฒ์งธ/๋ง์ง๋ง ๊ฐ์ฒด
first_post = Post.objects.first()
last_post = Post.objects.last()
# ๊ฐ์ ์ ํ
latest_5_posts = Post.objects.all()[:5]
# ํน์ ํ๋๋ง ๊ฐ์ ธ์ค๊ธฐ (์ฑ๋ฅ ์ต์ ํ)
posts = Post.objects.values('id', 'title', 'created_at')
# ์ค๋ณต ์ ๊ฑฐ
categories = Post.objects.values('category').distinct()
# ๊ฐ์ ์ธ๊ธฐ
total_posts = Post.objects.count()
published_count = Post.objects.filter(status='published').count()
# ์กด์ฌ ์ฌ๋ถ ํ์ธ
has_posts = Post.objects.filter(author=user).exists()
๐ ๊ณ ๊ธ ์ฟผ๋ฆฌ
# Q ๊ฐ์ฒด๋ก ๋ณต์กํ ์กฐ๊ฑด ๋ง๋ค๊ธฐ
from django.db.models import Q
# OR ์กฐ๊ฑด
posts = Post.objects.filter(
Q(status='published') | Q(status='archived')
)
# AND์ OR ์กฐํฉ
posts = Post.objects.filter(
Q(status='published') & (Q(title__contains='Django') | Q(content__contains='Django'))
)
# NOT ์กฐ๊ฑด
posts = Post.objects.filter(~Q(status='draft'))
# ๊ด๊ณ๋ฅผ ํตํ ์กฐํ (JOIN)
# ํน์ ์นดํ
๊ณ ๋ฆฌ์ ๊ฒ์๊ธ
posts = Post.objects.filter(category__name='Python')
# ํน์ ์์ฑ์์ ๊ฒ์๊ธ
posts = Post.objects.filter(author__username='john')
# ๋๊ธ์ด ์๋ ๊ฒ์๊ธ
posts = Post.objects.filter(comments__isnull=False).distinct()
# ์ญ์ฐธ์กฐ (related_name ์ฌ์ฉ)
user_posts = user.posts.all()
category_posts = category.posts.filter(status='published')
# ์ง๊ณ ํจ์
from django.db.models import Count, Avg, Sum, Max, Min
# ๊ฐ ์นดํ
๊ณ ๋ฆฌ๋ณ ๊ฒ์๊ธ ์
categories = Category.objects.annotate(post_count=Count('posts'))
# ํ๊ท ์กฐํ์
avg_views = Post.objects.aggregate(Avg('views'))
# ์์ฑ์๋ณ ๊ฒ์๊ธ ์
authors = User.objects.annotate(
post_count=Count('posts'),
total_views=Sum('posts__views')
)
# ์กฐ๊ฑด๋ถ ์ง๊ณ
from django.db.models import Case, When, IntegerField
posts = Post.objects.annotate(
comment_count=Count('comments'),
is_popular=Case(
When(views__gte=1000, then=1),
default=0,
output_field=IntegerField()
)
)
โ๏ธ ๋ฐ์ดํฐ ์์ (Update)
# ๋ฐฉ๋ฒ 1: ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์์ ์์
post = Post.objects.get(id=1)
post.title = '์์ ๋ ์ ๋ชฉ'
post.content = '์์ ๋ ๋ด์ฉ'
post.save()
# ๋ฐฉ๋ฒ 2: update() ๋ฉ์๋ (๋ ํจ์จ์ )
Post.objects.filter(id=1).update(
title='์์ ๋ ์ ๋ชฉ',
content='์์ ๋ ๋ด์ฉ'
)
# ์ฌ๋ฌ ๊ฐ์ฒด ํ ๋ฒ์ ์์
Post.objects.filter(status='draft').update(status='published')
# ์กฐํ์ ์ฆ๊ฐ
post = Post.objects.get(id=1)
post.views += 1
post.save()
# ๋๋ F ๊ฐ์ฒด ์ฌ์ฉ (๊ฒฝ์ ์กฐ๊ฑด ๋ฐฉ์ง)
from django.db.models import F
Post.objects.filter(id=1).update(views=F('views') + 1)
# ํน์ ํ๋๋ง ์ ์ฅ (์ฑ๋ฅ ์ต์ ํ)
post.title = '์ ์ ๋ชฉ'
post.save(update_fields=['title'])
๐๏ธ ๋ฐ์ดํฐ ์ญ์ (Delete)
# ๋จ์ผ ๊ฐ์ฒด ์ญ์
post = Post.objects.get(id=1)
post.delete()
# ์กฐ๊ฑด์ ๋ง๋ ์ฌ๋ฌ ๊ฐ์ฒด ์ญ์
Post.objects.filter(status='draft', created_at__lt='2023-01-01').delete()
# ๋ชจ๋ ๊ฐ์ฒด ์ญ์ (์ฃผ์!)
Post.objects.all().delete()
โข ๋๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ: bulk_create(), bulk_update() ์ฌ์ฉ
โข ํธ๋์ญ์ : ์ฌ๋ฌ ์์ ์ ์์์ ์ผ๋ก ์ฒ๋ฆฌํด์ผ ํ ๋๋ transaction.atomic() ์ฌ์ฉ
๐ฏ ์ฑ๋ฅ ์ต์ ํ ํ
# select_related: ForeignKey, OneToOneField ๊ด๊ณ (JOIN ์ฌ์ฉ)
posts = Post.objects.select_related('author', 'category').all()
# prefetch_related: ManyToManyField, ์ญ์ฐธ์กฐ ๊ด๊ณ (๋ณ๋ ์ฟผ๋ฆฌ)
posts = Post.objects.prefetch_related('comments').all()
# ๋ ๋ค ์ฌ์ฉ
posts = Post.objects.select_related('author', 'category').prefetch_related('comments')
# only: ํน์ ํ๋๋ง ๊ฐ์ ธ์ค๊ธฐ
posts = Post.objects.only('id', 'title', 'created_at')
# defer: ํน์ ํ๋ ์ ์ธํ๊ณ ๊ฐ์ ธ์ค๊ธฐ
posts = Post.objects.defer('content')
# iterator: ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ (๋ฉ๋ชจ๋ฆฌ ์ ์ฝ)
for post in Post.objects.iterator(chunk_size=1000):
# ์ฒ๋ฆฌ ๋ก์ง
pass
# ์ฟผ๋ฆฌ ํ์ธ (๋๋ฒ๊น
)
print(Post.objects.filter(status='published').query)
๐ ORM ์ค์ ํ์ฉ - SQLAlchemy
๐๏ธ ๊ธฐ๋ณธ ์ค์
from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from datetime import datetime
# ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ง ์์ฑ
engine = create_engine('mysql+pymysql://user:password@localhost/dbname', echo=True)
# Base ํด๋์ค ์์ฑ
Base = declarative_base()
# ์ธ์
ํฉํ ๋ฆฌ ์์ฑ
Session = sessionmaker(bind=engine)
๐ ๋ชจ๋ธ ์ ์
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), unique=True, nullable=False)
email = Column(String(100), unique=True, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
# ๊ด๊ณ ์ ์
posts = relationship('Post', back_populates='author', cascade='all, delete-orphan')
def __repr__(self):
return f"<User(username='{self.username}')>"
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
content = Column(Text)
author_id = Column(Integer, ForeignKey('users.id'))
created_at = Column(DateTime, default=datetime.utcnow)
# ๊ด๊ณ ์ ์
author = relationship('User', back_populates='posts')
comments = relationship('Comment', back_populates='post', cascade='all, delete-orphan')
def __repr__(self):
return f"<Post(title='{self.title}')>"
class Comment(Base):
__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
content = Column(Text, nullable=False)
post_id = Column(Integer, ForeignKey('posts.id'))
user_id = Column(Integer, ForeignKey('users.id'))
created_at = Column(DateTime, default=datetime.utcnow)
# ๊ด๊ณ ์ ์
post = relationship('Post', back_populates='comments')
user = relationship('User')
def __repr__(self):
return f"<Comment(content='{self.content[:20]}...')>"
# ํ
์ด๋ธ ์์ฑ
Base.metadata.create_all(engine)
โ๏ธ CRUD ์์
# ์ธ์
์์ฑ
session = Session()
# Create - ๋ฐ์ดํฐ ์์ฑ
user = User(username='john_doe', email='john@example.com')
session.add(user)
session.commit()
# ์ฌ๋ฌ ๊ฐ ์ถ๊ฐ
users = [
User(username='alice', email='alice@example.com'),
User(username='bob', email='bob@example.com')
]
session.add_all(users)
session.commit()
# Read - ๋ฐ์ดํฐ ์กฐํ
# ๋ชจ๋ ์ฌ์ฉ์
all_users = session.query(User).all()
# ํน์ ์กฐ๊ฑด
user = session.query(User).filter_by(username='john_doe').first()
# ๋ณต์กํ ์กฐ๊ฑด
from sqlalchemy import and_, or_
users = session.query(User).filter(
and_(
User.username.like('%john%'),
User.created_at > '2024-01-01'
)
).all()
# ์ ๋ ฌ
users = session.query(User).order_by(User.created_at.desc()).all()
# ์ ํ
users = session.query(User).limit(10).all()
# ๊ฐ์
user_count = session.query(User).count()
# Update - ๋ฐ์ดํฐ ์์
user = session.query(User).filter_by(username='john_doe').first()
user.email = 'newemail@example.com'
session.commit()
# ๋๋ update() ์ฌ์ฉ
session.query(User).filter_by(username='john_doe').update({
'email': 'newemail@example.com'
})
session.commit()
# Delete - ๋ฐ์ดํฐ ์ญ์
user = session.query(User).filter_by(username='john_doe').first()
session.delete(user)
session.commit()
# ๋๋ delete() ์ฌ์ฉ
session.query(User).filter_by(username='john_doe').delete()
session.commit()
# ์ธ์
์ข
๋ฃ
session.close()
๐ ๊ด๊ณ ํ์ฉ
# ๊ฒ์๊ธ๊ณผ ์์ฑ์ ํจ๊ป ์กฐํ (JOIN)
from sqlalchemy.orm import joinedload
posts = session.query(Post).options(
joinedload(Post.author)
).all()
# ์ฌ์ฉ์์ ๋ชจ๋ ๊ฒ์๊ธ
user = session.query(User).filter_by(username='john_doe').first()
user_posts = user.posts
# ์ญ์ฐธ์กฐ
post = session.query(Post).first()
author = post.author
# ์ค์ฒฉ ๊ด๊ณ
posts_with_comments = session.query(Post).options(
joinedload(Post.author),
joinedload(Post.comments)
).all()
๐ ๊ณ ๊ธ ์ฟผ๋ฆฌ
from sqlalchemy import func
# ์ง๊ณ
# ์ฌ์ฉ์๋ณ ๊ฒ์๊ธ ์
results = session.query(
User.username,
func.count(Post.id).label('post_count')
).join(Post).group_by(User.username).all()
# ํ๊ท , ์ต๋, ์ต์
avg_posts = session.query(func.avg(func.count(Post.id))).group_by(Post.author_id).scalar()
# ์๋ธ์ฟผ๋ฆฌ
from sqlalchemy import select
subq = select([func.count(Post.id)]).where(Post.author_id == User.id).label('post_count')
users_with_count = session.query(User, subq).all()
# ์๋์ฐ ํจ์
from sqlalchemy import over
ranked_posts = session.query(
Post,
func.row_number().over(
partition_by=Post.author_id,
order_by=Post.created_at.desc()
).label('rank')
).all()
๐ Node.js ORM - Sequelize & Prisma
๐ท Sequelize
Sequelize๋ Node.js์์ ๊ฐ์ฅ ์ค๋๋๊ณ ์ฑ์ํ ORM์ด์ผ.const { Sequelize, DataTypes } = require('sequelize');
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ
const sequelize = new Sequelize('database', 'username', 'password', {
host: 'localhost',
dialect: 'mysql',
logging: false
});
// ๋ชจ๋ธ ์ ์
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
age: {
type: DataTypes.INTEGER,
validate: {
min: 0,
max: 150
}
}
}, {
timestamps: true,
tableName: 'users'
});
const Post = sequelize.define('Post', {
title: {
type: DataTypes.STRING,
allowNull: false
},
content: {
type: DataTypes.TEXT
},
status: {
type: DataTypes.ENUM('draft', 'published', 'archived'),
defaultValue: 'draft'
}
});
// ๊ด๊ณ ์ ์
User.hasMany(Post, { foreignKey: 'authorId', as: 'posts' });
Post.belongsTo(User, { foreignKey: 'authorId', as: 'author' });
// ๋๊ธฐํ
await sequelize.sync();
// CRUD ์์
// Create
const user = await User.create({
username: 'john_doe',
email: 'john@example.com',
age: 25
});
// Read
const users = await User.findAll({
where: {
age: {
[Sequelize.Op.gte]: 20
}
},
order: [['createdAt', 'DESC']],
limit: 10
});
const user = await User.findOne({
where: { username: 'john_doe' }
});
// ๊ด๊ณ ํฌํจ
const userWithPosts = await User.findOne({
where: { username: 'john_doe' },
include: [{
model: Post,
as: 'posts'
}]
});
// Update
await User.update(
{ email: 'newemail@example.com' },
{ where: { username: 'john_doe' } }
);
// Delete
await User.destroy({
where: { username: 'john_doe' }
});
// ํธ๋์ญ์
await sequelize.transaction(async (t) => {
const user = await User.create({
username: 'alice',
email: 'alice@example.com'
}, { transaction: t });
await Post.create({
title: 'First Post',
content: 'Hello World',
authorId: user.id
}, { transaction: t });
});
๐ถ Prisma
Prisma๋ ์ฐจ์ธ๋ ORM์ผ๋ก, ํ์ ์์ ์ฑ๊ณผ ๊ฐ๋ฐ์ ๊ฒฝํ์ด ๋ฐ์ด๋! ๐์คํค๋ง ์ ์ (schema.prisma)
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
username String @unique
email String @unique
age Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
title String
content String? @db.Text
status Status @default(DRAFT)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
comments Comment[]
@@index([authorId])
@@map("posts")
}
model Comment {
id Int @id @default(autoincrement())
content String @db.Text
postId Int
post Post @relation(fields: [postId], references: [id])
userId Int
createdAt DateTime @default(now())
@@map("comments")
}
enum Status {
DRAFT
PUBLISHED
ARCHIVED
}
Prisma Client ์ฌ์ฉconst { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// Create
const user = await prisma.user.create({
data: {
username: 'john_doe',
email: 'john@example.com',
age: 25
}
});
// ๊ด๊ณ์ ํจ๊ป ์์ฑ
const userWithPost = await prisma.user.create({
data: {
username: 'alice',
email: 'alice@example.com',
posts: {
create: [
{ title: 'First Post', content: 'Hello World' },
{ title: 'Second Post', content: 'Another post' }
]
}
},
include: {
posts: true
}
});
// Read
const users = await prisma.user.findMany({
where: {
age: {
gte: 20
}
},
orderBy: {
createdAt: 'desc'
},
take: 10
});
const user = await prisma.user.findUnique({
where: { username: 'john_doe' }
});
// ๊ด๊ณ ํฌํจ
const userWithPosts = await prisma.user.findUnique({
where: { username: 'john_doe' },
include: {
posts: {
where: {
status: 'PUBLISHED'
},
orderBy: {
createdAt: 'desc'
}
}
}
});
// Update
const updatedUser = await prisma.user.update({
where: { username: 'john_doe' },
data: { email: 'newemail@example.com' }
});
// Delete
await prisma.user.delete({
where: { username: 'john_doe' }
});
// ์ง๊ณ
const userCount = await prisma.user.count();
const avgAge = await prisma.user.aggregate({
_avg: {
age: true
}
});
// ๊ทธ๋ฃนํ
const postsByAuthor = await prisma.post.groupBy({
by: ['authorId'],
_count: {
id: true
},
having: {
authorId: {
gt: 0
}
}
});
// ํธ๋์ญ์
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
username: 'bob',
email: 'bob@example.com'
}
});
await tx.post.create({
data: {
title: 'Bob\'s Post',
content: 'Content here',
authorId: user.id
}
});
});
// Raw SQL (ํ์ํ ๊ฒฝ์ฐ)
const result = await prisma.$queryRaw`
SELECT * FROM users WHERE age > ${20}
`;
// ์ฐ๊ฒฐ ์ข
๋ฃ
await prisma.$disconnect();
โข Prisma Studio: ๋ฐ์ดํฐ๋ฒ ์ด์ค GUI ๋๊ตฌ ์ ๊ณต
โข ๋ง์ด๊ทธ๋ ์ด์ : ์คํค๋ง ๋ณ๊ฒฝ ๊ด๋ฆฌ๊ฐ ์ฌ์
โข ์ฑ๋ฅ: ์ต์ ํ๋ ์ฟผ๋ฆฌ ์์ฑ
โข ๊ฐ๋ฐ์ ๊ฒฝํ: ์ง๊ด์ ์ธ API์ ํ๋ฅญํ ๋ฌธ์
๐ฏ ORM ์ฑ๋ฅ ์ต์ ํ ์ ๋ต
๐ N+1 ๋ฌธ์ ํด๊ฒฐ
N+1 ๋ฌธ์ ๋ ORM์์ ๊ฐ์ฅ ํํ ์ฑ๋ฅ ๋ฌธ์ ์ผ. ์๋ฅผ ๋ค์ด๋ณผ๊ฒ:# Django
posts = Post.objects.all() # 1๋ฒ์ ์ฟผ๋ฆฌ
for post in posts:
print(post.author.name) # N๋ฒ์ ์ถ๊ฐ ์ฟผ๋ฆฌ!
# ์ด 1 + N๋ฒ์ ์ฟผ๋ฆฌ ์คํ
๊ฒ์๊ธ์ด 100๊ฐ๋ฉด 101๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ์คํ๋ผ! ๐ฑ
# Django - select_related ์ฌ์ฉ
posts = Post.objects.select_related('author').all() # 1๋ฒ์ JOIN ์ฟผ๋ฆฌ
for post in posts:
print(post.author.name) # ์ถ๊ฐ ์ฟผ๋ฆฌ ์์!
# Sequelize - include ์ฌ์ฉ
const posts = await Post.findAll({
include: [{ model: User, as: 'author' }]
});
# Prisma - include ์ฌ์ฉ
const posts = await prisma.post.findMany({
include: { author: true }
});
๐ ์ ์ ํ ์ธ๋ฑ์ค ์ฌ์ฉ
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ธ๋ฑ์ค๋ ๊ฒ์ ์๋๋ฅผ ํฌ๊ฒ ํฅ์์์ผ!# Django
class Post(models.Model):
title = models.CharField(max_length=200)
status = models.CharField(max_length=20)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['status']),
models.Index(fields=['-created_at']),
models.Index(fields=['status', '-created_at']), # ๋ณตํฉ ์ธ๋ฑ์ค
]
# SQLAlchemy
from sqlalchemy import Index
class Post(Base):
__tablename__ = 'posts'
id = Column(Integer, primary_key=True)
title = Column(String(200))
status = Column(String(20), index=True)
created_at = Column(DateTime, index=True)
# ๋ณตํฉ ์ธ๋ฑ์ค
Index('idx_status_created', Post.status, Post.created_at.desc())
# Prisma
model Post {
id Int @id @default(autoincrement())
title String
status String
createdAt DateTime @default(now())
@@index([status])
@@index([createdAt(sort: Desc)])
@@index([status, createdAt(sort: Desc)])
}
๐ฏ ํ์ํ ํ๋๋ง ์กฐํ
# Django - only()
posts = Post.objects.only('id', 'title', 'created_at')
# Django - values()
posts = Post.objects.values('id', 'title')
# Sequelize
const posts = await Post.findAll({
attributes: ['id', 'title', 'createdAt']
});
# Prisma
const posts = await prisma.post.findMany({
select: {
id: true,
title: true,
createdAt: true
}
});
๐พ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์บ์ฑ
์์ฃผ ์กฐํ๋์ง๋ง ์์ฃผ ๋ณ๊ฒฝ๋์ง ์๋ ๋ฐ์ดํฐ๋ ์บ์ฑํ๋ ๊ฒ ์ข์!# Django with Redis
from django.core.cache import cache
def get_popular_posts():
cache_key = 'popular_posts'
posts = cache.get(cache_key)
if posts is None:
posts = Post.objects.filter(
status='published'
).order_by('-views')[:10]
cache.set(cache_key, posts, 3600) # 1์๊ฐ ์บ์ฑ
return posts
# Node.js with Redis
const redis = require('redis');
const client = redis.createClient();
async function getPopularPosts() {
const cacheKey = 'popular_posts';
const cached = await client.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const posts = await Post.findAll({
where: { status: 'published' },
order: [['views', 'DESC']],
limit: 10
});
await client.setEx(cacheKey, 3600, JSON.stringify(posts));
return posts;
}
๐ฆ ๋ฒํฌ ์ฐ์ฐ ์ฌ์ฉ
์ฌ๋ฌ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์ฒ๋ฆฌํ ๋๋ ๋ฒํฌ ์ฐ์ฐ์ ์ฌ์ฉํด!# Django
# ๋์ ์
for data in large_dataset:
Post.objects.create(**data) # N๋ฒ์ INSERT
# ์ข์ ์
posts = [Post(**data) for data in large_dataset]
Post.objects.bulk_create(posts, batch_size=1000) # ๋ฐฐ์น๋ก INSERT
# ์
๋ฐ์ดํธ๋ ๋ง์ฐฌ๊ฐ์ง
Post.objects.filter(status='draft').update(status='published')
# Sequelize
await Post.bulkCreate(dataArray, {
updateOnDuplicate: ['title', 'content']
});
# Prisma
await prisma.post.createMany({
data: dataArray,
skipDuplicates: true
});
๐ ์ฟผ๋ฆฌ ๋ถ์ ๋ฐ ๋ชจ๋ํฐ๋ง
# Django - ์ฟผ๋ฆฌ ๋ก๊น
import logging
logging.basicConfig()
logging.getLogger('django.db.backends').setLevel(logging.DEBUG)
# Django Debug Toolbar ์ฌ์ฉ
# settings.py
INSTALLED_APPS = [
...
'debug_toolbar',
]
# SQLAlchemy - ์ฟผ๋ฆฌ ์ถ๋ ฅ
engine = create_engine('...', echo=True)
# Prisma - ์ฟผ๋ฆฌ ๋ก๊น
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
# ์คํ ๊ณํ ํ์ธ
# Django
from django.db import connection
print(connection.queries)
# Raw SQL๋ก EXPLAIN ์คํ
cursor = connection.cursor()
cursor.execute('EXPLAIN SELECT * FROM posts WHERE status = %s', ['published'])
print(cursor.fetchall())
- โ N+1 ๋ฌธ์ ํด๊ฒฐ (select_related, prefetch_related)
- โ ์ ์ ํ ์ธ๋ฑ์ค ์ค์
- โ ํ์ํ ํ๋๋ง ์กฐํ (only, values, select)
- โ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์บ์ฑ
- โ ๋ฒํฌ ์ฐ์ฐ ์ฌ์ฉ
- โ ํ์ด์ง๋ค์ด์ ๊ตฌํ
- โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ํ ์ค์
- โ ์ฟผ๋ฆฌ ์คํ ๊ณํ ๋ถ์
- โ ๋๋ฆฐ ์ฟผ๋ฆฌ ๋ก๊น ๋ฐ ๋ชจ๋ํฐ๋ง
- โ ์ ์ ํ ํธ๋์ญ์ ๋ฒ์ ์ค์
๐ ๋ณด์ ๊ณ ๋ ค์ฌํญ
๐ SQL ์ธ์ ์ ๋ฐฉ์ด
SQL ์ธ์ ์ ์ ๊ฐ์ฅ ์ํํ ๋ณด์ ์ทจ์ฝ์ ์ค ํ๋์ผ. ORM์ ์ฌ์ฉํ๋ฉด ๋๋ถ๋ถ ์๋์ผ๋ก ๋ฐฉ์ด๋์ง๋ง, Raw SQL์ ์ฌ์ฉํ ๋๋ ์ฃผ์ํด์ผ ํด!# Django - ์ ๋ ์ด๋ ๊ฒ ํ์ง ๋ง!
username = request.GET.get('username')
query = f"SELECT * FROM users WHERE username = '{username}'"
User.objects.raw(query)
# ๊ณต๊ฒฉ์๊ฐ username์ "admin' OR '1'='1" ์
๋ ฅํ๋ฉด?
# SELECT * FROM users WHERE username = 'admin' OR '1'='1'
# ๋ชจ๋ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ๋
ธ์ถ๋จ!
# Django - ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ ์ฌ์ฉ
username = request.GET.get('username')
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])
# ๋๋ ORM ์ฌ์ฉ (๋ ์์ )
User.objects.filter(username=username)
# SQLAlchemy
username = request.args.get('username')
session.query(User).filter(User.username == username).all()
# Sequelize
const username = req.query.username;
await User.findAll({
where: { username: username }
});
# Prisma
const username = req.query.username;
await prisma.user.findMany({
where: { username: username }
});
๐ ๋น๋ฐ๋ฒํธ ์ํธํ
๋น๋ฐ๋ฒํธ๋ ์ ๋ ํ๋ฌธ์ผ๋ก ์ ์ฅํ๋ฉด ์ ๋ผ!# Django - ์๋์ผ๋ก ํด์ ์ฒ๋ฆฌ
from django.contrib.auth.models import User
user = User.objects.create_user(
username='john',
password='secret123' # ์๋์ผ๋ก ํด์๋จ
)
# ๋น๋ฐ๋ฒํธ ํ์ธ
user.check_password('secret123') # True
# Node.js - bcrypt ์ฌ์ฉ
const bcrypt = require('bcrypt');
// ๋น๋ฐ๋ฒํธ ํด์
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// ์ฌ์ฉ์ ์์ฑ
await User.create({
username: 'john',
password: hashedPassword
});
// ๋น๋ฐ๋ฒํธ ํ์ธ
const isValid = await bcrypt.compare(inputPassword, user.password);
๐ ์ ๊ทผ ๊ถํ ๊ด๋ฆฌ
# Django - ๊ถํ ์ฒดํฌ
from django.contrib.auth.decorators import login_required, permission_required
@login_required
def view_post(request, post_id):
post = get_object_or_404(Post, id=post_id)
# ์์ฑ์๋ง ์์ ๊ฐ๋ฅ
if post.author != request.user:
return HttpResponseForbidden()
return render(request, 'post.html', {'post': post})
# ๋๋ Django Guardian ์ฌ์ฉ (๊ฐ์ฒด ๋ ๋ฒจ ๊ถํ)
from guardian.shortcuts import assign_perm, get_objects_for_user
# ๊ถํ ๋ถ์ฌ
assign_perm('change_post', user, post)
# ๊ถํ ํ์ธ
if user.has_perm('change_post', post):
# ์์ ํ์ฉ
pass
# Express.js - ๋ฏธ๋ค์จ์ด๋ก ๊ถํ ์ฒดํฌ
const checkPostOwner = async (req, res, next) => {
const post = await Post.findByPk(req.params.id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
if (post.authorId !== req.user.id) {
return res.status(403).json({ error: 'Forbidden' });
}
req.post = post;
next();
};
app.put('/posts/:id', authenticate, checkPostOwner, async (req, res) => {
// ์์ ๋ก์ง
});
๐ ๋ฏผ๊ฐํ ์ ๋ณด ๋ณดํธ
# Django - ํ๋ ์ํธํ
from django_cryptography.fields import encrypt
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
ssn = encrypt(models.CharField(max_length=11)) # ์ฃผ๋ฏผ๋ฒํธ ์ํธํ
credit_card = encrypt(models.CharField(max_length=16))
# ํ๊ฒฝ ๋ณ์๋ก ๋ฏผ๊ฐ ์ ๋ณด ๊ด๋ฆฌ
# .env ํ์ผ
DATABASE_URL=mysql://user:password@localhost/dbname
SECRET_KEY=your-secret-key-here
AWS_ACCESS_KEY=your-aws-key
# Python - python-dotenv
from dotenv import load_dotenv
import os
load_dotenv()
DATABASE_URL = os.getenv('DATABASE_URL')
SECRET_KEY = os.getenv('SECRET_KEY')
# Node.js - dotenv
require('dotenv').config();
const dbConfig = {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD
};
๐จ ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ
- โ SQL ์ธ์ ์ ๋ฐฉ์ด (ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ ์ฌ์ฉ)
- โ ๋น๋ฐ๋ฒํธ ํด์ ์ฒ๋ฆฌ (bcrypt, Argon2 ๋ฑ)
- โ HTTPS ์ฌ์ฉ (์ ์ก ์ค ์ํธํ)
- โ ๋ฏผ๊ฐํ ๋ฐ์ดํฐ ์ํธํ
- โ ์ ๊ทผ ๊ถํ ์ฒดํฌ
- โ ์ ๋ ฅ ๊ฐ ๊ฒ์ฆ ๋ฐ sanitization
- โ Rate limiting (๋ฌด์ฐจ๋ณ ๋์ ๊ณต๊ฒฉ ๋ฐฉ์ด)
- โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ ๋ณด ๋ณดํธ
- โ ์ต์ ๊ถํ ์์น (DB ์ฌ์ฉ์ ๊ถํ ์ ํ)
- โ ์ ๊ธฐ์ ์ธ ๋ณด์ ์ ๋ฐ์ดํธ
- โ ๋ก๊น ๋ฐ ๋ชจ๋ํฐ๋ง
- โ ๋ฐฑ์ ๋ฐ ๋ณต๊ตฌ ๊ณํ
๐ ์ค์ ํ๋ก์ ํธ ์์
๐ ํ๋ก์ ํธ ๊ตฌ์กฐ
blog_project/
โโโ manage.py
โโโ blog/
โ โโโ __init__.py
โ โโโ settings.py
โ โโโ urls.py
โ โโโ wsgi.py
โโโ posts/
โโโ __init__.py
โโโ models.py
โโโ serializers.py
โโโ views.py
โโโ urls.py
โโโ permissions.py
๐ ๋ชจ๋ธ ์ ์ (models.py)
from django.db import models
from django.contrib.auth.models import User
from django.utils.text import slugify
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
class Meta:
verbose_name_plural = "Categories"
ordering = ['name']
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(unique=True)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
excerpt = models.TextField(max_length=500, blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='posts')
tags = models.ManyToManyField(Tag, related_name='posts', blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
views = models.IntegerField(default=0)
featured_image = models.URLField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', '-published_at']),
models.Index(fields=['author', '-created_at']),
]
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
if not self.excerpt:
self.excerpt = self.content[:200]
super().save(*args, **kwargs)
def __str__(self):
return self.title
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='replies')
is_approved = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f'{self.author.username}: {self.content[:50]}'
๐ Serializers (serializers.py)
from rest_framework import serializers
from .models import Post, Category, Tag, Comment
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
class CategorySerializer(serializers.ModelSerializer):
post_count = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'description', 'post_count']
def get_post_count(self, obj):
return obj.posts.filter(status='published').count()
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ['id', 'name', 'slug']
class CommentSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
replies = serializers.SerializerMethodField()
class Meta:
model = Comment
fields = ['id', 'author', 'content', 'parent', 'replies', 'created_at']
def get_replies(self, obj):
if obj.replies.exists():
return CommentSerializer(obj.replies.all(), many=True).data
return []
class PostListSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
comment_count = serializers.SerializerMethodField()
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'excerpt', 'author', 'category',
'tags', 'status', 'views', 'comment_count', 'created_at', 'published_at']
def get_comment_count(self, obj):
return obj.comments.filter(is_approved=True).count()
class PostDetailSerializer(serializers.ModelSerializer):
author = UserSerializer(read_only=True)
category = CategorySerializer(read_only=True)
tags = TagSerializer(many=True, read_only=True)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ['id', 'title', 'slug', 'content', 'excerpt', 'author',
'category', 'tags', 'status', 'views', 'featured_image',
'comments', 'created_at', 'updated_at', 'published_at']
class PostCreateUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ['title', 'content', 'category', 'tags', 'status', 'featured_image']
def create(self, validated_data):
tags = validated_data.pop('tags', [])
post = Post.objects.create(**validated_data)
post.tags.set(tags)
return post
def update(self, instance, validated_data):
tags = validated_data.pop('tags', None)
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
if tags is not None:
instance.tags.set(tags)
return instance
๐ฏ Views (views.py)
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django.db.models import Q, Count
from .models import Post, Category, Tag, Comment
from .serializers import (
PostListSerializer, PostDetailSerializer, PostCreateUpdateSerializer,
CategorySerializer, TagSerializer, CommentSerializer
)
from .permissions import IsAuthorOrReadOnly
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related('author', 'category').prefetch_related('tags', 'comments')
permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly]
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
search_fields = ['title', 'content']
ordering_fields = ['created_at', 'views', 'published_at']
def get_serializer_class(self):
if self.action == 'list':
return PostListSerializer
elif self.action in ['create', 'update', 'partial_update']:
return PostCreateUpdateSerializer
return PostDetailSerializer
def get_queryset(self):
queryset = super().get_queryset()
# ํํฐ๋ง
status_param = self.request.query_params.get('status')
if status_param:
queryset = queryset.filter(status=status_param)
category = self.request.query_params.get('category')
if category:
queryset = queryset.filter(category__slug=category)
tag = self.request.query_params.get('tag')
if tag:
queryset = queryset.filter(tags__slug=tag)
# ๊ณต๊ฐ๋ ๊ฒ์๊ธ๋ง (์์ฑ์๊ฐ ์๋ ๊ฒฝ์ฐ)
if not self.request.user.is_authenticated:
queryset = queryset.filter(status='published')
return queryset
def perform_create(self, serializer):
serializer.save(author=self.request.user)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
# ์กฐํ์ ์ฆ๊ฐ
Post.objects.filter(pk=instance.pk).update(views=models.F('views') + 1)
serializer = self.get_serializer(instance)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def popular(self, request):
"""์ธ๊ธฐ ๊ฒ์๊ธ"""
posts = self.get_queryset().order_by('-views')[:10]
serializer = PostListSerializer(posts, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def trending(self, request):
"""์ต๊ทผ ์ธ๊ธฐ ๊ฒ์๊ธ"""
from datetime import timedelta
from django.utils import timezone
week_ago = timezone.now() - timedelta(days=7)
posts = self.get_queryset().filter(
created_at__gte=week_ago
).order_by('-views')[:10]
serializer = PostListSerializer(posts, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def add_comment(self, request, pk=None):
"""๋๊ธ ์ถ๊ฐ"""
post = self.get_object()
serializer = CommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(post=post, author=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Category.objects.annotate(
post_count=Count('posts', filter=Q(posts__status='published'))
)
serializer_class = CategorySerializer
lookup_field = 'slug'
class TagViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
lookup_field = 'slug'
๐ Permissions (permissions.py)
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
"""
์์ฑ์๋ง ์์ /์ญ์ ๊ฐ๋ฅ, ๋๋จธ์ง๋ ์ฝ๊ธฐ๋ง ๊ฐ๋ฅ
"""
def has_object_permission(self, request, view, obj):
# ์ฝ๊ธฐ ๊ถํ์ ๋ชจ๋์๊ฒ ํ์ฉ
if request.method in permissions.SAFE_METHODS:
return True
# ์ฐ๊ธฐ ๊ถํ์ ์์ฑ์์๊ฒ๋ง
return obj.author == request.user
๐ฃ๏ธ URLs (urls.py)
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import PostViewSet, CategoryViewSet, TagViewSet
router = DefaultRouter()
router.register(r'posts', PostViewSet, basename='post')
router.register(r'categories', CategoryViewSet, basename='category')
router.register(r'tags', TagViewSet, basename='tag')
urlpatterns = [
path('', include(router.urls)),
]
์ด๋ ๊ฒ ๋ง๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ API ์๋ํฌ์ธํธ๊ฐ ์์ฑ๋ผ:โข POST /api/posts/ - ๊ฒ์๊ธ ์์ฑ
โข GET /api/posts/{id}/ - ๊ฒ์๊ธ ์์ธ
โข PUT /api/posts/{id}/ - ๊ฒ์๊ธ ์์
โข DELETE /api/posts/{id}/ - ๊ฒ์๊ธ ์ญ์
โข GET /api/posts/popular/ - ์ธ๊ธฐ ๊ฒ์๊ธ
โข GET /api/posts/trending/ - ํธ๋ ๋ฉ ๊ฒ์๊ธ
โข POST /api/posts/{id}/add_comment/ - ๋๊ธ ์ถ๊ฐ
โข GET /api/categories/ - ์นดํ ๊ณ ๋ฆฌ ๋ชฉ๋ก
โข GET /api/tags/ - ํ๊ทธ ๋ชฉ๋ก
๐ ํ์ต ๋ก๋๋งต๊ณผ ์ถ์ฒ ์๋ฃ
๐บ๏ธ ํ์ต ๋จ๊ณ
โข ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ ์์น (์ ๊ทํ, ๊ด๊ณ ์ค์ )
โข ๊ธฐ๋ณธ CRUD ์์ ์ค์ต
โข ๊ฐ๋จํ ํ๋ก์ ํธ ๋ง๋ค์ด๋ณด๊ธฐ
์ถ์ฒ ์๋ฃ:
- W3Schools SQL Tutorial
- SQLZoo (๋ํํ SQL ํ์ต)
- ์ํ์ฝ๋ฉ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์
โข ๋ชจ๋ธ ์ ์์ ๊ด๊ณ ์ค์
โข ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ์์ฑ
โข ๋ง์ด๊ทธ๋ ์ด์ ๊ด๋ฆฌ
์ถ์ฒ ์๋ฃ:
- Django ๊ณต์ ๋ฌธ์ (Django ORM)
- SQLAlchemy ํํ ๋ฆฌ์ผ
- Prisma ๊ณต์ ๊ฐ์ด๋
- Real Python ORM ๊ฐ์ข
โข ๋ณต์กํ ์ฟผ๋ฆฌ์ ๊ด๊ณ ๋ค๋ฃจ๊ธฐ
โข ์ฑ๋ฅ ์ต์ ํ ์ ์ฉ
โข ๋ณด์ ๊ณ ๋ ค์ฌํญ ๊ตฌํ
ํ๋ก์ ํธ ์์ด๋์ด:
- ๋ธ๋ก๊ทธ ํ๋ซํผ
- ์ ์์๊ฑฐ๋ ์ฌ์ดํธ
- SNS ์๋น์ค
- ํ๋ก์ ํธ ๊ด๋ฆฌ ๋๊ตฌ
โข ์บ์ฑ ์ ๋ต
โข ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ
โข NoSQL๊ณผ RDBMS ํ์ด๋ธ๋ฆฌ๋ ์ฌ์ฉ
โข ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
์ถ์ฒ ์๋ฃ:
- "Designing Data-Intensive Applications" (์ฑ )
- AWS Database ์๋น์ค ๋ฌธ์
- Redis ๊ณต์ ๋ฌธ์
- MongoDB University
๐ก ์ค๋ฌด ํ
2. ๊ณต์ ๋ฌธ์ ์ฝ๊ธฐ
๋ธ๋ก๊ทธ ๊ธ๋ ์ข์ง๋ง, ๊ณต์ ๋ฌธ์๊ฐ ๊ฐ์ฅ ์ ํํ๊ณ ์ต์ ์ ๋ณด๋ฅผ ๋ด๊ณ ์์ด. ์ฒ์์ ์ด๋ ต๊ฒ ์ง๋ง ์ต์ํด์ง๋ฉด ๊ฐ์ฅ ๋น ๋ฅธ ํ์ต ๋ฐฉ๋ฒ์ด์ผ.
3. ์ฝ๋ ๋ฆฌ๋ทฐ ๋ฐ๊ธฐ
GitHub์ ์ฝ๋๋ฅผ ์ฌ๋ฆฌ๊ณ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค์ ํผ๋๋ฐฑ์ ๋ฐ์๋ด. ๋๋ ์จ๋ผ์ธ ์ปค๋ฎค๋ํฐ์์ ์ง๋ฌธํ๊ณ ๋ต๋ณํ๋ฉด์ ๋ฐฐ์!
4. ์คํ์์ค ํ๋ก์ ํธ ์ฐธ์ฌ
์ค์ ์๋น์ค์์ ์ฌ์ฉ๋๋ ์ฝ๋๋ฅผ ๋ณด๋ฉด์ ๋ง์ด ๋ฐฐ์ธ ์ ์์ด. ์์ ๋ฒ๊ทธ ์์ ์ด๋ ๋ฌธ์ ๊ฐ์ ๋ถํฐ ์์ํด๋ด.
5. ์ฑ๋ฅ ์ธก์ ์ต๊ดํ
ํญ์ ์ฟผ๋ฆฌ ์คํ ์๊ฐ์ ์ฒดํฌํ๊ณ , ๋๋ฆฐ ๋ถ๋ถ์ ์ฐพ์์ ์ต์ ํํ๋ ์ต๊ด์ ๋ค์ฌ. ์ด๊ฒ ์ค๋ ฅ ํฅ์์ ์ง๋ฆ๊ธธ์ด์ผ!
๐ ์ ์ฉํ ๋ฆฌ์์ค
์จ๋ผ์ธ ๊ฐ์ ํ๋ซํผ:โข Udemy - ๋ค์ํ ORM ๊ฐ์ข
โข Coursera - ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๋ฌธ ๊ณผ์
โข Pluralsight - ์ค๋ฌด ์ค์ฌ ๊ฐ์
โข ์ธํ๋ฐ - ํ๊ตญ์ด ๊ฐ์
์ปค๋ฎค๋ํฐ:
โข Stack Overflow - ์ง๋ฌธ๊ณผ ๋ต๋ณ
โข Reddit (r/django, r/webdev) - ํ ๋ก ๊ณผ ์ ๋ณด ๊ณต์
โข Dev.to - ๊ฐ๋ฐ ๋ธ๋ก๊ทธ
โข ์ํ์ฝ๋ฉ ์ปค๋ฎค๋ํฐ
๋๊ตฌ:
โข DBeaver - ๋ฐ์ดํฐ๋ฒ ์ด์ค GUI ๋๊ตฌ
โข Postman - API ํ ์คํธ
โข Django Debug Toolbar - ์ฟผ๋ฆฌ ๋ถ์
โข pgAdmin - PostgreSQL ๊ด๋ฆฌ
๐ฏ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๋๊ณผ ORM์ ์น ๊ฐ๋ฐ์ ํต์ฌ ๊ธฐ์ ์ด์ผ. ์ฒ์์ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ง๋ง, ๊พธ์คํ ์ฐ์ตํ๋ค ๋ณด๋ฉด ์ด๋์ ์์ฐ์ค๋ฝ๊ฒ ์ฌ์ฉํ๊ณ ์๋ ์์ ์ ๋ฐ๊ฒฌํ๊ฒ ๋ ๊ฑฐ์ผ.
โข SQL์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ํตํ๋ ์ธ์ด
โข ORM์ ๊ฐ์ฒด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฐ๊ฒฐํด์ฃผ๋ ๋ง๋ฒ ๊ฐ์ ๋๊ตฌ
โข ์ฑ๋ฅ ์ต์ ํ๋ ์ค๋ฌด์์ ์ ๋ง ์ค์ํด (N+1 ๋ฌธ์ , ์ธ๋ฑ์ค, ์บ์ฑ)
โข ๋ณด์์ ์ ๋ ์ํํ ํ๋ฉด ์ ๋ผ (SQL ์ธ์ ์ , ๋น๋ฐ๋ฒํธ ์ํธํ)
โข ์ค์ ๊ฒฝํ์ด ๊ฐ์ฅ ์ค์ํด - ๋ง์ด ๋ง๋ค์ด๋ณด์!
๊ธฐ์ตํด! ์๋ฒฝํ ๊ฐ๋ฐ์๋ ์์ด. ๋ชจ๋ ๊ณ์ ๋ฐฐ์ฐ๊ณ ์ฑ์ฅํ๋ ์ค์ด์ผ. ์ค์๋ฅผ ๋๋ ค์ํ์ง ๋ง๊ณ , ๋ง์ด ์๋ํด๋ณด๊ณ , ์คํจํ๋ฉด์ ๋ฐฐ์๋๊ฐ๋ฉด ๋ผ. ๐ช
๊ทธ๋ฆฌ๊ณ ํผ์ ๊ณ ๋ฏผํ์ง ๋ง๊ณ ์ปค๋ฎค๋ํฐ์ ์ฐธ์ฌํด์ ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค๊ณผ ์ํตํด. ์ง๋ฌธํ๊ณ , ๋ต๋ณํ๊ณ , ์ฝ๋๋ฅผ ๊ณต์ ํ๋ฉด์ ํจ๊ป ์ฑ์ฅํ๋ ๊ฑฐ์ผ!
ํ์ดํ ! ๋ ํ ์ ์์ด! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.