[Study] DDIA: Chapter 2: 데이터 모델과 질의 언어

이형걸·2025년 2월 24일
0

Study

목록 보기
2/5

이번 장에서는 문제를 어떻게 바라볼 것인지에 대한 접근 방식과, 애플리케이션 개발에서 매우 중요한 요소인 데이터 모델과 각 질의 언어에 대해 설명합니다.

데이터 모델은 크게 5가지 유형으로 나뉩니다.

  1. 관계형 모델(Relational Model)
  2. 문서 모델(Document Model)
  3. 계층 모델(Hierarchical Model) → 현대에서는 거의 안쓰임
  4. 네트워크 모델(Network Model) → 현대에서는 거의 안쓰임
  5. 그래프형 데이터 모델(Graph Data Model)

그동안 MySQL, PostgreSQL, MariaDB 같은 관계형 데이터베이스에는 익숙했지만, 상대적으로 덜 알고 있던 문서 모델과 그래프 기반 모델을 더 깊이 이해할 수 있는 계기가 되어 유익했습니다.

또한, 왜 하나의 모델이 아닌 여러 가지 데이터 모델이 개발되었으며, 어떤 발전 과정을 거쳐왔는지, 그리고 일부 모델은 현재 거의 사용되지 않는 이유까지 살펴볼 수 있어 흥미로웠습니다.


0. 내용 정리

데이터 모델은 크게 5가지로 나뉘어 진다.

  • 관계형 모델(relation model)
  • 문서 모델(document model)
  • 계층 모델(Hierarchical Model) → 현대에서는 거의 안쓰임
  • 네트워크 모델(Network model) → 현대에서는 거의 안쓰임
  • 그래프형 데이터 모델(Graph Data Model)

관계형 모델

Image

비즈니스 데이터 처리에 근원이 있다. 보통 트랜잭션 처리, 일괄 처리에 특화되어 있으며 오늘날 일상적으로 사용된다.

1970~80년대 초반에는 네트워크 모델, 계층 모델을 주로 사용했지만, 결국 관계형 모델이 우위를 차지했다.

조인, 다대일 관계, 다대다 관계를 더 잘 지원한다. (고유한 식별자로 참조: 외래키)

문서 모델

2010년대에 관계형 모델의 우위를 뒤집기 위해 나온 최신 시도이다. Not Only SQL

  • 대규모 데이터 셋, 매우 높은 쓰기 처리량을 RDB 보다 쉽게 할 수 있다.
  • 무료 오픈소스 소프트웨어에 대한 선호도 확산.
  • Relation Model 에서 지원하지 않은 특수 질의 동작
  • 관계형 스키마의 제한에서 벗어난 동적이고 자유로운 데이터 모델
  • 계층 모델과 마찬가지로 다대일, 다대다 관계 표현, 조인 지원이 약하다.(고유한 식별자로 참조: 문서 참조(document reference))
    • 다중 질의를 만들어서 애플리케이션 단에서 조인을 흉내 내야 한다. 읽기 시점에 확인
  • 현재까지는 코다실(네트워크 모델)의 전철을 밟고 있진 않다.

계층 모델

1970년대 가장 많이 사용된 DB 인 IBM 의 정보관리시스템(IMS)의 데이터 모델이다.

JSON 구조와 마찬가지로 모든 데이터를 레코드 내에 중첩된 레코드 트리로 표현한다.

일대다 관계는 잘 동작. But, 다대일, 다대다 관계 표현 X, 조인 지원 X

해결책으로 제시: 관계형 모델, 네트워크 모델

네트워크 모델

코다실(CODASYL)이라 불리는 위원회에서 표준화 되었다. 코다실 모델이라고도 부른다.

네트워크 모델에서 레코드는 부모가 여러개일 수 있다. 다대일과 다대다 관계 표현 가능

네트워크 모델의 레코드 간 연결은 외래 키 보다는 프로그래밍 언어의 포인터와 더 비슷하다.

레코드에 접근하는 유일한 방법은 최상위 레코드(root)에서 연속된 연결 경로를 따르는 것이다. (→ 접근 경로)

DB 질의와 갱신을 위한 코드가 복잡하고 유연하지 못하다! 아주 많은 수작업과 코드를 재작성해야 함 (계층 모델, 네트워크 모델 모두)

그래프형 데이터 모델

Image

그래프는 두 유형의 객체로 이뤄진다. 정점(Vertex), 간선(Edge)

그래프는 동종 데이터에 국한되지 않는다.

  • 속성 그래프 모델(Neo4j, Titan, InfiniteGraph)
  • 트리플 저장소 모델(Datomic, Allegrograph)

그래프용 선언형 질의 언어 3가지

  • 사이퍼(Cypher)
  • 스파클(SPARQL)
  • 데이터로그(Datalog)

New) 새롭게 알게 된 점

1. 문서 모델에서의 읽기 시점에 확인(다중 질의, 후속 질의)

1-1) 관계형 모델에서 질의 (JOIN을 사용한 데이터 조회, 외래키)

SELECT users.name, orders.product
FROM users
JOIN orders ON users.id = orders.user_id
WHERE users.id = 1;

외래 키를 이용한 JOIN을 실행하면 데이터가 미리 연결된 상태에서 조회됨.

✔ 관계형 모델에서는 외래 키를 기반으로 JOIN을 수행하여 데이터를 읽는 순간에 일관성을 유지함.

📌 즉, 관계형 모델에서는 JOIN을 통해 두 개의 테이블을 결합하여 한 번에 데이터를 가져오므로, "읽기 시점에 관계를 확인한다"는 개념이 적용되지 않음.

1-2) 문서 모델의 문서 참조(Document Reference)

문서 모델(NoSQL, MongoDB 등)에서는 테이블이 아니라 독립적인 문서(Document)로 데이터를 저장하며, 데이터 간의 관계를 "참조(Reference)"를 통해 연결합니다.

하지만 이 관계는 사전에 강제되지 않으며, 데이터를 읽을 때(조회할 때) 참조 문서를 직접 가져와서 확인해야 합니다.

✅ 예제 (MongoDB - 문서 참조 방식)

1.users 컬렉션

{
  "_id": 1,
  "name": "Alice"
}

2. orders 컬렉션

{
  "_id": 101,
  "user_id": 1,
  "product": "Laptop"
}

3. 읽기 시점에 확인하는 방식

// 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를 기준으로 추가 질의를 실행하여 데이터를 가져온다는 점!

✔ 즉, 데이터가 저장될 때는 단순히 참조 값만 들어 있고, 데이터를 읽을 때 해당 참조를 기반으로 후속 질의(추가 조회)가 필요함.

📌 이것이 "읽기 시점에 확인한다"는 의미!

  • MongoDB의 문서 참조 방식에서는 데이터를 저장할 때는 단순히 참조 ID만 저장하고,
  • 실제 데이터를 읽을 때(조회할 때) 해당 ID를 기반으로 추가 질의를 실행하여 데이터를 가져옴.

2. 스키마 차이에 따른 데이터 타입 변경을 위한 접근 방식 차이

  • 문서 모델(Document Model): 읽기 스키마(schema-on-read): 암묵적, 데이터를 읽을 때만 해석
  • 관계형 모델(Relation Model): 쓰기 스키마(schema-on-write): 명시적, 모든 데이터가 스키마를 따름을 보장

상황: User의 성(first_name)과 이름을 분리해서 저장해야하는 요구사항 발생

2-1) 문서 데이터베이스

  • 새로운 필드를 가진 새로운 문서를 작성
  • 애플리케이션 코드: user.first_name이 없는 경우에는 name을 split해서 first_name을 직접 가져오면 된다.
if (user && user.name && !user.first_name) {
	user.first_name = user.name.split(" ")[0];
}

2-2) 관계형 데이터베이스

  • Migration 필요
-- 일반적인 마이그레이션, 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 컬럼에 데이터를 추가 할 수 있다.

3. 선언형 질의(Declarative Query) vs. 명령형 질의(Imperative Query)

  • 선언형 질의(Declarative Query Language):
    • "무엇을 할 것인가?" 에 초점을 맞춘 방식.
    • 사용자는 원하는 결과만 명시하고, 내부적인 실행 방법(절차)은 데이터베이스 시스템이 최적화하여 결정함.
    • 예: SQL, SPARQL, LINQ
  • 명령형 질의(Imperative Query API):
    • "어떻게 할 것인가?" 에 초점을 맞춘 방식.
    • 사용자가 데이터를 처리하는 절차(알고리즘)를 직접 명시해야 함.
    • 예: JDBC(Java), PyMongo(Python), Cursor 기반 API

3-1) 선언형 질의 예시 (SQL)

SELECT name, age
FROM users
WHERE age > 25
ORDER BY name ASC;

설명

  • 사용자는 users 테이블에서 age > 25인 사람들을 가져오라고만 지시함.
  • 데이터베이스 엔진이 내부적으로 최적화된 실행 계획을 선택하여 수행.

3-2) 명령형 질의 예시 (JDBC - Java)

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 실행 → 결과 집합을 반복 처리하는 방식.
  • 사용자가 어떻게 데이터를 검색하고 처리할지 직접 명시해야 함.

🔹 3-3) 결론

  • 단순한 데이터 조회/처리는 SQL 같은 선언형 질의가 유리.
  • 명령형 API는 데이터를 직접 조작해야 하는 경우(트랜잭션, 비즈니스 로직 포함 시) 적합.

💡 SQL은 선언적이지만, JDBC 같은 API를 사용하면 명령형 프로그래밍 방식이 적용된다. 🚀


Difficulty) 어려웠거나 이해하지 못한 부분

맵리듀스(MapReduce) 질의

맵리듀스(MapReduce)는 많은 컴퓨터에서 대량의 데이터를 처리하기 위한 프로그래밍 모델이다.

  • 많은 문서를 대상으로 읽기 전용(read-only) 질의를 수행할 때 사용한다.
  • 선언형 질의, 명령형 질의 API 의 중간 정도다.
  • 질의 로직은 프레임워크가 반복적으로 호출하는 조각 코드로 표현한다.
  • 여러 함수형 프로그래밍에 있는 map(collect 라고도 함)과 reduce(fold나 inject라고도 함) 함수를 기반으로 한다.
    • map데이터를 변환해서 반환한다.
    • reduce데이터를 집계 한다.
    • map과 reduce를 동일한 Input이 들어가면 항상 동일한 Output을 반환하도록 구성하면 분산처리에 활용할 수 있다.
  • 몽고DBmap, reduce 함수를 수행할 때 순수(pure) 함수여야 한다. 전달된 데이터만 사용하고 그 외의 부수 효과는 없어야 한다.
  • 💡 MongoDB 5.0 이후로 mapReduce는 공식적으로 deprecated(비추천) 되었습니다. 대신 집계 파이프라인(Aggregation Pipeline)이 성능이 좋고 최적화되어 있으므로 사용을 권장합니다.

✅ 1. 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 }
]

✅ 2. 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 }
]

Amendment) 추가 내용

기억해두면 좋을 내용:

Image
  • JSON 표현은 RDB 의 Schema(다중 테이블 스키마) 보다 더 나은 지역성(locality) 을 갖는다.
    • RDB 에서는 다중 질의 or 다중 JOIN 을 수행해야 한다.
    • JSON 표현은 모든 정보가 한 곳에 있기 때문에 질의 한번으로 충분하다.

Reference) 참고

profile
현명하고 성실하게 살자

0개의 댓글