티스토리 뷰

반응형

맵리듀스 질의

맵리듀스(MapReduce)는 많은 컴퓨터에서 대량의 데이터를 처리하기 위한 프로그래밍 모델로, 구글에 의해 널리 알려졌다. 몽고DB와 카우치DB를 포함한 일부 NoSQL 데이터 저장소는 제한 된 형태의 맵리듀스를 지원한다. 이 메커니즘은 많은 문서를 대상으로 읽기 전용(read-only) 질의를 수행할 때 사용한다.

전반적인 맵리듀스는 10장에서 자세히 설명하고 지금은 몽고DB의 모델사용에 대해 간단히 살펴본다.

맵리듀스는 선언형 질의 언어도 완전한 명령형 질의 API도 아닌 그 중간 정도에 있다. 질의 로직 처리 프레임워크가 반복적으로 호출하는 조각 코드로 표현한다. 맵리듀스는 여러 함수형 프로그래밍 언어에 있는 map(collect라고도 함)과 reduce(fold나 inject라고도 함)함수를 기반으로 한다.

예를 들어 해양 생물학자가 되어 바다에서 동물을 볼 때마다 데이터베이스에 관찰 기록을 추가한다고 해보자. 지금부터 한 달에 얼마나 자주 상어를 발견하는지 보고서를 작성하려고 한다.

포스트그레스큐엘에서는 다음과 같이 질의를 표현할 수 있다.

SELECT date_trunc('month', eobservation_timestamp) AS observation_month, 
       sum(num_animals) AS total_animals
FROM observations
WHERE family = 'Sharks'
GROUP BY observation_month;

이 질의는 먼저 상어과에 속하는 종만 보이도록 관측치를 필터링한 다음, 관측치가 발생한 달력의 월로 그룹화하고 마지막으로 해당 달의 모든 관측치에 보여진 동물 수를 합친다.

같은 내용을 몽고DB의 맵리듀스 기능을 이용해 다음과 같이 표현할 수 있다.

db.observations.mapReduce(
	function map() {
    	var year = this.observationTimestamp.getFullYear();
        var month = this.observationTimestamp.getMonth() + 1;
        emit(year + '-' + month, this.numAnimals);
    },
    function reduce(key, values) {
    	return Array.sum(values);
    },
    {
    	query: {family: 'sharks'},
        out: 'monthlySharkReport'
    }
);

예를 들어 observations 컬렉션에 다음의 두 문서가 있다고 가정해보자.

{
	observationTimestamp: Date.parse('Mon, 25 Dec 1995 12:34:56 GMT'),
    family: 'sharks',
    species: 'carcharodon carcharias',
    numAnimals: 3
}
{
	observationTimestamp: Date.parse('Tue, 12 Dec 1995 16:17:18 GMT'),
    family: 'sharks',
    species: 'carcharias taurus',
    numAnimals: 4
}

map 함수는 각 문서마다 한 번 호출 되고 결국 emit('1995-12', 3)과 emit('1995-12', 4)가 실행된다. 그 뒤에 reduce 함수는 reduce('1995-12', [3,4])로 호출되고 7을 반환한다.

몽고DB의 map과 reduce 함수는 수행할 때 약간 제약 사항이 있다. 두 함수는 순수(pure) 함수여야한다. 즉 입력으로 전달된 데이터만 사용하고 추가적인 데이터베이스 질의를 수행할 수 없어야하며 부수 효과(side effect)가 없어야 한다. 이런 제약 사항 때문에 데이터베이스가 임의 순서로 어디서나 이 함수를 실행할 수 있고 장애가 발생해도 함수를 재실행 할 수 있다. 이런 제약 사항이 있어도 map과 reduce 함수는 여전히 강력하다. 문자열을 파싱하고 라이브러리 함수를 호출하고 계산을 실행 하는 등의 작업을 map과 reduce 함수에서 할 수 있다.

클러스터 환경에서 분산 실행을 위한 프로그래밍 모델인 맵리듀스는 상당히 저수준 프로그래밍 모델이다. SQL 같은 고수준 질의 언어도 맵리듀스 연산의 파이프라인으로 구현할 수 있지만(10장 참고) 맵리듀스를 사용하지 않은 분산 SQL 구현도 많다. SQL에 단일 장비에서 수행되도록 제한하는 것은 없으며 맵리듀스가 분산 질의 실행에 대한 독점권을 가진 것도 아니다.

질의 중간에 자바스크립트 코드를 사용할 수 있다는 점은 고급 질의가 가능한 훌륭한 기능이지만 맵 리듀스에만 해당하는 것은 아니다. 일부 SQL 데이터베이스도 자바스크립트 함수로 확장될 수 있다. (https://blog.heroku.com/javascript_in_your_postgres

맵리듀스의 사용성 문제는 연계된 자바스크립트 함수 두 개를 신중하게 작성해야 한다는 점인데 이는 종종 하나의 질의를 작성하는 것보다 어렵다. 더욱이 선언형 질의 언어는 질의 최적화기가 질의 성능을 높일 수 있는 기회를 제공한다. 이런 이유로 몽고DB 2.2는 집계 파이프라인(aggregation pipeline)이라 부르는 선언형 질의 언어 지원을 추가했다. 이 언어에서 앞의 상어 수 세기 질의는 다음과 같다.

db.observations.aggregate([
	{ $match: { family: 'sharks' } },
    { $group: {
    	_id: {
        	year: { $year: '$observationTimestamp' },
            month: { $month: '$observationTimestamp' }
        },
        totalAnimals: { $sum: '$numAnimals' }
    } }
]);

집계 파이프라인 언어는 표현 측면에서 SQL의 부분 집합과 유사하지만 SQL의 영어 문장 스타일 구문보다는 JSON 기반 구문을 사용한다. 아마도 이 차이는 취향의 문제일 것이다. 여기서 배울 점은 NoSQL 시스템이 뜻하지 않게 SQL을 재발견하고 있다는 점이다.

그래프형 데이터 모델

앞에서 다대다 관계가 다양한 데이터 모델을 구별하는 중요한 기능임을 살펴봤다. 애플리케이션이 주로 일대다 관계(트리 구조 데이터)이거나 레코드 간 관계가 없다면 문서 모델이 적합하다.

하지만 데이터에서 다대다 관계가 매우 일반적이라면 어떻게 해야할까? 관계형 모델은 단순한 다대다 관계를 다룰 수 있지만 데이터간 연결이 더 복잡해지면 그래프로 데이터를 모델링하기 시작하는 편이 더 자연스럽다.

그래프는 두 유형의 객체로 이뤄진다. 정점(vertex)(노드 나 엔티티 라고 한다)과 간선(edge)(관계나 호(arc)라고도 한다)이다. 많은 유형의 데이터를 그래프로 모델링 할 수 있다. 일반적인 예는 다음과 같다.

소셜 그래프

정점은 사람이고 간선은 사람들이 서로 알고 있음을 나타낸다.

웹 그래프

정점은 웹 페이지고 간선은 다르페이지에 대한 HTML 링크를 나타낸다.

도로나 철도 네트워크

정점은 교차로이고 간선은 교차로 간 도로나 철로 선을 나타낸다.

이 같은 그래프 상에서 동작하는 잘 알려진 여러 알고리즘이 있다. 예를 들어 자동차 내비게이션 시스템은 도로 네트워크에서 두 지점간 최단 경로를 검색하고 페이지랭크(pagerank)는 웹 그래프를 사용해 웹 페이지의 인기와 검색 결과에서 순위를 결정할 수 있다.

방금 전 예제에서 그래프의 정점은 모두 같은 유형(사람이나 웹 페이지, 도로 교차로)을 나타낸다. 하지만 그래프는 이런 동종 데이터에 국한되지 않는다. 그래프를 종종 데이터와 마찬가지 방식으로 사용하면 단일 데이터 저장소에 완전히 다른 유형의 객체를 일관성 있게 저장할 수 있는 강력한 방법을 제공한다. 예를 들어 페이스북은 다른 여러 유형의 정점과 간선을 단일 그래프로 유지한다. 정점은 사람, 장소, 이벤트, 체크인, 사용자가 작성한 코멘트를 나타낸다. 간선은 어떤 사람이 서로 친구인지 어떤 위치에서 체크인이 발생했는지 누가 어떤 포스트에 코멘트를 했는지 누가 이벤트에 참석했는지 등을 나타낸다.

이번 절에서는 다음 그림 예제를 사용한다. 어떤 소셜 네트워크나 계보 데이터베이스에서 가져온 예제로 아이다호(Idaho) 출신의 루시(Lucy)와 프랑스 본(Beaune) 출신의 알랭(Alain) 두 사람을 보여준다. 둘은 결혼해서 런던에 살고 있다.

그래프에서 데이터를 구조화하고 질의하는 몇 가지 다른(하지만 관련된) 방법이 있다. 이번 절에서는 속성 그래프 모델(네오포제이(Neo4j), 타이탄(Titan), 인피니티그래프(InfiniteGraph)로 구현됨)과 트리플 저장소 모델(데이토믹(Datomic), 알레그로그래프(Allegrograph) 등으로 구현됨)을 설명하고 그래프용 선언형 질의 언어 세 가지, 사이퍼(Cypher)와 스파클(SPARQL), 데이터로그(Datalog)를 살펴본다. 이 밖에도 그렘린(Gremlin)과 같은 명령형 그래프 질의 언어와 프리글(Pregel)과 같은 그래프 처리 프레임워크가 있다.

속성 그래프

속성 그래프 모델에서 각 정점은 다음과 같은 요소로 구성된다.

- 고유한 식별자
- 유출(outgoing) 간선 집합
- 유입(incoming) 간선 집합
- 속성 컬렉션(키-값 쌍)

각 간선은 다음과 같은 요소로 구성된다.

- 고유한 식별자
- 간선이 시작하는 정점(꼬리 정점)
- 간선이 끝나는 정점(머리 정점)
- 두 정점 간 관계 유형을 설명하는 레이블
- 속성 컬렉션(키-값 쌍)

다음 예제와 같이 두 개의 관계형 테이블(하나는 정점이고 하나는 간선)로 구성된 그래프 저장소를 생각해보자. 다음 예제 스키마는 각 정점이나 간선 속성 저장을 위해 postgresql json 데이터 타입을 사용했다. 머리와 꼬리 정점은 각 간선마다 저장된다. 정점을 위한 유입 간선과 유출 간선의 집합이 필요하다면 edges 테이블에 head_vertex나 tail_vertex로 각각 질의할 수 있다.

CREATE TABLE vertices ( 
	vertex_id integer PRIMARY KEY,
    properties json
);

CREATE TABLE edges (
	edge_id integer PRIMARY KEY,
    tail_vertex integer REFERENCES vertices (vertex_id),
    head_vertex integer REFERENCES vertices (vertex_id),
    label text,
    properties json
);

CREATE INDEX edges_tails ON edges (tail_vertex);
CREATE INDEX edges_heads ON edges (head_vertex);

이 모델의 몇 가지 중요한 면은 다음과 같다.

1. 정점은 다른 정점과 간선으로 연결된다. 특정 유형과 관련 여부를 제한하는 스키마는 없다.
2. 정점이 주어지면 정점의 유입과 유출 간선을 효율적으로 찾을 수 있고 그래프를 순회할 수 있다. 즉 일련의 정점을 따라 앞 뒤 방향으로 순회한다. 예제에서 tail_vertex와 head_vertex 컬럼에 대해 색인을 생성하는 이유다.
3. 다른 유형의 관계에 서로 다른 레이블을 사용하면 단일 그래프에 다른 유형의 정보를 저장하면서도 데이터 모델을 깔끔하게 유지할 수 있다.

그림에서 볼 수 있었듯이 이런 기능을 통해 그래프는 데이터 모델링을 위한 많은 유연성을 제공한다. 그림은 전통적인 관계형 스키마에서 표현하기 어려운 사례 몇 가지를 보여준다. 예를 들어 국가마다 지역 구조가 다르다(프랑스에서는 주, 도인 반면 미국에서는 군, 주다) 국가 안의 국가처럼 역사의 굴곡(독립된 주와 국가의 복잡성을 무시)이 있는 경우도 있고 데이터 입도(granularity)(루시의 현재 거주지는 도시로 명시돼 있지만 출생지는 주 수준으로만 명시됨)가 가지각색이다.

루시와 알랭 또는 그 밖의 사람에 대한 다른 여러 사실을 포함시키기 위해 그래프를 확장한다고 생각해보자. 예를 들어 그들이 가진 음식 알레르기를 나타내고(알레르겐(allergen)은 정점으로 두고 사람과 알레르겐은 간선으로 구성해 알레르기를 표현) 어떤 음식에 어떤 물질이 포함됐는지 보여주는 정점 집합과 알레르겐을 연결할 때 사용할 수 있다. 그러면 각 사람들이 먹을 수 있는 안전한 음식이 무엇인지 알아내는 질의 작성이 가능하다. 그래프는 발전성이 좋아서 애플리케이션에 기능을 추가하는 경우 애플리케이션의 데이터 구조 변경을 수용하게끔 그래프를 쉽게 확장할 수 있다.

사이퍼 질의 언어

사이퍼(Cypher)는 속성 그래프를 위한 선언형 질의 언어로, Neo4j 그래프 데이터베이스용으로 만들어졌다. 사이퍼는 영화 매트릭스에 나오는 등장 인물의 이름으로 ,암호학에서의 사이퍼(cipher)와는 관련이 없다.

다음 예제에서는 이전 그림의 왼쪽 부분을 그래프 데이터베이스로 삽입하는 사이퍼 질의를 보여준다. 나머지 그래프도 비슷하게 추가할 수 있지만 가독성을 위해 생략했다. 각 정점에는 미국이나 아이다호 같은 상징적인 이름이 지정되어 있다. 질의의 다른 부분에서 이 이름을 사용해 정점간 간선을 화살표 표기를 사용해 만들 수 있다. 즉, (Idaho) -[:WITHIN] -> (USA)의 경우 꼬리 노드는 Idaho, 머리 노드는 USA인 WITHIN 레이블의 간선이 된다.

CREATE
	(NAmerica:Location {name: 'North America', type: 'continent'}),
    (USA:Location {name: 'United States', type: 'country' }),
    (Idaho:Location {name: 'Idaho', type: 'state' }),
    (Lucy:Person {name: 'Lucy' }),
    (Idaho) -[:WITHIN]->(USA) -[:WITHIN]-> (NAmerica),
    (Lucy) -[:BORN_IN]->(Idaho)

그림에서 모든 정점과 간선을 데이터베이스에 추가하면 흥미로운 질문 하나를 던질 수 있다. 예를 들어 미국에서 유럽으로 이민 온 모든 사람들의 이름 찾기 같은 질문이다. 더 정확하게 말하면 미국 내 위치의 BORN_IN 간선을 가진 정점과 유럽 내 위치의 LIVING_IN 간선을 갖는 모든 정점을 찾아서 이 정점들의 NAME 속성을 반환하는 문제다.

다음 예제에서는 사이퍼에서 해당 질의를 어떻게 표현하는지 보여준다. MATCH 문에서는 같은 화살표 표기를 이용해 그래프에서 패턴을 찾는다. (person) -[:BORN_IN] -> () 는 BORN_IN 레이블을 가진 간선과 관련된 두 정점을 찾는다. 이 간선의 꼬리 정점은 person 변수에 묶여 있고 머리 정점은 명기하지 않은 채로 남아있다.

MATCH
	(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name: 'United States'}),
    (person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name: 'Europe'})
RETURN person.name

질의는 ㅏ음과 같이 읽힌다.

다음 두 가지 조건을 만족하는 정점(person이라 부름)을 찾아라

1. person은 어떤 정점을 향하는 BORN_IN 유출 간선을 가진다. 이 정점에서 name 속성이 'United States'인 Location 유형의 정점에 도달할 때까지 일련의 WITHIN 유출 간선을 따라간다.
2. 같은 person 정점은 LIVES_IN 유출 간선도 가진다. 이 간선과 WITHIN 유출 간선을 따라가면 결국 name 속성이 'Europe'인 Location 유형의 정점에 도달하게 된다.

각 person 정점마다 name 속성을 반환한다.

질의를 실행하는 데는 여러 가지 방법이 있다. 여기에 설명된 내용은 데이터베이스에서 모든 사람을 훑어보는 작업을 시작으로 사람들의 출생지와 거주지를 확인해 기준에 맞는 사람들만 반환한다.

하지만 동일하게 두 개의 Location 정점에서 시작해 반대 방향으로 수행할 수 있다. name 속성에 색인이 있다면 미국과 유럽을 나타내는 두 개의 정점을 효율적으로 찾는다. 그다음 각각의 WITHIN 유입 간선을 따라가며 미국과 유럽의 모든 위치(주, 지역, 도시 등) 찾기를 진행한다. 마지막으로 위치를 나타내는 정점 중 하나에서 BORN_IN 이나 LIVES_IN 유입 간선을 통해 발견된 사람들을 구한다.

ㅂ토오 선언형 질의 언어는 질의를 작성할 때 이처럼 수행에 대해 자세히 지정할 필요가 없다. 질의 최적화기가 가장 효율적이라고 예측한 전략을 자동으로 선택하므로 작성자는 나머지 애플리케이션만 작성하면 된다.

SQL의 그래프 질의

전 예제에서는 관계형 데이터베이스에 그래프 데이터를 표현할 수 있음을 제안했다. 그렇다면 그래프 데이터를 관계형 구조로 넣어도 SQL을 사용해 질의할 수 있을까?

대답은 '예'지만 약간 어렵다. 관계형 데이터베이스에서는 대개 질의에 필요한 조인을 미리 알고 있다. 그래프 질의에서는 찾고자 하는 정점을 찾기 전에 가변적인 여러 간선을 순회해야 한다. 미리 조인 수를 고정할 수 없다.

예제에서 간선 순회는 사이퍼 질의의 () -[:WITHIN*0..]-> () 문에서 발생한다. person의 LIVES_IN 간선은 위치 유형(거리, 도시, 지구, 군, 주 등)을 가리킨다. 도시는 군으로, 군은 주로, 주는 국가와 WITHIN 간선으로 이어진다. LIVES_IN 간선은 찾고자 하는 위치 정점을 가리킬 수 있으나 위치 계층 구조에서 제거된 일부 수준일 수 도 있다.

사이퍼에서 :WITHIN*0..는 이 사실을 매우 간결하게 표현한다. 이는 '0회 이상 WITHIN 간선을 따라가라'는 의미다. 정규 표현식의 * 연산자와 같다.

SQL:19999 이후로 가변 순회 경로에 대한 질의 개념은 재귀 공통 테이블 식(recursive common table exrpression)(WITH RECURSIVE 문)을 사용해서 표현할 수 있다. 다음 예제는 이 기법을 사용해 동일한 질의(미국에서 유럽으로 이민 온 사람들의 이름 찾기)를 SQL로 표현했다.(postgreSQL, IBM DB2, Oracle, SQL 서버에서 지원함). 하지만 사이퍼와 비교하면 문법이 매우 어렵다.

WITH RECURSIVE
	-- in_usa는 미국 내 모든 지역의 정점 ID 집합이다.
    in_usa(vertex_id) AS (
    	SELECT vertex_id FROM vertices WHERE properties->>'name' = 'United States' -- 1
    UNION
    	SELECT edges.tail_vertex FROM edges -- 2
        	JOIN in_usa ON edges.head_vertex = in_usa.vertex_id 
            WHERE edges.label = 'within'
    ),
    -- in_europe은 유럽 내 모든 지역의 정점 ID 집합이다.
    in_europe(vertex_id) AS ( 
    	SELECT vertex_id FROM vertices WHERE properties->>'name' = 'Europe' -- 3
    UNION
    	SELECT edges.tail_vertex FROM edges
        	JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
            WHERE edges.label = 'within'
    ),
    -- born_in_usa는 미국에서 태어난 모든 사람의 정점 ID 집합이다.
    born_in_usa(vertex_id) AS ( -- 4
    	SELECT edges.tail_vertex FROM edges
        	JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
            WHERE edges.label = 'born_in'
    ),
    -- lives_in_europe은 유럽에서 태어난 모든 사람의 정점 ID 집합이다.
    lives_in_europe(vertex_id) AS ( -- 5
    	SELECT edges.tail_vertex FROM edges
        	JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
            WHERE edges.label = 'lives_in'
    )
    
SELECT vertices.properties->>'name'
FROM vertices
-- 미국에서 태어나 유럽에서 자란 사람을 찾아 조인한다.
JOIN born_in_usa ON vertices.vertex_id = born_in_usa.vertex_id -- 6
JOIN lives_in_europe ON verticies.vertex_id = lives_in_europe.vertex_id;

1. 먼저 name 속성이 'United States'인 정점을 찾아 in_usa 정점 집합의 첫 번째 엘리먼트로 만든다.
2. in_usa 집합의 정점들의 모든 within 유입 간선을 따라가 같은 집합에 추가한다. 모든 within 간선을 방문할 때까지 수행한다.
3. name 속성이 'Europe'인 정점을 시작으로 동일하게 수행해 in_europe 집합을 만든다.
4. 미국에서 태어난 사람을 찾기 위해 in_usa 집합의 각 정점에 대해 born_in 유입 간선을 따라간다.
5. 비슷하게 유럽에서 사는 사람을 찾기 위해 in_europe 집합의 각 정점에 대해 lives_in 유입 간선을 따라간다.
6. 마지막으로 조인을 이용해 미국에서 태어난 사람 집합과 유럽에서 사는 사람 집합의 교집합을 구한다.

동일한 질의를, 하나의 질의 언어는 4줄로 작성하고 다른 질의 언어는 29줄로 작성해야 한다는 점이다. 이는 다양한 데이터 모델이 서로 다른 사용 사례를 만족하기 위해 설계됐다는 사실을 보여준다. 따라서 애플리케이션에 적합한 데이터 모델 선택하는 작업은 중요하다.

트리플 저장소와 스파클

트리플 저장소 모델은 속성 그래프 모델과 거의 동등하다. 이 모델은 같은 생각을 다른 용어를 사용해 설명한다. 그럼에도 애플리케이션 구축에 유용한 도구일지도 모를 트리플 저장소를 위한 다양한 도구와 언어가 있기 때문에 논의할 만한 가치가 있다.

트리플 저장소에서는 모든 정보를 (주어(subeject), 서술어(predicate), 목적어(object)) 처럼 매우 간단한 세 부분 구문(three-part statements) 형식으로 저장한다. 예를 들어 ( 짐, 좋아하다, 바나나 ) 트리플에서 짐은 주어, 좋아하다는 서술어(동사), 바나나는 목적어다.

트리플의 주어는 그래프의 정점과 동등하다. 목적어는 두 가지 중 하나다.

1. 문자열이나 숫자 같은 원시 데이터타입의 값, 이 경우 트리플의 서술어와 목적어는 주어 정점에서 속성의 키, 값과 동등하다. 예를 들어(루시, 나이, 33)은 {'age': 33} 속성을 가진 정점 lucy와 같다.
2. 그래프의 다른 정점 이 경우 서술어는 그래프의 간선이고 주어는 꼬리 정점이며 목적어는 머리 정점이다. 예를 들어 (루시, 결혼하다, 알랭)에서 주어와 목적어인 루시와 알랭은 모두 정점이고 서술어 결혼하다는 두 정점을 잇는 간선의 레이블이다.

다음 예제는 이전 예제와 동일한 데이터를 보여주며 터틀(Turtle) 형식의 트리플로 작서오댔다. 터틀 형식은 Notation3(N3)의 부분집합이다.

@prefix : <urn:example:>.
_:lucy		a		:Person.
_:lucy		:name	"Lucy"
_:lucy		:bornIn	_:idaho.
_:idaho		a		:Location.
_:idaho		:name	"Idaho".
_:idaho		:type	"state".
_:idaho		:within	_:usa.
_:usa		a		:Location.
_:usa		:name	"United States".
_:usa		:type	"country".
_:usa		:within	_:namerica.
_:namerica	:a		:Location.
_:namerica	:name	"North America".
_namerica	:type	"continent".

위 예제에서는 그래프 정점을 _:someName으로 썼다. _:someName은 이 파일 외부의 것을 의미하지 않는다. _:someName은 트리플이 같은 정점을 참조하는지 달리 알지 못하기 때문에 존재한다. 서술어가 간선을 나타내면 목적어는 _:idaho :within _:usa처럼 정점이 된다. 서술어가 속성이라면 목적어는 _:usa :name "United States"처럼 문자열 리터럴이 된다.

동일한 주어를 반복하는 작업은 단순 반복 작업이다. 다행히 세미콜론을 사용해 동일한 주어에 대해 여러 경우를 표현할 수 있다. 이 방식은 터틀 형식을 상당히 멋지고 읽기 쉽게 만든다. 다음 예제를 보면 잘 나타난다.

@prefix : <urn:example:>.

_:lucy	a :Person;	:name	"Lucy";		:bornIn	_:Idaho
_:idaho	a :Location;	:name	"Idaho";	:type	"state";	:within	_:usa
_:usa	a :Location;	:name	"United States";	:type	"country";	:within	_:namerica
_:namerica	a	:Location;	:name	"North America";	:type	"continent".

 

시맨틱 웹

트리플 저장소에 관한 내용을 읽다보면 시맨틱 웹 관련 기사의 소용돌이에 빠질지도 모른다. 트리플 저장소 데이터 모델은 시맨틱 웹과는 완전히 독립적이다. 예를 들어 데이토믹은 트리플 저장소지만 시맨틱 웹과의 어떤 관계도 주장하지 않는다. 하지만 많은 사람들이 이 둘은 매우 밀접한 관계가 있다고 생각하기 때문에 간략하게 논의할 필요가 있다.

시맨틱 웹은 기본적으로 간단하고 합리적인 개념이다. 즉 웹 사이트는 이미 사람이 읽을 수 있는 텍스트와 그림으로 정보를 게시하고 있으니 컴퓨터가 읽게끔 기계가 판독 가능한 데이터로도 정보를 게시하는 건 어떨까라는 개념이다. 자원 기술 프레임워크(Resource Description Framework, RDF)는 서로 다른 웹 사이트가 일관된 형식으로 데이터를 게시하기 위한 방법을 제안한다. RDF는 서로 다른 웹사이트의 데이터가 일종의 전 인터넷 '만물 데이터베이스(database of everything)'인 데이터 웹(web of data)에 자동으로 결합할 수 있게 한다.

안타깝게도 시맨틱 웹은 2000년대 초반에 과대평가됐고 지금까지 현실에서 실현된 흔적이 없어 많은 사람이 부정적인 견해를 보였 다. 또한 어지러운 약어의 과잉과 지나치게 복잡한 표준 제안, 자만심으로 어려움을 겪었다.

하지만 이런 단점들이 있음에도 시맨틱 웹 프로젝트에서 유래한 좋은 작업이 많이 있다. 시맨틱 웹에서 RDF 데이터를 게시하는 일에 관심이 없다고 하더라도 트리플은 애플리케이션의 훌륭한 내부 데이터 모델이 될 수 있다.

RDF 데이터 모델

바로 이전 예제에서 사용한 터틀 언어는 RDF 데이터를 사람이 읽을 수 있는 형식(human-readable format)으로 표현한다. 때로는 RDF를 XML 형식으로 쓰기도 한다. 하지만 XML 형식은 다음 예제와 같이 같은 내용을 훨씬 장황하게 만든다. 한눈에 쉽게 보기 위해서는 터틀/N3를 선호하며 아파치 제나(Jena) 같은 도구는 필요한 경우 서로 다른 RDF 형식으로 자동 변환할 수 있다.

<rdf:RDF xmlns="urn:example:"

<rdf:RDF xmlns="urn:example:"
		 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  <Location rdf:nodeID="idaho>
  	<name>Idaho</name>
    <type>state</type>
    <within>
    	<Location rdf:nodeID="usa">
        	<name>United States</name>
            <type>country</type>
            <within>
            	<Location rdf:nodeID="namerica">
                	<name>North America</name>
                    <type>continent</type>
                </Location>
            </within>
       </Location>
     </within>
   </Location>
 
  <Person rdf:nodeID="lucy">
  	<name>Lucy</name>
    <bornIn rdf:nodeId="idaho"/>
  </Person>
</rdf:RDF>

RDF는 인터넷 전체의 데이터 교환을 위해 설계했기 때문에 약간 이상한 점이 있다. 트리플의 주어 ,서술어, 목적어는 주로 URI다. 예를 들어 서술어는 단지 WITHIN이나 LIVES_IN이 아니라 <http://my-company.com/namespace#within> 또는 <http://my-company.com/namespace#lives_in> 같은 URI일 수 있다. 이런 설계의 배경은 데이터를 다른 사람의 데이터와 결합할 수 있어야 하기 때문이다. 만약 within이나 lives_in 단어에 다른 의미를 붙이면 실제로 서술어는 <http://other.org/foo#within>와 <http://other.org/foo#lives_in>이기 때문에 충돌이 생기지 않는다.

URL <http://my-compnay.com/namespace>는 반드시 실제로 접속 가능한 주소일 필요는 없다. RDF 관점에서는 단순히 네임스페이스 일 뿐이다. http:// 가 붙은 URL과의 혼동을 피하기 위해 이번 절의 예제에서는 urn:example:within과 같이 접속할 수 없는 URI를 사용한다. 다행히 이 접두어는 파일의 맨위에 한번만 지정하고 잊어버리면 된다.

스파클 질의 언어

스파클(SPARQL)은 RDF 데이터 모델을 사용한 트리플 저장소 질의 언어다.(스파클은 SPARQL Protocol and RDF Query Language의 줄임말이다). 스파클은 사이퍼보다 먼저 만들었고 사이퍼 의 패턴 매칭을 스파클에서 차용했기 때문에 이 둘은 매우 유사해 보인다.

앞에서 살펴본 질의(미국에서 유럽으로 이주한 사람을 찾기)는 사이퍼보다 스파클에서 더욱 간결해 진다.

PREFIX : <urn:example:>

SELECT ?personName WHERE {
	?person :name ?personName.
    ?person :bornIn / :within* / :name "United States".
    ?person :livesIn / :within* / :name "Europe".
}

구조는 매우 유사하며 다음 두 표현식은 동등하다(스파클에서 변수는 물음표로 시작한다).

(person) -[:BORN_IN] -> () - [:WITHIN*0..] -> (location) #사이퍼
?person :bornIn / :within* ?location. #스파클

RDF는 속성과 간선을 구별하지 않고 서술어만 사용하기 때문에 속성 매칭을 위해 동일한 구문을 사용할 수 있다. 다음 표현식에 따르면 usa 변수는 문자열 값이 "United States"인 name 속성을 가진 모든 정점이어야 한다.

(usa {name: 'United States'}) # 사이퍼
?usa :name "United States". # 스파클

스파클은 훌륭한 질의 언어다. 시맨틱 웹이 아니더라도 애플리케이션이 내부적으로 사용하는 강력한 도구가 될 수 있다.

그래프 데이터베이스와 네트워크 모델의 비교

이전 포스팅에서 "문서 데이터베이스는 역사를 반복하고 있나?"에서 코다실과 관계형 모델이 IMS에서 다대다 관계의 문제를 해결하기 위해 어떻게 경쟁했는지 설명했다. 언뜻보면 코다실의 네트워크 모델은 그래프 모델과 유사해보인다. 어쩌면 그래프 데이터베이스로 위장한 코다실이 다시 등장한게 아닐까?

아니다. 코다실과 그래프 데이터베이슨느 몇 가지 중요한 점에서 다르다.

- 코다실 데이터베이스에는 다른 레코드 타입과 중첩 가능한 레코드 타입을 지정하는 스키마가 있다. 그래프 데이터베이스에는 이런 제한이 없다. 모든 정점은 다른 정점으로 가는 간선을 가질 수 있다. 이것은 변화하는 요구사항을 쉽게 적용할 수 있는 유연성을 애플리케이션에 준다.
- 코다실에서 특정 레코드에 도달하는 유일한 방법은 레코드의 접근 경로 중 하나를 탐색하는 방식이다. 그래프 데이터베이스는 고유 ID를 가지고 있고 임의 정점을 직접 참조하거나 색인을 사용해 특정 값을 가진 정점을 빠르게 찾을 수 있다.
- 코다실에서 레코드의 하위 항목은 정렬된 집합이므로 데이터베이스는 정렬(저장소 배치에 영향을 줄 수 있음)을 유지해야 하고 애플리케이션이 새로운 데이터를 데이터베이스에 삽입할 때 정렬된 집합에서 새로운 레코드의 위치를 염두에 둬야한다. 그래프 데이터베이스에서 정점과 간선은 정렬하지 않는다. 질의를 만들 때만 결과를 정렬 할 수 있다.
- 코다실에서 모든 질의는 명령형이고 작성이 어렵다. 또한 스키마가 변경되면 질의가 쉽게 손상된다. 원한다면 그래프 데이터베이스도 명령형 코드로 순회를 작성할 수 있다. 하지만 대부분의 그래프 데이터베이스는 사이퍼나 스파클 같은 고수준 선언형 질의 언어를 제공한다.

초석: 데이터로그

데이터로그(Datalog)는 스파클이나 사이퍼보다 훨신 오래된 언어로 1980년대 학계에서 광범위하게 연구됐다. 소프트웨어 엔지니어 사이에서는 잘 알려져있지 않지만 그럼에도 중요한 이유는 이후 질의 언어의 기반이 되는 초석을 제공하기 때문이다.

실제로 일부 데이터 시스템에서 데이터로그를 사용한다. 예를 들어, 데이토믹은 데이터로그를 질의 언어로 사용한다. 그리고 캐스캘로그(Cascalog)는 데이터로그의 구현체로서 하둡의 대용량 데이터셋에 질의를 위한 용도다.

데이터로그의 데이터 모델은 트리플 저장소 모델과 유사하지만 조금 더 일반화됐다. (주어, 서술어, 목적어)로 트리플을 작성하는 대신 서술어(주어, 목적어)로 작성한다. 다음 예제는 데이터로그로 예제 데이터를 작성하는 방법을 보여준다.

name(namerica, 'North America').
type(namerica, continent).

name(usa, 'United States').
type(usa, country).
within(usa, namerica).

name(idaho, 'Idaho').
type(idaho, state).
within(idaho, usa).

name(lucy, 'Lucy).
born_in(lucy, idaho).

이제 데이터를 정의 했으니 다음 예제와 같이 이전과 동일한 질의를 작성할 수 있다. 사이퍼나 스파클 질의와는 조금 다르게 보이지만 그렇다고 흥미를 잃을 필요는 없다. 데이터로그는 프롤로그의 부분집합이다. 프롤로그는 컴퓨터 과학을 공부했다면 한 번쯤 본 적이 있을 것이다.

within_recursive(Location, Name) :- name(Location, Name). /* 규칙 1 */
within_recurisve(Location, Name) :- within(Location, Via), /* 규칙 2 */
                         	within_recursive(Via, Name). 
migrated(Name, BornIn, LivingIn) :- name(Person, Name), /* 규칙 3 */
                         	born_in(person, BornLoc),
                                 within_recursive(BornLoc, BornIn),
                                 lives_in(Person, LivingLoc),
                                 within_recurisve(LivingLoc, LivingIn).
 ?- migrated(Who, 'United States', 'Europe').
 /* Who = '루시'. */

사이퍼와 스파클은 SELECT로 바로 질의하는 반면 데이터로그는 단계를 나눠 한 번에 조금씩 질의로 나아간다. 먼저 새로운 서술어를 데이터베이스에 전달하는 규칙(rule)을 정의한다. 이 예제에서는 서술어 within_recursive와 migrated 두 개를 새로 정의한다. 서술어는 데이터베이스에 저장된 트리플이 아니다. 서술어는 데이터나 다른 규칙으로부터 파생된다. 규칙(rule)은 함수가 다른 함수를 호출하거나 재귀적으로 자신을 호출하는 것 처럼 다른 규칙을 참조할 수 있다. 이처럼 복잡한 질의를 작은 부분으로 나눠 차례차례 구성할 수 있다.

규칙에서 대문자로 시작하는 단어는 변수다. 그리고 서술어는 사이퍼와 스파클의 서술어와 대응된다. 예를 들어 name(Location, Name)은 변수 Location = namerica와 Name = 'North America'를 가진 트리플 name(namerica, 'North America')에 대응된다.

시스템이 :- 연산자의 오른편에 있는 모든 서술어의 대응을 찾으면 규칙이 적용된다. 규칙이 적용될 때 :- 의 왼편이 데이터베이스에 추가된다(변수는 대응된 값으로 대최된다).

다음은 이 규칙을 적용할 수 있는 방법 중 하나다.

1. 데이터베이스에 name(nameirca, 'North America')가 존재하면 규칙 1을 적용한다. 규칙 1은 within, recursive(namerica, 'North America')를 생성한다.
2. 데이터베이스에 within(usa, namerica)가 존재하고 이전 단계에서 within_recursive(naemrica, 'North America')를 생성했으면 규칙 2를 적용한다. 규칙 2는 within_recursive(usa, 'North America')를 생성한다.
3. 데이터베이스에 within(idaho, usa)가 존재하고 이전 단계에서 within_recursive(usa, 'North America')를 생성했으면 규칙 2를 적용한다. 규칙 2는 within_recursive(idaho, 'North America')를 생성한다.

within_recursive 서술어는 규칙 1과 2를 반복 적용해 데이터베이스에 포함된 북아메리카(또는 다른 장소 이름)의 모든 위치를 찾을 수 있다. 질의는 BornIn = 'United States'와 LivingIn = 'Europe'이고 변수 Who에 사람을 남기게 한 다음 데이터 로그 시스템에 어떤 값이 변수 Who에 나타날 수 있는지 물어본다. 최종적으로 이전 사이퍼나 스파클 질의와 같은 답을 얻는다.

데이터로그 접근 방식은 이번장에서 이전에 설명한 질의 언어와는 다른 사고가 필요하다. 하지만 다른 질의와 규칙을 결합하거나 재사용할 수 있기 때문에 매우 강력한 접근 방식이다. 간단한 일회성 질의에 사용하기는 편리하지는 않지만 데이터가 복잡하면 더 효과적으로 대처할 수 있다.

정리

데이터 모델은 광범위한 주제다. 이번 장에서는 다양한 종류의 모델을 간략하게 살펴봤다. 각 모델의 모든 세부 사항을 담진 못했지만 설명했던 개략적 내용만으로도 애플리케이션의 요구사항에 가장 적합한 모델을 찾고 싶은 욕구를 충분히 자극했을 것으로 기대한다.

역사적으로 데이터를 하나의 큰 트리(계층 모델)로 표현하려고 노력했지만 다대다 관계를 표현하기에는 트리 구조가 적절하지 않았다. 이 문제를 해결하기 위해 관계형 모델이 고안됐다. 최근 개발자들은 관계형 모델에도 적합하지 않은 애플리케이션이 있다는 사실을 발견했다. 새롭게 등장한 비관계형 데이터저장소인 'NoSQL'은 다음과 같은 두 가지 주요 갈래가 있다.

1. 문서 데이터베이스는 데이터가 문서 자체에 포함돼 있으면서 하나의 문서와 다른 문서 간 관계가 거의 없는 사용사례를 대상으로 한다.
2. 그래프 데이터베이스는 문서 데이터베이스와는 정반대로 모든 것이 잠재적으로 관련 있다는 사용 사례를 대상으로 한다.

세 가지 모델(문서, 관계형, 그래프) 모두 현재 널리 사용하고 있으며 각 모델은 각자의 영역에서 훌륭하다. 한 모델을 다른 모델로 흉내 낼 수 있지만(예를 들어 그래프 데이터는 관계형 데이터베이스로 표현할 수 있다) 그 결과는 대부분 엉망이다. 이 것이 바로 단일 만능 솔루션이 아닌 각기 목적에 맞는 다양한 시스템을 보유해야 하는 이유다.

문서 및 그래프 데이터베이스가 가진 공통점 중 하나는 일반적으로 저장할 데이터를 위한 스키마를 강제하지 않아 변화하는 요구사항에 맞춰 애플리케이션을 쉽게 변경할 수 있다는 점이다. 하지만 애플리케이션은 데이터가 특정 구조를 갖는다고 가정할 가능성이 높다. 이는 스키마가 명시적인지(쓰기에 강요됨) 암시적인지(읽기에 다뤄짐)의 문제일 뿐이다.

각 데이터 모델은 고유한 질의 언어나 프레임워크를 제공한다. 몇 가지 예로 SQL, 맵리듀스, 몽고DB의 집계 파이프라인, 사이퍼, 스파클, 데이터로그 등을 설명했다. 또한 CSS와 XSL/XPath도 다뤘는데 이는 데이터베이스 질의 언어는 아니지만 흥미로운 유사점이 있다.

많은 분야를 다뤘다 하더라도 아직 언급되지 않은 데이터 모델이 남았다. 간략하게 몇 가지 예를 들면 다음과 같다.

- 게놈(genome) 데이터로 작업하는 연구원은 종종 염기 서열 유사도 검색(sequence-similarity searches)을 수행해야 한다. 이 작업은 (DNA 분자를 나타내는) 하나의 아주 긴 문자열을 가지고, 유사하지만 동일하지 않은 문자열을 대용량 문자열 데이터베이스에서 찾는 작업을 말한다. 여기서 설명한 어떤 데이터베이스도 이 같은 사용 사례를 처리할 수 없기 때문에 연구원들이 젠뱅크(GenBank)라는 특화된 게놈 데이터베이스 소프트웨어를 작성했다.
- 입자 물리학자들은 수십년 동안 빅데이터 스타일의 대규모 데이터 분석을 해왔고 대형 강입자 충돌기(Large Hadron Collider, LHC) 같은 프로젝트의 작업은 데이터 규모가 현재 수백 페타파이트에 달한다!. 이런 규모에서 하드웨어 비용이 통제 불능 상태가 되지 않기 위해서는 사용자 정의 솔루션이 필요하다.
전문(full-text) 검색은 틀림없이 데이터베이스와 함께 자주 사용되는 일종의 데이터모델이다. 정보 검색은 이 책에서 다루지 않는 광범위한 전문 주제지만 3장과 3부에서 색인 검색을 다룬다.

아직 다루지 못한 많은 데이터모델은 일단 그대로 남겨둔다. 다음 장에서는 이번 장에서 설명한 데이터 모델을 구현할 때 발생할 수 있는 트레이드오프를 살펴본다.

반응형