열렬히.뛰기

빅데이터 다루기

데이터베이스 > 빅데이터 다루기

빅데이터과 SQL의 한계

SQL은 관계형 데이터베이스에서 가장 널리 쓰이는 쿼리 언어.

하지만 빅데이터는 형식이 다양하고, 처리해야 할 데이터 양/속도가 매우 크므로, SQL만으로는 한계가 있음.

따라서 이를 해결하기 위해서는 다음과 같은 조건들이 필요함.

  1. 병렬 저장 및 병렬 처리 필수.
  2. 수천 대 이상의 컴퓨터에서 데이터 처리 필요.
  3. 단순한 SQL 지원뿐만 아니라, 확장성과 고성능도 요구됨.
  4. 일부 기능(예: 트랜잭션 등)을 포기하면 확장성 있는 시스템 구축이 쉬워짐.

이에 따라 빅데이터를 처리하는 시스템의 2가지 유형을 분류해보자면,

  1. 고성능 트랜잭션 처리 시스템
    • 짧고 많은 요청을 빠르게 처리해야 함
    • 완전한 관계형 기능이 없어도 됨. 일반적으로 Key-Value 저장소 사용.
  2. 비관계형 데이터용 쿼리 처리 시스템
    • 웹 로그, 문서, 검색 시스템 등을 처리.
    • 비정형 데이터 파일 다수 처리 필요. SQL로 표현하기 힘든 연산을 위해 임의의 코드 실행 지원.
    • 전통적인 파일 시스템 + 단일 프로그램 처리 → 확장 한계 도달.
    • 따라서 병렬 처리, 장애 복구, 부하 균형 등을 처리할 수 있는 시스템 필요.

빅데이터 저장 시스템

빅데이터 시스템은 극도로 높은 확장성 요구사항을 가지고 있음.

수억 명의 사용자와 이로 인한 짧은 기간 내의 폭발적인 부하의 증가를 감당하려면, 수천 개의 컴퓨팅 및 노드에 걸쳐 분산해서 저장해야 함.

따라서 이를 위한 몇 가지의 시스템이 존재하며, 이를 알아보도록 하자.

  1. 📦 샤딩 (Sharding)
  2. 🗝️ 키-값 저장 시스템 (NoSQL)
  3. 💽 병렬 및 분산 데이터베이스 (Parallel and Distributed Databases)
  4. 📁 분산 파일 시스템 (Distributed File Systems)

1번 → 4번 순서대로 데이터가 점점 커지는 방향이라고 생각하면 된다.

1. 샤딩 (shading)

큰 데이터를 저장하려면 어떻게 해야할까? 먼저 하나의 DB를 여러 개로 나눠쓰면 되지 않을까?

하나의 큰 데이터베이스를 여러 개로 나눠쓰는 방법을 샤딩(shading)이라고 한다.

  • 샤드 키 또는 파티셔닝 키를 기준으로 데이터를 분할 (예: 사용자 ID 또는 계정 ID)
  • Range 파티셔닝: ID 범위를 나누어 할당 (예: ID 1~100,000 → DB1)
  • Hash 파티셔닝: 해시 함수로 파티션 결정

📌 문제점

  • 애플리케이션이 어떤 데이터가 어디 있는지 직접 추적해야 함
  • 다중 DB 질의 불가 → 여러 DB에서 읽은 후 애플리케이션에서 결과 조합
  • 데이터베이스 과부하 발생 시 재분산 필요 → 매우 복잡
  • 복제일관성 유지도 어려움

그래서 도입한 것이 바로 병렬 키-값 저장소!

2. 병렬 키-값 저장소 (NoSQL)

shading이 가진 단점을 극복하기 위해, 여러 개의 DB를 써야할 듯 하다. 이때, 여러 개의 DB를 key만 가지고 구분할 순 없을까?

  • 여러 키들이 ‘자동으로’ 나눠져서 여러 DB에 분산 저장되는 구조
  • 이때, 각 DB는 NoSQL 기반의 비관계형 시스템. 그래서 NoSQL이라고도 함.
  • 근데, 왜 비관계형 시스템을 쓸까?
    1. 관계형 DB와 달리 수평적으로 확장이 가능. 즉, 서버를 수천 개로 늘려서 데이터를 분산 처리하기에 좋음
    2. 빠른 읽기/쓰기가 가능해서 고성능으로 실시간 데이터를 처리할 수 있음.
    3. 유연한 데이터 구조를 채택할 수 있음. 꼭 행과 열로 이뤄져 있지 않음.
javascript
// 사용자(user) 컬렉션에 저장할 문서 예시
{
  "_id": "user123",  // key 역할
  "name": {
    "first": "Alex",
    "last": "Kim"
  },
  "email": "alex@example.com",
  "age": 29,
  "preferences": {
    "theme": "dark",
    "language": "ko"
  }
}
  • Google의 Bigtable, Amazon의 DynamoDB, Facebook의 Cassandra, MongoDB 등이 대표적인 병렬 키-값 저장소라고 할 수 있음.

📌 병렬 키-값 저장소의 특징

  • 간단한 인터페이스 제공: put(key, value), get(key)
    • 키 기반 저장 및 조회만 지원 (선택 조건 기반 조회는 불가)
  • 레코드는 여러 머신에 자동 분산 저장
    • 복제, 자동 부하 분산, 확장성 내장
  • JSON 형식으로 데이터를 저장
    • 값에 스키마 구조가 있는 경우 쿼리 가능

3. 병렬 및 분산 DB

이번에는, 여러 개의 DB를 SQL을 이용해 엮을 순 없을까?

  • 여러 컴퓨터에서 실행되는 DB로, 대규모 쿼리를 병렬 처리 가능.
  • 1980년대부터 개발되었으며, SQL을 지원하는 데이터베이스.

📌 병렬 및 분산 DB의 특징

  • 수십~수백 대의 관계형 데이터베이스가 모여 있다고 생각하면 됨.
  • SQL 쿼리와 트랜잭션을 정상적으로 지원. (JOIN, GROUP BY 등 고급 기능 포함).
  • 그러나 다음과 같은 단점들이 존재 :
    1. 복잡한 쿼리는 실행 시간이 길어질 수 있음.
    2. 클러스터가 커질수록 장애 처리 및 복구가 어려움 (수천 대 이상에서는 특히).
  • 따라서 빠르고 확장 잘 되는 구조에서는 병렬 키-값 저장소를 쓰고, 정교한 데이터 처리/분석이 필요한 경우에는 병렬 및 분산 DB를 쓰는 경우가 많음.
  • 만약 위의 2가지 방식으로도 처리할 수 없을만큼 큰 데이터가 있는 경우, 분산 파일 시스템을 사용한다.

4. 분산 파일 시스템

(NoSQL, SQL 상관없이) 데이터베이스로 저장이 불가능할 정도로 크다면?

여러 컴퓨터에 큰 파일을 나눠서 저장하는 시스템.

  • 즉 저장 단위가 파일이며, 데이터베이스에서 사용하는 레코드나 테이블보다 훨씬 더 크다.

단, 클라이언트에게는 하나의 파일 시스템처럼 보이게 함.

  • 파일 및 디렉토리 시스템을 통해 클라이언트는 파일 위치를 알 필요 없이 접근 가능.
  • 웹 페이지, 로그, 이미지 등 비정형 대형 데이터 저장에 이상적.
  • 대표적인 시스템으로 GFS와 Hadoop이 있다.
    • Google File System (GFS): 구글에서 개발
    • Hadoop (하둡): GFS 기반의 오픈소스 시스템

분산 파일 시스템의 특징

  • 파일은 여러 블록으로 나뉘어 저장되며, 각 블록은 여러 컴퓨터에 복제되어 저장됨 (보통 3개 복제).
    • 시스템 장애 발생 시에도 접근 가능하게 함.
  • 파일 시스템은 디렉토리 및 서브디렉토리 제공
    • 파일 이름 → 블록 ID 목록 매핑
    • 각 블록 ID → 실제 블록 저장 위치(머신 ID) 매핑
  • Java, Python 등 다양한 언어 API로 HDFS 접근 가능
  • 로컬 파일 시스템과 연결하여 로컬 파일처럼 사용 가능

예시 : HDFS의 구조

NameNode: 파일 이름 → 블록 ID → 머신 ID 정보 관리

DataNode: 실제 데이터 블록 저장.

이때, 동작 방식은 다음과 같다.

  • 읽기 요청 시: NameNode가 블록 정보 + 머신 위치 반환 → 클라이언트가 해당 머신에서 직접 블록 가져옴
  • 쓰기 요청 시: 새로운 블록 ID 생성 + 머신 지정 → 클라이언트가 해당 머신에 데이터 저장

복제 및 일관성

빅데이터의 경우, 데이터의 안정성을 위한 복제 및 일관성이 더 중요해진다.

먼저, 복제의 목적을 생각해보자.

  1. 가용성 확보: 일부 머신이 고장나도 데이터 접근 가능
  2. 업데이트 시 모든 복제본에 반영 필요

복제를 한다고 칠 때, 크게 3가지 문제를 고려해야 한다.

요소 설명
Consistency (일관성) 모든 서버에 같은 값이 있음
Availability (가용성) 요청하면 무조건 응답이 옴
Partition Tolerance (네트워크 분할 내성) 서버 간 통신이 안 돼도 작동 가능

이를 분산 시스템의 3가지 균형 요소 (CAP 이론) 이라고 한다.

문제는…이 셋을 동시에 완벽하게 만족하는 시스템은 불가능하다!

그래서 시스템은 보통 Consistency vs Availability 사이에서 하나를 우선 선택해야 함.

우선순위 설명 예시
🔒 Consistency 우선 데이터를 항상 최신으로 보여줌→ 대신, 일부 상황에선 잠깐 응답 안 할 수도 있음 금융 시스템, 은행 등
⚡ Availability 우선 항상 응답은 함→ 대신, 조금 옛날 데이터를 보여줄 수 있음 SNS, 캐시 시스템 등

빅데이터 처리 시스템 (1)

데이터가 너무 커서 분산 파일 시스템에 저장을 할 수 밖에 없는 상황이라고 하자.

이럴 때, 데이터를 어떻게 처리해야 할까? 보통 쓰는 방법이 바로 MapReduce라는 기술이다.

맵리듀스 (MapReduce)

맵리듀스란? 병렬 처리를 위한 공통적인 패턴을 모델링한 패러다임으로

  • map() 함수를 통해 대량의 입력 데이터 각각에 대해 처리하고
  • reduce() 함수를 통해 그 결과를 그룹화하거나 집계하는 방식.

이 두 함수는 수십 년 전부터 함수형 프로그래밍 언어(LISP 등)와 병렬 컴퓨팅에서 사용되어 온 개념

왜 MapReduce인가?

예제 : 단어 수 세기(Word Count)

  • 수천 개의 텍스트 파일에서 등장한 모든 단어의 개수를 세는 작업이 있다고 해보자.
  • 작은 파일 1개라면 프로그램이 메모리에서 단어를 읽어가며 카운트하면 되겠지만, 수십 테라바이트의 파일이라면? 순차 처리가 불가능하므로, 병렬화가 필요!
  • 그런데 병렬 처리는 쉽지 않음. 여러 머신에서 파일을 나눠 처리할 수는 있지만, 결과 합치기, 작업 조정, 장애 복구 등은 직접 처리하려면 복잡해짐.

그래서 MapReduce는 개발자가 map() 함수와 reduce() 함수만 작성하게 하고, 나머지 병렬 처리, 오류 처리, 네트워크 통신 등은 MapReduce 시스템이 자동 처리하도록 해줌.

인제 이 예제에서, map() 함수와 reduce() 함수가 어떤 역할을 하는지 보자.

우선 다음과 같은 텍스트가 있다고 치자.

javascript
“One a penny, two a penny, hot cross buns.”

✳ map() 함수

  • 각 줄(line)을 단어 단위로 나눔
  • 각 단어를 발견할 때마다 (단어, 1) 형식으로 출력
javascript
("one", 1), ("a", 1), ("penny", 1), ("two", 1), ("a", 1), ("penny", 1), 
("hot", 1), ("cross", 1), ("buns", 1)

✳ shuffle 단계

  • map 결과 중 같은 key(단어)를 가진 것끼리 모은다.
javascript
("a", [1, 1]), ("penny", [1, 1]), ("one", [1]), ...

✳ reduce() 함수

  • key 단위로 그룹화 + 집계를 하는 게 reduce 단계의 역할!
javascript
("a", 2), ("penny", 2), ("one", 1), ...

맵리듀스의 특징 : 병렬 처리 구조

  • map()과 reduce()는 여러 머신에서 동시에 실행
    • 하나의 파일도 여러 부분으로 쪼개서 여러 map task가 처리 가능
    • reduce task는 각 단어(key)별로 그룹화된 데이터를 받음
  • map 결과는 로컬에 저장되고, reduce 단계에서는 네트워크로 해당 데이터를 받아 그룹화하여 처리함
  • 이런 전체 흐름을 자동으로 병렬화해주는 게 MapReduce의 진짜 강점!

맵리듀스용 언어

  • MapReduce는 SQL로 표현하기 어려운 연산을 쉽게 처리 가능
    • 예: 단어 수 세기, 역색인 생성, PageRank 계산 등
  • 사실 대부분의 작업은 SQL로 표현이 더 쉬움.
  • 그러나 이를 위해 데이터를 다시 관계형 DB에 집어넣는 것은 낭비일 수 있음.
  • 따라서 SQL과 유사한 맵리듀스용 언어가 등장.
시스템 설명
Hive (by Facebook) SQL 유사 언어로 HDFS 위 데이터 쿼리 가능
Pig (by Yahoo!) Pig Latin이라는 선언적 언어 사용
SCOPE (by Microsoft) SQL 스타일 언어로 MapReduce 프로그램 작성

빅데이터 처리 시스템 (2)

맵리듀스의 한계

MapReduce의 단점 : map()과 reduce()로 빅데이터를 처리하는 모든 연산을 작성하는 건 매우 번거로움.

  • 단순한 join 연산조차도 map과 reduce로 표현하면 코드가 길고 가독성이 낮아짐.
  • 따라서 join과 같은 연산을 직접적으로 표현할 수 있는 대수 연산 연산자가 필요함.

그래서, 최신 병렬 데이터 처리 시스템은 join 외에도 외부 조인 등 다양한 분석 연산자들을 직접 지원하기 시작

  • 요즘 트렌드인 머신러닝 역시 학습용 레코드를 받아 학습된 모델을 출력하는 일종의 연산자로 생각할 수 있음.
  • 이러한 연산을 연산자들의 시퀀스 또는 트리 구조로 표현하면 복잡한 데이터 처리 파이프라인 구현이 쉬워짐.

전통적인 관계대수의 한계

아니 그럼 전통적인 관계대수를 쓰면 되는 거 아닌가?

전통적인 관계 대수는 단순한 속성들로 구성된 릴레이션(표 형태) = 정형 데이터만 다룸.

  • 하지만 최신 시스템은 복잡한 자료형도 포함한 데이터 = 비정형 데이터도 처리할 수 있어야 하며, SQL 수준 이상의 표현력이 요구됨.
  • 복잡한 로직을 지원하려면 전체 프로그래밍 언어 수준의 표현 능력이 필요함.

빅데이터 처리용 프레임워크

  1. Apache Tez
  • 저수준의 API를 제공하며, 시스템 프로그래밍에 적합함.
    • Java를 이용해 작성해지만, Thread, Memory 관리, IO 버퍼링, 고성능 통신 처리 등이 가능
    • (빅데이터 처리 프레임워크 중 상대적으로 저수준에 해당하는 것으로 보면 될듯)
  • Hive on Tez: 맵리듀스용 언어인 Hive SQL을 Tez의 대수 연산 DAG로 컴파일하여 실행함.
  • Tez에서는 노드의 DAG(유향 비순환 그래프)를 구성하고, 각 노드에 실행할 코드를 지정함.
  • 데이터는 여러 노드에 나눠지고, 각 노드는 병렬로 실행됨.
  1. Apache Spark
  • 애플리케이션 개발자용 고수준 API 제공. 마찬가지로 Java로 작성
  • 다양한 저장소와 연동 가능 (HDFS, S3, RDB 등)
  • Spark의 기본 데이터 표현: RDD (Resilient Distributed Dataset)
    • “Resilient”은 장애 허용, “Distributed”는 분산 저장 의미
    • Spark의 연산자는 하나 이상의 RDD를 입력으로 받아, RDD를 출력함.
  • Spark의 특징 (1) : 병렬 실행
    • RDD는 여러 머신에 분할 저장되고, 각 연산도 해당 머신에서 병렬 실행됨
    • 예: reduceByKey()는 그룹 단위로 레코드를 한 머신으로 모아야 하므로, repartition을 수행함
  • Spark의 특징 (2) : Lazy Evaluation
    • 코드에서 flatMap() → mapToPair() → reduceByKey() 등의 연산을 해도, 실제 계산은 saveAsTextFile() 같은 액션이 호출될 때 한 번에 실행됨.
    • 이렇게 하면 쿼리 최적화가 가능하며, 쿼리 트리를 더 빠른 구조로 바꾸고 실행할 수 있음
  • Spark의 특징 (3) : AG 구조 (트리 이상의 연산도 가능)
    • 연산 결과가 여러 곳에서 재사용되면 DAG(Directed Acyclic Graph) 구조가 됨
  • Spark의 특징 (4) : 구조화된 데이터 처리 (DataSet)
    • 구조화된 데이터 = 정형 데이터 = 일반적인 릴레이션(테이블)
    • Spark는 구조화된 데이터를 위한 DataSet 타입도 제공
    • 압축 포맷 지원: Parquet, ORC, Avro
    • DB에서 읽기 위한 JDBC 커넥터도 지원

코드 예시 1 : 빅데이터 처리 예시

java
public class WordCount {
    public static void main(String[] args) throws Exception {
        if (args.length < 1) {
            System.err.println("Usage: WordCount <file-or-directory-name>");
            System.exit(1);
        }
        SparkSession spark =
                SparkSession.builder().appName("WordCount").getOrCreate();

        // 입력 데이터 불러오기
        // 각 줄이 하나의 레코드가 되고, 내부적으로 여러 노드에 데이터를 분할하여 병렬 처리
        JavaRDD<String> lines =
                spark.read().textFile(args[0]).javaRDD();

        // 람다식을 이용해 단어 단위로 나누고, 반복자를 반환
        // flatMap()은 1:N 매핑을 처리함
        JavaRDD<String> words =
                lines.flatMap(s -> Arrays.asList(s.split(" ")).iterator());

        // (word, 1) 쌍 생성
        JavaPairRDD<String, Integer> ones =
                words.mapToPair(s -> new Tuple2<>(s, 1));

        // 단어별 합산: reduceByKey()
        JavaPairRDD<String, Integer> counts =
                ones.reduceByKey((i1, i2) -> i1 + i2);

        // 출력 저장
        counts.saveAsTextFile("outputDir");
        List<Tuple2<String, Integer» output = counts.collect();
        for (Tuple2<String,Integer> tuple : output) {
            System.out.println(tuple);
        }
        spark.stop();
    }
}

코드 예시 2 : Dataset 타입을 이용한 구조화된 데이터 읽기 + 필터링 + 조인 + 그룹화

java
Dataset<Row> instructor = spark.read().parquet("...");
Dataset<Row> department = spark.read().parquet("...");
instructor.filter(instructor.col("salary").gt(100000))
	.join(department, instructor.col("dept name")
	.equalTo(department.col("dept name")))
	.groupBy(department.col("building"))
	.agg(count(instructor.col("ID")));

코드 예시 3 : 클래스 기반 접근

java
Dataset<Instructor> instructor = spark.read()
																	.parquet("...")
																	.as(Encoders.bean(Instructor.class));
  • parquet이라는 압축 파일 형식 → Dataset 형태로 변환하는 코드
  • 이때 Instructor 클래스는 자바 Bean 형식이어야 함.
    • 자바 Bean 형식? 자바 객체의 속성에 접근할 수 있는 표준 방법을 정해놓은 규칙
    • 기본 생성자, private 필드, getter 메소드, setter 메소드가 바로 그것
  • 저 코드의 작동 방식은 다음과 같음.
    1. Parquet 파일에서 "name", "salary" 같은 컬럼명을 읽음
    2. Instructor 클래스에서 getName(), getSalary() 같은 메서드를 찾음
    3. 해당 속성에 자동으로 매핑해서 객체로 만들어줌

스트리밍 데이터

특정 어플리케이션은 계속해서 도착하는 데이터를 실시간으로 처리해야 함.

이런 연속적인 데이터를 스트리밍 데이터(streaming data)라고 부름

  • 예시 : 주식 시장, 전자상거래, 센서 데이터, 네트워크, SNS

스트리밍 데이터의 문제점

스트리밍 데이터: 계속 흐르는 데이터, 끝이 없음 (Unbounded)

  • “스트림에 튜플이 몇 개 있는가?” → 끝이 없기 때문에 정답을 낼 수 없음
  • 그렇다면, 특정 범위를 기준으로 스트림을 자르면 되지 않을까? → 이를 윈도우라고 한다.
    • ex) 10:00 ~ 10:59까지 도착한 모든 튜플
  • 스트리밍 데이터를 처리 시 우선 윈도우가 완료되었음을 확인할 수 있어야 함.
    • 타임스탬프 기반 or punctuation(삽입된 메타 튜플) 사용

스트리밍 데이터의 질의

① 📡 Continuous Queries

  • 스트림을 기존 RDBMS에 계속 insert로 처리
  • 사용자는 “조건에 맞는 insert만 계속 모니터링” 하게 됨
  • 단점: 너무 많은 업데이트가 발생함 → 집계 용도에는 부적합

② 🧾 Stream Query Languages

  • SQL/관계대수 확장 형태

  • 반드시 윈도우를 지정한 후에 관계 연산 수행

  • 다양한 윈도우 지원:

    🧱 Tumbling: 겹치지 않는 시간대별 윈도우

    🔁 Hopping: 일정 간격마다 중복 가능 윈도우

    🪟 Sliding: 각 튜플을 중심으로 하는 윈도우

    🕒 Session: 사용자 세션 기준 윈도우

  • 예시 : 시간대별 아이템 구매 합계 예시 (Azure Stream Analytics 구문)

java
SELECT itemid, System.Timestamp as window_end, SUM(amount)
FROM order TIMESTAMP BY datetime
GROUP BY itemid, tumblingwindow(hour, 1)

③ ⚙️ Algebraic Operators on Streams

  • 각 튜플에 대해 실행되는 사용자 정의 연산자를 작성함
  • 연산자 사이에 DAG(Directed Acyclic Graph)를 구성해 데이터 흐름을 정의함
  • 대표 시스템:
    • 🌀 Apache Storm (노드: Spout, Bolt)
    • ☕ Apache Kafka (Pub/Sub 기반 라우팅)
    • ⚡ Apache Flink (Window 지원, DAG 기반)

④ 🔍 Pattern Matching (CEP 시스템)

  • 특정 패턴이 발견되면 규칙에 따라 즉시 반응
    • ex) 5분 간격으로 로그인 실패 3회 → 알림
  • 시스템 예시:
    • Oracle Event Processing
    • Microsoft StreamInsight
    • FlinkCEP (Apache Flink의 일부)

스트리밍 앱의 구조

처리하는 방법은 알았지만, 저장은 어떻게 할까? 또 기존의 데이터는 어떻게 해야 할까?

스트리밍 앱의 경우, 만들기 전 이러한 고민들을 해야 한다.

그래서 나온 구조가 바로 Lambda Architecture (람다 아키텍쳐)!

람다 아키텍쳐

이 구조에서는 기본적으로 데이터를 2가지로 구분한다는 전제를 깐다.

  1. 스트리밍 데이터 : 시간의 구분이 없이 계속 들어오는 데이터 (연속적임)
  2. 배치 데이터 : 매 시간마다 규칙적으로 들어오는 데이터 (이산적임)

2가지 데이터를 위해 앱을 크게 3가지 레이어로 나눠서 설계한다.

레이어 설명 기술 예시
🗃️ Batch Layer 대량 데이터를 모아서 정기적으로 처리함 → 정확한 결과 보장 Hadoop, Spark
Speed Layer 최신 데이터를 실시간으로 처리 → 빠른 응답 Apache Storm, Spark Streaming, Flink
📦 Serving Layer 위 두 레이어에서 나온 결과를 통합해서 조회 Cassandra, HBase, Druid, Elasticsearch
특징 설명
🕓 배치는 느리지만 정확 오래 걸려도 전체 데이터를 정확하게 처리 가능
⚡ 스트리밍은 빠르지만 가끔 부정확 실시간 반영은 되지만, 장애나 누락 가능성 존재
🧠 둘을 합치면…? 빠르고 정확한 하이브리드 시스템 완성!

물론 장점만 있는 건 아니다.

문제점 설명
💥 시스템 복잡성 같은 로직을 배치/스트리밍에 두 번 구현해야 함 (중복 코드)
🧪 테스트 어려움 두 레이어 동기화가 잘 안 되면 버그 발생
💾 저장소도 두 개 배치 뷰, 실시간 뷰 따로 관리 필요

스트리밍 데이터의 연산

그럼, 이러한 스트리밍 데이터의 질의를 어떻게 효율적으로 처리할 수 있을까?

  • Spark 같은 시스템은 스트림을 알고리즘 처리 단위로 쪼개서 처리
    • 이를 “Discretized Streams (DStream)”이라고 부름
    • ex) Spark는 1분 단위로 스트림을 나누고, 그걸 일반 데이터셋처럼 처리함
  • 하지만 연산 전에 미리 윈도우를 나눠야 함 → 유연성 ↓

💡 대안: Flink / Storm 방식

  • 연산 자체가 스트림 → 스트림 구조
    • ex) map, filter, select 등은 즉시 가능
    • 집계나 reduce는 윈도우 단위로만 출력 가능
  • 결과적으로 모든 출력은 타임스탬프를 포함한 스트림 형태로 처리됨

그래프 데이터

그래프 데이터와 그래프 DB

  • 최근에는 네트워크 데이터가 늘어나고 있음. ex) SNS
  • 이러한 데이터 역시 아래와 같이 SQL로 표현을 할 수 있으나, 제한적임
sql
node(ID, label, node_data)
edge(fromID, toID, label, edge_data)
  • 따라서 나온 것이 바로 그래프 DB!

그래프 DB의 특징

Neo4j 같은 그래프 DBMS는 다음과 같은 추가 기능을 제공함:

✅ 노드와 엣지를 명시적으로 정의할 수 있는 문법 지원

경로 질의(path query)를 위한 특화된 쿼리 언어 제공 (예: Cypher)

✅ 일반 SQL보다 빠른 질의 처리

🔍 예시: 조인처럼 관계 질의하기

  • 학생이 교수님에게 지도받는 관계(advisor)라고 하자.
  • Cypher라는 언어를 이용
sql
MATCH (i:instructor)<-[:advisor]-(s:student)
WHERE i.dept_name = 'Comp. Sci.'
RETURN i.ID AS ID, i.name AS name, collect(s.name) AS advisees
  • (:instructor)<-[:advisor]-(:student) → 그래프 상에서 학생 → 교수 방향의 advisor 엣지를 의미
  • collect()는 그룹바이 후 리스트를 만듦
  • SQL로는 복잡한 조인을 Cypher는 훨씬 직관적으로 처리 가능

🔁 예제 : 재귀 질의

과목 간 선수과목 관계 (prereq) 그래프를 따라가며 모든 간접 선수과목까지 탐색

sql
MATCH (c1:course)-[:prereq*1..]->(c2:course)
RETURN c1.course_id, c2.course_id
  • *1.. : 최소 1개 이상의 엣지를 따라가는 경로를 의미
  • 재귀적 경로 탐색이 가능함

대규모 그래프 DB

수십억 노드와 엣지를 다루는 경우에는 병렬 처리가 필수

① 🧮 MapReduce / Spark 기반 처리

  • 그래프를 관계형 테이블처럼 표현하여, Join 연산을 반복
  • Spark, Hadoop, 병렬 RDBMS 사용 가능
  • 🔁 단점: 매 반복마다 전체 그래프를 읽음 → 비효율적

② 🧠 BSP (Bulk Synchronous Processing) 프레임워크

Pregel (Google), Apache Giraph, GraphX (Spark)에 의해 구현됨

  • 각 **노드에 상태(state)**를 유지하며 알고리즘 실행
  • 반복(Iteration) 단위를 superstep이라 부름
  • 각 노드는 메시지를 받아 처리하고, 다음 노드로 전달 가능
특징 설명
💌 메시지 기반 처리 이웃 노드끼리 메시지 전달
🔁 반복적(superstep) 연산 모든 노드가 멈추면 알고리즘 종료
🧠 병렬화 쉬움 각 노드 연산을 병렬로 실행 가능