이번 장에서는 문제를 어떻게 바라볼 것인지에 대한 접근 방식과, 애플리케이션 개발에서 매우 중요한 요소인 데이터 모델과 각 질의 언어에 대해 설명합니다.
데이터 모델은 크게 5가지 유형으로 나뉩니다.
- 관계형 모델(Relational Model)
- 문서 모델(Document Model)
- 계층 모델(Hierarchical Model) → 현대에서는 거의 안쓰임
- 네트워크 모델(Network Model) → 현대에서는 거의 안쓰임
- 그래프형 데이터 모델(Graph Data Model)
그동안 MySQL, PostgreSQL, MariaDB 같은 관계형 데이터베이스에는 익숙했지만, 상대적으로 덜 알고 있던 문서 모델과 그래프 기반 모델을 더 깊이 이해할 수 있는 계기가 되어 유익했습니다.
또한, 왜 하나의 모델이 아닌 여러 가지 데이터 모델이 개발되었으며, 어떤 발전 과정을 거쳐왔는지, 그리고 일부 모델은 현재 거의 사용되지 않는 이유까지 살펴볼 수 있어 흥미로웠습니다.
데이터 모델은 크게 5가지로 나뉘어 진다.
관계형 모델(relation model)문서 모델(document model)계층 모델(Hierarchical Model) → 현대에서는 거의 안쓰임네트워크 모델(Network model) → 현대에서는 거의 안쓰임그래프형 데이터 모델(Graph Data Model)비즈니스 데이터 처리에 근원이 있다. 보통 트랜잭션 처리, 일괄 처리에 특화되어 있으며 오늘날 일상적으로 사용된다.
1970~80년대 초반에는 네트워크 모델, 계층 모델을 주로 사용했지만, 결국 관계형 모델이 우위를 차지했다.
조인, 다대일 관계, 다대다 관계를 더 잘 지원한다. (고유한 식별자로 참조: 외래키)
2010년대에 관계형 모델의 우위를 뒤집기 위해 나온 최신 시도이다. Not Only SQL
1970년대 가장 많이 사용된 DB 인 IBM 의 정보관리시스템(IMS)의 데이터 모델이다.
JSON 구조와 마찬가지로 모든 데이터를 레코드 내에 중첩된 레코드 트리로 표현한다.
일대다 관계는 잘 동작. But, 다대일, 다대다 관계 표현 X, 조인 지원 X
→ 해결책으로 제시: 관계형 모델, 네트워크 모델
코다실(CODASYL)이라 불리는 위원회에서 표준화 되었다. 코다실 모델이라고도 부른다.
네트워크 모델에서 레코드는 부모가 여러개일 수 있다. 다대일과 다대다 관계 표현 가능
네트워크 모델의 레코드 간 연결은 외래 키 보다는 프로그래밍 언어의 포인터와 더 비슷하다.
레코드에 접근하는 유일한 방법은 최상위 레코드(root)에서 연속된 연결 경로를 따르는 것이다. (→ 접근 경로)
→ DB 질의와 갱신을 위한 코드가 복잡하고 유연하지 못하다! 아주 많은 수작업과 코드를 재작성해야 함 (계층 모델, 네트워크 모델 모두)
그래프는 두 유형의 객체로 이뤄진다. 정점(Vertex), 간선(Edge)
그래프는 동종 데이터에 국한되지 않는다.
그래프용 선언형 질의 언어 3가지
SELECT users.name, orders.product
FROM users
JOIN orders ON users.id = orders.user_id
WHERE users.id = 1;
✔ 외래 키를 이용한 JOIN을 실행하면 데이터가 미리 연결된 상태에서 조회됨.
✔ 관계형 모델에서는 외래 키를 기반으로 JOIN을 수행하여 데이터를 읽는 순간에 일관성을 유지함.
📌 즉, 관계형 모델에서는 JOIN을 통해 두 개의 테이블을 결합하여 한 번에 데이터를 가져오므로, "읽기 시점에 관계를 확인한다"는 개념이 적용되지 않음.
문서 모델(NoSQL, MongoDB 등)에서는 테이블이 아니라 독립적인 문서(Document)로 데이터를 저장하며, 데이터 간의 관계를 "참조(Reference)"를 통해 연결합니다.
하지만 이 관계는 사전에 강제되지 않으며, 데이터를 읽을 때(조회할 때) 참조 문서를 직접 가져와서 확인해야 합니다.
users 컬렉션{
"_id": 1,
"name": "Alice"
}
orders 컬렉션{
"_id": 101,
"user_id": 1,
"product": "Laptop"
}
// 1. users 컬렉션에서 특정 사용자를 조회
const user = db.users.findOne({ _id: 1 });
// 2. 해당 사용자의 주문 내역을 조회 (별도 질의 필요)
const orders = db.orders.find({ user_id: user._id }).toArray();
✔ 여기서 중요한 점은, orders 컬렉션의 문서가 users 컬렉션과 강제로 연결된 것이 아니라,
✔ 데이터를 읽을 때(find() 실행 시) user_id를 기준으로 추가 질의를 실행하여 데이터를 가져온다는 점!
✔ 즉, 데이터가 저장될 때는 단순히 참조 값만 들어 있고, 데이터를 읽을 때 해당 참조를 기반으로 후속 질의(추가 조회)가 필요함.
📌 이것이 "읽기 시점에 확인한다"는 의미!
상황: User의 성(first_name)과 이름을 분리해서 저장해야하는 요구사항 발생
if (user && user.name && !user.first_name) {
user.first_name = user.name.split(" ")[0];
}
-- 일반적인 마이그레이션, first_name 컬럼을 추가하고 업데이트합니다.
ALTER TABLE users ADD COLUMN first_name test;
UPDATE users SET first_name = split_part(name, ' ', 1) -- PostgreSQL
UPDATE users SET first_name = subst4ring_index(name, ' ', 1) -- MySQL
대부분의 RDB의 경우 ALTER TABLE 을 수밀리초 안에 끝낸다.
But, MySQL 의 경우, ALTER TABLE 시에 전체 테이블 복사, 큰 테이블에 실행시 수 분에서 수 시간까지 중단시간 발생!
큰 테이블에 UPDATE 실행시 모든 로우가 재작성될 수 있기 때문에 이것도 오래 걸릴 수 있다.
users 테이블에 first_name 컬럼을 빈 값으로 추가한 후 user 데이터를 읽는 시점에 first_name 컬럼에 데이터를 추가 할 수 있다.SELECT name, age
FROM users
WHERE age > 25
ORDER BY name ASC;
✔ 설명
users 테이블에서 age > 25인 사람들을 가져오라고만 지시함.import java.sql.*;
public class ImperativeQueryExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydb";
String user = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, user, password)) {
String query = "SELECT name, age FROM users WHERE age > 25 ORDER BY name ASC";
try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) {
while (rs.next()) {
System.out.println("Name: " + rs.getString("name") + ", Age: " + rs.getInt("age"));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
✔ 설명
💡 SQL은 선언적이지만, JDBC 같은 API를 사용하면 명령형 프로그래밍 방식이 적용된다. 🚀
맵리듀스(MapReduce)는 많은 컴퓨터에서 대량의 데이터를 처리하기 위한 프로그래밍 모델이다.
map(collect 라고도 함)과 reduce(fold나 inject라고도 함) 함수를 기반으로 한다.map은 데이터를 변환해서 반환한다.reduce는 데이터를 집계 한다.map, reduce 함수를 수행할 때 순수(pure) 함수여야 한다. 전달된 데이터만 사용하고 그 외의 부수 효과는 없어야 한다.mapReduce는 공식적으로 deprecated(비추천) 되었습니다. 대신 집계 파이프라인(Aggregation Pipeline)이 성능이 좋고 최적화되어 있으므로 사용을 권장합니다.mapReduce 사용 예제 (Deprecated)db.orders.insertMany([
{ "_id": 1, "product": "Apple", "quantity": 3 },
{ "_id": 2, "product": "Banana", "quantity": 5 },
{ "_id": 3, "product": "Apple", "quantity": 2 },
{ "_id": 4, "product": "Orange", "quantity": 4 },
{ "_id": 5, "product": "Banana", "quantity": 2 }
]);
// 🔹 mapReduce 실행
db.orders.mapReduce(
function() { emit(this.product, this.quantity); }, // 1️⃣ Map 함수: (제품, 수량) 형태로 변환
function(key, values) { return Array.sum(values); }, // 2️⃣ Reduce 함수: 같은 제품끼리 합산
{ out: "total_sales" } // 3️⃣ 결과 저장
);
// 🔹 결과 조회
db.total_sales.find().pretty();
mapReduce 결과[
{ "_id": "Apple", "value": 5 },
{ "_id": "Banana", "value": 7 },
{ "_id": "Orange", "value": 4 }
]
Aggregation Pipeline 사용 예제 (권장 방식)db.orders.aggregate([
{
$group: {
_id: "$product",
totalQuantity: { $sum: "$quantity" }
}
}
]);
Aggregation Pipeline 결과[
{ "_id": "Apple", "totalQuantity": 5 },
{ "_id": "Banana", "totalQuantity": 7 },
{ "_id": "Orange", "totalQuantity": 4 }
]