Search
Duplicate

02. 데이터 모델과 질의 언어

목차

개요

내 언어의 한계는 내 세계의 한계를 의미한다. - 루트비히 비트겐슈타인, 논리-철학 논고(1922)
데이터 모델은 소프트웨어를 개발하는데 있어 가장 중요한 요소중 하나다.
단순히 소프트웨어가 어떻게 작성되어있는지 뿐 아니라 해결하려는 문제를 어떻게 생각할지에 대해서도 영향을 미치기 때문이다.
현실의 데이터 구조를 최종적으로 데이터 메모리, 디스크, 네트워크 상의 바이트 단위로 저장하기까지는 각 계층별로 데이터 모델을 두고 이 모델들이 각각의 계층을 이루면서 표현된다.
예를 들어, 사람이라는 현실속의 개념을 최종적으로 저장하기까지의 데이터 계층은 다음과 같이 표현될 수 있다.
각 계층은 명확한 데이터 모델을 제공해 하위 계층의 복잡성을 숨기고 이러한 추상화는 다른 구성원과 협업을 가능하게 한다.
우리가 데이터 모델이라고 하면 흔하게 생각하는 관계형 데이터 모델(relational model)만 해도 학습 서적이 매우 많다. 그렇기에 하나의 데이터 모델을 익히는데도 상당한 노력이 필요하다.
하지만, 데이터 모델은 매우 다양한 유형이 있고, 유형별로 사용법에 대한 러닝커브도 다르고, 동작에 대한 지원 여부도 제각각이다. 심지어 연산 속도도 기능별로 다를 수 있다.
우리가 데이터 모델을 하나만 사용하고 내부 동작을 신경쓰지 않아도 된다고 하더라도 소프트웨어 작성만으로도 쉬운 일은 아니다. 하지만, 데이터 모델은 그 위에서 소프트웨어가 할 수 있는 일과 할 수 없는 일에 대한 많은 영향을 주기에 애플리케이션에 적합한 데이터 모델을 선택하는 작업은 중요하다.
이번 장에서는 데이터 저장과 질의를 위한 다양한 범용 데이터 모델을 살펴보도록 한다.

관계형 모델과 문서 모델

오늘날 사용하는 대부분의 데이터 모델은 관계형 모델을 기반으로 하는 SQL이다.
데이터(SQL의 테이블)는 관계(relation)로 구성되고 각 관계는 순서 없는 튜플(tuple)(SQL의 로우)모음이다.
이 관계형 모델이 나왔을 당시에는 이론적인 제안이기도 하고 효율적으로 구현이 가능할지에 대한 의문이 있었으나 현재에 이르러서는 관계형 데이터 베이스 관리 시스템(relational database management system, RDBMS)과 SQL이 정규화된 구조로 데이터를 저장및 질의가 필요한 사람들이 대부분 사용하는 도구가 되었다.
관계형 데이터베이스는 다음과 같은 비즈니스 데이터 처리에서 사용되는데 오늘날 일반적으로 수행되는 작업으로 이런 작업들이 관계형 데이터베이스의 근원이라고 할 수 있다.
트랜잭션 처리
: 영업이나 은행 거래, 항공 예약, 창고에 재고 보관
일괄 처리
: 고객 송장 작성, 급여 지불, 보고
이후 네트워크 모델이나 계층 모델, XML 데이터베이스 등 다양한 데이터 모델들이 나왔지만, 결국 관계형 모델이 살아남았고, 오늘날에 와서는 비즈니스 데이터 처리라는 영역을 넘어 웹에서 볼 수 있는 대부분의 서비스(ex: 오라인 커뮤니티, 소셜 네트워크, 게임, SaaS, …)에서 여전히 관계형 데이터베이스를 사용하고 있다.

NoSQL의 탄생

최근 나온 데이터 모델 중에서 관계형 모델의 아성을 뛰어넘으려 하는 가장 성공적인 시도로 NoSQL이라는 이름은 현재 Not Only SQL이라는 의미로 재해석되어 사용되고 있다.
그럼 어째서 NoSQL이 사용되는 걸까? 다음과 같은 다양한 이유가 있다.
대규모 데이터셋이나 매우 높은 쓰기 처리량 달성을 관계형 데이터베이스보다 쉽게 할 수 있는 확장성의 필요
상용 데이터베이스 제품보다 무료 오픈소스 소프트웨어에 대한 선호도 확산
관계형 모델에서 지원하지 않는 특수 질의 동작
관계형 스키마의 제한에 대한 불만과 더욱 동적이고 표현력이 풍부한 데이터 모델에 대한 바람
결국 애플리케이션의 다양한 요구사항중에서는 관계형 모델로 만족시킬 수 없는 요구사항들이 있고, 이러한 요구사항을 NoSQL이 충족시켜줄 수 있기 때문에, 사용되는 것으로 보인다. 실제로 지금은 애플리케이션별로 NoSQL과 관계형 데이터베이스가 같이 사용되고 있으며 이런 개념을 다중 저장소 지속성(polyglot persistence)라 한다.

객체 관계형 불일치

JPA와 같은 ORM을 공부하다보면 왜 ORM을 사용해야 하는가에 대한 설명으로 나오는게 객체 관계형 불일치이다. 오늘날 대부분의 애플리케이션이 객체지향 프로그래밍 언어로 개발되는 반면, SQL 데이터 모델은 그렇지 못하기에 데이터를 관계형 테이블에 저장하기 위해서는 애플리케이션 코드와 데이터 베이스 모델 객체(테이블, 로우, 컬럼) 사이에 변환 작업이 필요하고 이를 위한 전환 계층이 필요하다.
이런 모델간의 분리를 임피던스 불일치(impedance mismatch)라 부른다.
좀 더 자세한 내용은 다음 포스팅을 참고하도록 하자.

문서 데이터베이스는 역사를 반복하고 있나

다대다 관계(N:M) 를 표현하는 가장 좋은 방법은 무엇일까? 이에 대해 NoSQL과 문서 데이터베이스는 다시 논쟁을 열었는데, 이 논쟁은 사실 더 옛날 초기의 전산화 데이터베이스 시스템에서 반복되었던 논쟁이다.
과거 비즈니스 데이터 처리를 위해 사용하던 대표적인 데이터베이스는 IBM의 정보 관리 시스템(Information Management System, IMS)이 있는데, 이 IMS는 계층 모델이라 부르는 아주 간단한 데이터 모델을 사용하고 이는 현재 문서 데이터베이스에서 사용하는 JSON 모델과 비슷하다.
JSON과 동일하게 모든 데이터를 레코드 내에 중첩된 레코드 트리로 표현한다.
그렇기에 문서 데이터베이스처럼 IMS도 일대다(1:N)관계에서는 잘 동작하지만, 다대다(N:M)관계 표현은 어렵고 조인은 지원하지 않는다.
그렇기에 개발자는 비정규화된 데이터를 중복 할 지 한 레코드와 다른 레코드의 참조를 수동으로 해결할 지 결정해야 했다. 이는 오늘날 문서 데이터베이스를 사용해 작업하는 개발자가 해결해야 하는 문제와 유사하다.
이러한 계층 모델의 한계를 극복하기 위해 대표적으로 두 가지의 해결책이 다음과 같다.
관계형 모델(오늘날 사실상 지배자)
네트워크 모델(지금은 거의 잊혀졌다.)

문서 데이터베이스의 다대다(N:M)관계

다음과 같이 엔티티 유형별로 컬렉션 두 개를 사용해 모델링하는데, 각 컬렉션은 관련된 엔티티를 참조하는 식별자 리스트를 관리한다.
과목: { { courseID: 'C1667', title: 'Introduction to Anthropology', instructor: 'Dr. Margret Austin', credits: 3, enrolledStudents: ['S1837', 'S3737', 'S9825' … 'S1847'] }, { courseID: 'C2873', title: 'Algorithms and Data Structures', instructor: 'Dr. Susan Johnson', credits: 3, enrolledStudents: ['S1837','S3737', 'S4321', 'S9825' … 'S1847'] }, { courseID: C3876, title: 'Macroeconomics', instructor: 'Dr. James Schulen', credits: 3, enrolledStudents: ['S1837', 'S4321', 'S1470', 'S9825' … 'S1847'] }, ... 학생: { { studentID:'S1837', name: 'Brian Nelson', gradYear: 2018, courses: ['C1667', C2873,'C3876']}, { studentID: 'S3737', name: 'Yolanda Deltor', gradYear: 2017, courses: [ 'C1667','C2873']},}
JSON
복사

네트워크 모델

코다실(Conference on Data Systems Languages, CODASYL) 이라 불리는 위원회에서 표준화한 네트워크 모델은 코다실 모델이라 불리기도 한다.
이 네트워크 모델은 계층 모델을 일반화한다.
계층 모델의 트리 구조에서 모든 레코드가 정확히 하나의 부모를 가지는데, 네트워크 모델에서는 레코드가 여러 부모를 가질 수 있다. 즉, 다대일과 다대다 관계를 모델링 할 수 있다.
여기서 레코드 간 연결은 프로그래밍 언어의 포인터 와 비슷한데 최상위 레코드(root record)에서 연속된 연결 경로를 따라가는게 원하는 레코드에 접근하는 유일한 방법으로, 이를 접근 경로라 한다.
다대다 관계에서 이러한 접근 경로는 다양한 경로가 동일한 레코드로 이어질 수 있고, 개발자는 경로의 맨 앞에서 이런 다양한 접근 경로를 계속 추적해야 한다.
만약 레코드가 다중 부모(즉, 다른 레코드에서 다중으로 유입된 포인터)를 가진다면, 애플리케이션 코드는 모든 관계를 모두 추적해야 한다.
또한, 접근 경로를 변경할 수는 있지만, 변경을 위해 필요한 비용(수 많은 수작업 데이터 베이스 질의 코드를 모두 살펴보고, 새로운 접근 경로를 재작성 해야 한다.)이 매우 높기 때문에 애플리케이션의 데이터 모델을 바꾸는 작업이 매우 어려운 일이었다.

관계형 모델

관계형 모델은 알려진 모델을 배치하는 일이 하는 일의 전부다.
관계는 튜플(로우)의 컬렉션이 전부로, 복잡한 중첩 구조나, 접근 경로도 없다.
관계형 데이터베이스에서 질의 최적화기(query optimizer)는 질의의 어느 부분을 어떤 순서로 실행할지를 결정하고 사용할 색인을 자동으로 결정한다.
이러한 선택이 접근 경로라 할 수 있는데, 네트워크 모델과의 큰 차이점은 접근 경로를 애플리케이션 개발자가 아닌 질의 최적화기가 자동으로 만든다는 것이고, 이는 접근 경로에 대해 신경쓰지 않아도 결론이 나고 그렇기에 관계형 모델은 새로운 기능을 추가하는 작업이 훨씬 쉽다.

문서 데이터베이스와의 비교

문서 데이터베이스는 별도 테이블이 아닌 상위 레코드 내의 중첩된 레코드를 저장하지만, 다대일과 다대다 관계를 표현할 때 관계형 데이터베이스와 마찮가지로 고유한 식별자를 참조한다는 공통점을 가진다. 이를 관계형 모델에서는 외래 키라 부르고 문서 모델에서는 문서 참조(document reference)라 부른다. 이러한 식별자는 조인이나 후속 질의를 사용해 읽기 시점에 확인한다.
그렇다면, 문서 데이터 모델과 관계형 모델은 각기 어떤 상황에서 선호될까?
문서 데이터 모델이 선호되는 경우
: 스키마 유연성, 지역성에 기인한 고성능성과 일부 애플리케이션의 데이터 구조와 더 가깝다.
관계형 데이터 모델이 선호되는 경우
: 조인, 다대일, 다대다 관계를 더 잘 지원한다.

애플리케이션 코드를 간단하게 하는 데이터 모델

상호 연결이 많은 데이터는 관계형 데이터 모델은 무난하고 그래프 모델은 자연스럽다.
데이터가 문서와 비슷한 구조(ex: 일대다 관계 트리)에서는 문서 모델이 좋다.
: 문서와 비슷한 구조를 여러 테이블로 나누어 찢는(shredding) 관계형 기법은 복잡도를 증가시켜 복잡한 애플리케이션 코드를 발생시킨다.
중첩 구조가 깊을 경우 문서 모델은 중첩 항목을 바로 참조할 수 없기에 관계형 데이터 모델이 좋다.(중첩도가 낮을 경우 문제가 되지 않아 문서 모델도 괜찮다.)
조인 기능이 많이 필요한 애플리케이션은 관계형 데이터 모델이 좋다.
다대다 관계를 사용하는 애플리케이션에서는 관계형 데이터 모델이 좋다.
: 비정규화로 조인의 필요성을 줄일 수는 있지만, 애플리케이션 코드가 비정규화된 데이터의 일관성을 유지하기 위해 추가 작업이 필요하다. 또한 이러한 추가 작업은 복잡하고 속도도 느리다.

문서 모델에서의 스키마 유연성

문서 데이터베이스와 관계형 데이터베이스에서 지원하는 JSON은 문서의 데이터에 어떤 스키마를 강요하지 않는다.
{ name: "catsbi", attr: 123 }
JSON
복사
{ name: "catsbi", attr: "123", attr2: 456 }
JSON
복사
{ name: "catsbi", attr: { innerAttr: 123 } }
JSON
복사
즉, 위와 같이 임의의 키와 값을 문서에 추가할 수 있다는 의미이기도 하고, 읽을 때 클라이언트는 문서에 포함된 필드의 존재 여부를 보장하지 않는다는 의미이기도 하다. 그렇기에 스키마리스(schemaless)라 불리기도 하지만, 엄밀히 말하면 암묵적인 스키마는 있고 데이터베이스는 이를 강요하지 않을 뿐이다.
쓰기 스키마(schema-on-write)에선 명시적이고 데이터 베이스는 쓰여지는 데이터가 스키마를 따르고 있음을 보장한다. 정적(컴파일 타임) 타입 확인과 비슷하다.
읽기 스키마(schema-on-read)에선 데이터 구조는 암묵적이고 데이터를 읽을 때만 해석된다. 프로그래밍 언어의 동적(런타임) 타입 확인과 유사하다.
이러한 접근 방식의 차이는 데이터 타입 변경을 하고자 할 때 두드러지는데, 예를 들어 기존에는 name이라는 필드에 풀네임(ex: 김 철수) 을 저장하고 있었지만, 성과 이름을 분리해 따로 저장하려고 한다고 할 때, 문서 데이터베이스와 관계형 데이터베이스는 각기 다른 방식으로 처리한다.
문서 데이터베이스
if(user && user.name && !user.firstName) { user.firstName = user.name.split(" ")[0]; }
Java
복사
관계형 데이터베이스
ALTER TABLE users ADD COLUMN first_name text; #postgreSQL UPDATE users SET first_name = split_part(name, ' ', 1); #mySQL UPDATE users SET first_name = substring_index(name, ' ', 1);
SQL
복사
두 작업만 보면 단순한 분기처리보다 스키마 변경을 시도하는 관계형 데이터베이스가 문제일 것 같다. 하지만 대부분의 관계형 데이터베이스 시스템은 ALTER TABLE 문을 수 밀리초 내에 수행하기에 문제가 되지 않는다. (MySQL은 전체 테이블을 복사하기 때문에 중단 시간이 길어질 수 있다.)
큰 테이블에 UPDATE 문을 수행하는 경우에도 모든 로우가 재작성 될 수 있기 때문에 수행 시간이 길어질 수 있다. 그럴 경우엔 애플리케이션은 추가된 필드(first_name)이 기본값인 널(null)로 설정되도록 하고 읽는 시점에 채울 수 있다.
그럼 읽기 스키마 접근 방식은 언제 유리할까?
컬렉션의 채워질 항목이 모두 동일한 구조가 아닐 경우
다음과 같이 오브젝트의 유형이 각기 다른데 각 유형별로 별도의 테이블을 만들어 저장하는건 효과적이지 않다. 이런 경우 스키마리스 문서는 스키마보다 자연스러운 데이터 모델이라 할 수 있다.
하지만, 이렇게 항목이 유연하지 않고 모든 레코드가 동일한 구조로 예상이 가능하다면 스키마가 문서화와 구조를 강제하기 유용하다.

질의를 위한 데이터 지역성

애플리케이션이 전체 문서(ex: 웹 페이지 상에 문서 노출 동작)에 자주 접근해야 할 때 저장소 지역성(storage locality)을 활용하면 성능 이점이 있다.
지역성의 이점은 한 번에 해당 문서의 많은 부분을 필요로 할 때 적용되는데, 필요한 정보들이 여러 테이블에 분산되어 있다면, 다중 색인 검색이 필요하고 더 많은 디스크탐색과 시간이 필요해진다.
데이터베이스는 문서의 작은 부분에만 접근해도 전체 문서를 적재해야 하기 때문에 큰 문서에서는 낭비일 수 있다. 문서를 갱신할 때도 전체 문서를 재작성해야 한다.
NutritionalInformation { calorie: 334, //칼로리 - 필수 sodium: 115, //나트륨 carbohydrate: 15, // 탄수화물 - 필수 sugars: 0.4, //당류 fat: 11, //지방 - 필수 transFat: 0, // 트랜스지방 saturatedFat: 4.1 //포화지방 cholesterol: 0; //콜레스테롤 protein: 2; // 단백질 - 필수 }
JSON
복사
위와 같은 영양 정보에서 단백질 정보 하나를 가져오기 위해서 나머지 전체 문서 정보를 가져와야 한다.
이런 문서의 크기를 바꾸지 않는 수정은 쉽게 수정할 수 있다.
그렇기 때문에 문서를 최대한 작게 유지하며 크기가 증가하는 쓰기를 피하라고 권장한다.
이러한 제한과 권장으로 관계형 데이터베이스가 문서 데이터베이스와의 차이를 줄여서 문서 데이터베이스가 유용한 상황을 줄일 수 있다.

문서 데이터베이스와 관계형 데이터베이스 통합

PostgreSQL은 9.3버전, MySQL은 5.7, IBM DB2는 10.5버전부터 JSON 문서에 대해 비슷한 수준의 지원 기능을 제공한다. (참고. postgreSQL의 JSON type 사용하기)
문서 데이터베이스쪽에서도 리싱크 DB는 질의 언어에 대해 관계형 조인을 제공하고, 몽고DB드라이버도 자동으로 데이터베이스 참조를 확인한다.
(실제로는 클라이언트 측 조인을 수행하여 네트워크 비용이 추가되고 최적화가 부족하기에 속도는 느릴 수 있다.)
이처럼 각 데이터베이스가 시간이 흐르며 서로의 장점들을 반영하여 보완하며 비슷해지고 있다.

데이터를 위한 질의 언어

관계형 모델이 등장하고 데이터를 질의하는 방법은 대표적으로 다음과 같이 두 가지로 구분된다.
선언형 질의
명령형 질의
이 두 가지 질의는 어떤 차이가 있을까?

명령형 질의

IMS와 코다실에서는 명령형 코드를 사용하여 데이터베이스에 질의한다.
그리고 우리가 많이 사용하는 프로그래밍 언어 역시 명령형 언어다. 즉 다음과 같이 특정 순서로 특정 연산을 수행하게끔 컴퓨터에 지시(명령)하는 것이다.
public List<Animal> getSharks() { animals.stream().filter(animal-> animal.equalFamily(SHARKS)).collect(toList()); }
Java
복사
위 코드는 동물 목록에서 family가 상어인 동물만 반환하는 코드다.
컴퓨터에게 동물 목록을 순회하며 familiy속성이 shark 인 동물을 필터링하고, 그룹핑해서 반환하라고 명령하고 있다. 이러한 일련의 순서는 무시할 수 없고, 순서에 따라 값이 달라지거나 문제가 생길수도 있다. 그렇기에 애플리케이션 개발자는 명령형 언어의 지시 순서에 신경을 써야 한다.
그렇기에 병렬 처리도 까다로운 편이다.

선언형 질의

SQL이 대표적인 선언형 질의인데, 위에서 작성한 동물목록에서 family속성이 상어인 동물을 찾는 질의는 다음과 같다.
SELECT * FROM animals WHERE family = 'Sharks';
Java
복사
이러한 선언형 질의 언어에서는 목표를 달성하기 위한 방법이 아닌 다음과 같은 요소를 지정하기만 하면 된다.
알고자 하는 데이터의 패턴
결과가 충족해야 하는 조건
데이터 변환(정렬(order by) , 그룹화(group by), 집계(count, avg, sum, …))
이런 패턴만 지정해주면 어떤 색인이나 조인 함수를 지정할지나 순서에 대해서는 데이터베이스 시스템의 질의 최적화기가 알아서 한다.
그렇기에 명령형 질의와 비교해서 더 간결하고 쉽게 작업할 수 있으며 상세 구현은 숨겨져 있어서 질의를 변경하지 않고 데이터베이스 시스템 성능을 향상시킬수도 있다.
또한, 결과의 패턴만을 지정하는 선언형 질의는 결과를 결정하기 위한 알고리즘을 직접 신경쓸 필요가 없기 때문에 병렬 구현을 사용하기가 쉽다.

맵리듀스 질의

맵리듀스(MapReduce)는 많은 컴퓨터에서 대량의 데이터를 처리하기 위한 프로그래밍 모델이다.
몽고DB, 카우치DB를 포함한 일부 NoSQL에서는 제한된 형태의 맵리듀스를 지원하며 많은 문서를 대상으로 읽기 전용(read-only)질의를 수행할 때 사용한다.
선언형 질의야 명령형 질의야?
둘 다 아닌 중간 정도의 질의라고 할 수 있다. 질의 로직은 처리 프레임워크가 반복적으로 호출하는 조각 코드로 표현하며 함수형 프로그래밍 언어에 있는 map(≓collect), reduce(≓ fold, inject)함수를 기반으로 한다. 이를 일반적인 선언형 질의와 비교하면서 살펴보자.
집계 조건: 한달에 얼마나 자주 상어를 발견하는지 집계
선언형 질의(feat. PostgreSQL)
SELECT date_trunc('month', observation_timestamp) AS observation_month , sum(num_animals) AS total_animals FROM observations WHERE family = 'Shark' GROUP BY observation_month;
SQL
복사
이 질의는 상어과에 속하는 종만 보이도록 관측치를 필터링 한 뒤 관측치가 발생한 달력의 월로 그룹화하고 해당 달의 모든 관측치에 보여진 동물 수를 합친다.
맵리듀스 질의(feat.몽고DB)
db.observations.mapReduce( function map() { # 1 var year = this.observationTimestamp.getFullYear(); var month = this.observationTimestamp.getMonth() + 1; emit(year + "-" + month, this.numAnimals); # 2 }, function reduce(key, values) { # 3 return Arrays.sum(values); # 4 }, { query: {family: "Shark"}, # 5 out: "monthlySharkReport" # 6 } };
SQL
복사
1.
자바스크립트 함수로 질의와 일치하는 모든 문서에 한 번씩 호출되며 this는 문서 객체로 설정된다.
2.
map 함수는 키(2013-122014-1과 같이 연도와 월로 구성된 문자열)와 값(관측치에 있는 동물 수)을 방출(emit)한다.
3.
map에서 방출된 키-값 쌍은 키로 그룹화된다. 같은 키를 갖는 모든 키-값 쌍은 reduce 함수를 한 번 호출한다.
4.
reduce함수는 특정 월의 모든 관측치에서 동물 수를 합친다.
5.
상어 종만 거르기 위한 필터를 선언적으로 지정한다.
6.
최종 출력은 monthlySharkReport 컬렉션에 기록한다.
여기서 map과 reduce 함수는 다른 함수형 프로그래밍과 동일한 제약을 가진다.
순수(pure)해야 하고, 부수 효과(side effect)가 없어야 한다.
이러한 맵리듀스 질의는 자바스크립트코드를 이용한 고급 질의가 가능하기에 훌륭하지만, 연계된 함수 두개를 작성하는게 때로 질의하나를 작성하는 것보다 어렵다는 문제가 있는데, 선언형 질의 언어는 질의 최적화기가 질의 성능을 높일 수 있다는 점도 고려되어, 몽고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기반 구문을 사용한다.

그래프형 데이터 모델

애플리케이션이 일대다(1:N)관계이거나 레코드 간 관계가 없을 경우는 문서 모델, 단순한 다대다 관계는 관계형 모델로 충분하다. 그런데 여기서 다대다(N:M)관계가 더 복잡해지는 경우에는 그래프형 데이터 모델이 적합하다.
그래프는 두 유형의 객체로 이뤄진다.
정점(vertex): 노드나 엔티티라고도 불린다.
간선( edge): 관계나 호(arc)라고도 한다.
다음과 같은 인터넷 통신에서 클라이언트 to 클라이언트 통신을 할 때 역시 위성, 해저케이블, 기타 통신서버같은 노드들을 거치는데, 이 역시 일종의 그래프 모델이라 할 수 있다.
인터넷 통신 구조
이런 그래프는 다음과 같이 동종 데이터(ex: 사람, 웹 페이지, 통신서버등) 가 아닌 다른 유형의 정점과 간선을 연결함으로써 완전히 다른 유형의 객체를 일관성있게 저장할 수 있도록 해준다.
그래프 구조 데이터 예제
이런 그래프에서 데이터를 구조화하고 질의하는 몇 가지 다른 방법이 있다.
데이터 구조화 모델
속성 그래프 모델
네오포제이(Neo4j), 타이탄(Titan), 인피니티그래프(InfiniteGraph)
트리플 저장소 모델
데이토믹(Datomic), 알레그로그래프(Allegrograph)
그래프용 선언형 질의 언어
사이퍼(Cyper)
스파클(SPARQL)
데이터로그(Datalog)
명령형 그래프 질의 언어
그렘린(Gremlin)
그래프 처리 프레임워크
프리글(Pregel)

속성 그래프 모델

속성 그래프 모델의 정점과 간선은 다음과 같은 속성을 가진다.

정점

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

간선

고유한 식별자
간선이 시작하는 정점(꼬리 정점)
간선이 끝나는 정점(머리 정점)
두 정점 간 관계 유형을 설명하는 레이블
속성 컬렉션(키-값 쌍)
이러한 속성을 다음과 같이 두 개의 관계형 테이블로 구성해볼 수 있다.
CREATE TABLE vertices ( vertex_id integer PRIMARY KEY, properties json ); CREATE TABLE edges ( edge_id integer PRIMARY KEY, tail_vertex integer REFERENCES verticees (vertex_id), head_vertex integer REFERENCES verticees (vertex_id), label text, properties json ) CREATE INDEX edges_tails ON edges (tail_vertex); CREATE INDEX edges_heads ON edges (head_vertex);
SQL
복사
정점과 간선 테이블은 각각의 속성 정보를 json 타입을 이용해 저장하고 간선은 머리와 꼬리 정점 식별자를 저장한다. 그렇기에 각 정점을 위한 유입,유출 간선을 알고 싶다면 edges 테이블에서 head_vertex, tail_vertex로 질의하면 된다.
이러한 모델은 다음과 같이 몇 가지 중요한 점이 있다.
1.
정점은 다른 정점과 간선으로 연결되며 특정 유형과 관련 여부를 제한하는 스키마는 없다.
2.
정점이 주어지면 정점의 유입/유출 간선을 효율적으로 찾을 수 있고 그래프 순회가 가능하다.
3.
다른 유형의 관계에 서로 다른 레이블을 사용하면 단일 그래프에 다른 유형의 정보를 저장하면서도 데이터 모델을 깔끔하게 유지할 수 있다.

사이퍼 질의 언어

속성 그래프를 위한 선언형 질의 언어인 사이퍼(Cyper)는 Neo4j 그래프 데이터베이스용으로 만들어졌다. 위에서 봤던 그래프 구조 데이터 모델의 좌측에 있던 영역을 사이퍼 질의를 통해 표현하면 다음과 같이 작성할 수 있다.
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)
SQL
복사
여기서 질의는 어떻게 할 수 있을까?
미국에서 유럽으로 이민 온 모든 사람들을 찾는 질의를 작성해보자.
MATCH (person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name: 'United States'}), (person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (us:Location {name: 'Europe'}) RETURN person.name
SQL
복사
위 질의는 다음과 같이 읽을 수 있다.
1.
person은 어떤 정점을 향하는 BORN_IN 유출 간선을 가진다. 이 정점에서 name 속성이 United States인 Location 유형의 정점에 도달할 때까지 일련의 WITHIN 유출 간선을 따라간다.
2.
같은 person 정점은 LIVES_IN 유출 간선도 가진다. 이 간선과 WITHIN 유출 간선을 따라가면 name 속성이 Europe인 Location 유형의 정점에 도달하게 된다.
이 질의를 수행하면 모든 사람을 순회하며 사람들의 출생지와 거주지를 확인해 조건에 맞는 사람들만 반환한다. 혹은 반대로 미국과 유럽이란 Location 정점에서 시작해 반대 방향으로 수행할수도 있다.
보통 선언형 질의 언어는 질의를 작성할 떄 이처럼 수행에 대해 자세히 지정할 필요가 없다.
선언형 질의 언어는 질의 최적화기가 자동으로 효율적인 전략을 선택하기 때문이다.
우리는 나머지 애플리케이션만 잘 작성하면 된다.

SQL의 그래프 질의

위에서 그래프 데이터를 관계형 데이터베이스로 표현할 수 있음을 확인했다.
이와 반대로 그래프 데이터를 관계형 구조로 넣어서 SQL로 질의하는 것도 약간 어렵지만 가능하다.
이는 미리 조인 수를 고정할 수 없기 때문인데, 미리 질의에 필요한 조인을 알 고 있는 관계형 데이터베이스와 달리 그래프 질의에서는 찾고자하는 정점을 찾기 전 가변적인 여러 간선을 순회 해야 하기 때문이다.
사이퍼 질의에서 간선 순회는 () -[:WITHIN*0..]→() 문에서 발생하는데 이 가변 순회 경로에 대한 질의 개념을 SQL로 수행하기 위해서는 재귀 공통 테이블 식(recursive common table expression)(with recursive 문)을 사용해 표현할 수 있다.
WITH RECURSIVE in_usa(vertex_id) AS ( SELECT vertex_id FROM vertices WHERE properties ->>'name' = 'United States' UNION SELECT edges.tail_vertex FROM edges JOIN in_usa ON edges.head_vertex = in_usa.vertex_id WHERE edges.label = 'within' ), in_europe(vertex_id) AS ( SELECT vertex_id FROM vertices WHERE properties ->>'name' = 'Europe' 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(vertex_id) AS ( 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(vertex_id) AS ( 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 JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id;
SQL
복사
위에서 봤던 질의와 동일한 질의인데 4줄이 29줄이 되는 마법을 봐버렸다.
여기서 우리는 다양한 데이터모델이 각기 다른 요구사항을 만족하기 위해 설계되었고, 애플리케이션에서 적절한 데이터 모델 선택이 얼마나 중요한지에 대해 보여준다.

트리플 저장소

트리플 저장소 모델은 속성 그래프 모델과 거의 동등하다.
동일한 개념을 다른 용어를 사용해 설명한다. 그럼에도 불구하고 애플리케이션 구축에 유용한 도구일지도 모를 트리플 저장소를 위한 다양한 도구와 언어가 있기에 논의할만한 가치가 있다.
트리플 저장소에서는 정보를 주어(subject), 서술어(predicate), 목적어(object) 간단한 세 부분 구문 형식으로 저장한다.
여기서 주어는 그래프의 정점과 동등하고, 목적어는 문자열이나 숫자 같은 원시 데이터타입의 값 혹은 그래프의 다른 정점과 동등하다.

시맨틱 웹

트리플 저장소에 대한 내용을 보면 시맨틱 웹과 연관이 많지 않을까 생각할 수 있지만,트리플 저장소 데이터 모델은 시맨틱 웹과는 독립적이다.
시맨틱 웹
의미론적인 웹이라는 의미로 인터넷과 같은 분산환경에서 리소스에 대한 정보와 자원 사이의 관계-의미 정보(Semanteme)를 기계가 처리할 수 있도록 온톨로지 형태로 표현하고, 이를 자동화된 기계가 처리하도록 하는 프레임워크이자 기술이다. - 위키백과 (link)
하지만, 시맨틱 웹은 과대평가된 부분이 많았고 현재까지 실제로 실현된 흔적이 없어서 거품이 아니였을까 하고 있다. 하지만, 그럼에도 불구하고 시맨틱 웹 프로젝트에서 유래한 작업들 중 유용한 작업들도 많이 있다.

RDF 데이터 모델

자원 기술 프레임워크(Resource Description Framework, RDF)는 서로 다른 웹 사이트가 일관된 형식으로 데이터를 게시하기 위해 제안되는 방법이다.
여기서 자원은 웹상에 존재하는 대부분의 객체를 의미하며, 웹페이지는 주로 제목,저자,수정일과 같은 정보들이 RDF로 서술하는 대상이 된다.
전달하기위한 신텍스로 XML을 쓰기도하고, 한눈에 쉽게 보기 위해 터틀/N3를 쓰기도 하고, 아파치 제나(Jena) 같은 도구는 서로 다른 RDF 형식으로 자동 변환할 수 있다.

스파클(SPARQL)

RDF 데이터 모델을 사용한 트리플 저장소 질의 언어로 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".
SQL
복사
구조는 매우 유사하다. 다음의 두 표현식은 모두 동등하다.
사이퍼 : (person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (location),
스파클 : ?person :bornIn / :within* ?location.
여기서 RDF는 속성과 간선 구분없이 서술어만 사용하기에 속성 매칭을 위해 동일한 구문을 사용할 수 있어야 한다.
사이퍼: (usa {name: 'United States'}),
스파클: ?usa : name “United States”

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

네트워크 모델(코다실 모델이라고도 부른다.)은 계층 모델의 한계를 극복하기 위해 나온 모델로, 그래프 모델과 유사해보인다. 하지만, 다음과 같은 부분에서 차이를 보이니 유념해두자.
코다실 데이터베이스에는 다른 레코드 타입과 중첩 가능한 레코드 타입을 지정하는 스키마가 있다. 그래프 데이터베이스에는 이런 제한이 없기에 모든 정점이 다른 정점으로 가는 간선을 가질 수 있다.
코다실에서 특정 레코드에 도달하는 방법은 레코드의 접근 경로중 하나를 탐색하는 것이다. 그래프 데이터베이스는 고유 ID를 가지고 임의 정점을 직접 참조하거나 색인을 사용해 특정 값을 가진 정점을 빠르게 찾을 수 있다.
코다실의 레코드의 하위 항목은 정렬된 집합으로 데이터베이스는 정렬을 유지해야하고, 새로운 데이터가 데이터베이스에 삽입할 때 정렬된 집합에서 새로운 레코드의 위치를 염두해둬야 한다. 그래프 데이터베이스는 정점과 간선 정렬을 하지 않고 질의를 만들 때만 결과를 정렬할 수 있다.
코다실에서 모든 질의는 명령형이고 작성이 어렵고, 스키마가 변경되면 질의가 쉽게 손상된다. 대부분의 그래프 데이터베이스는 고수준 선언형 질의 언어(ex: 사이퍼, 스파클)를 제공한다.

정리

2장에서는 다양한 종류의 데이터 모델을 살펴보며, 각 모델별로 내세우는 장점이나 특징에 대해 알아봤다. 다양한 데이터 모델을 알고 다 사용해야한다. 라는 주제라기보다는 지금까지 대부분이 단 하나의 데이터 모델(ex: 관계형 데이터 모델)만 알고 사용하고 있었는데, 애플리케이션의 다양한 요구사항을 최적으로 맞추기 위해서는 더 많은 데이터 모델을 알아두고 적용할 수 있어야 한다는 점을 자극받기를 원하는 것 같다.
목적에 따라 관계형만이 아닌 문서, 그래프 모델도 각자의 영역에서 더 훌륭한 퍼포먼스를 보여주고 있고, 하나의 모델로 다른 모델을 무리하게 따라가다간 더 엉망인 결과만을 보여줄 수 있다는 점을 시사한다.
다음 장에서는 이번 장에서 소개한 데이터 모델을 구현할 때 발생할 수 있는 트레이드오프에 대해 살펴본다.