이 글은 Martin Kleppmann의 데이터 중심 애플리케이션 설계를 읽고 기억하고자 적는 게시글입니다.
2. 데이터 모델과 질의 언어
옛날의 RDB의 중점으로 개발되던 환경과는 다르게 현재의 다양한 데이터 모델은 애플리케이션에 적합한 모델을 선택해야하도록 만들었습니다. 이 부분에서 최근에 각광받기 시작한 스트리밍 서비스 혹은 컴퓨터 비전과 NLP 분야의 발전은 그저 알고리즘의 발전 뿐만 아니라 데이터 모델의 발전도 기여했다는 생각이 들었습니다.
이번 장에서 저자는 데이터 모델 중 가장 대표적인 관계형 모델과 문서 모델, 그리고 그래프 기반 데이터 모델을 비교하며 어떻게 동작하는지를 보여줍니다.
1. RDB
RDB는 우리가 흔히 알고있는 MariaDB와 Oracle 같은 관계형 데이터 베이스를 말합니다. 강한 스키마 구조를 가지고 있어서 특정 데이터를 담을 때 각 테이블의 컬럼 속성을 명확하게 설정해야 합니다.
이러한 스키마의 강제성은 상당히 좋은 영향을 주기도 하는데, 일단 입력되는 데이터의 도메인(범위)를 명시 할 수 있습니다. 즉 도메인을 벗어나는 데이터는 입력되지 않습니다. 뿐만아니라 기본키, 외래키(실제로 잘 쓰나?) 를 사용해서 구조를 명확하게 표현합니다.
개인적으로 제가 제일 좋아하는 형태입니다. 스키마 설정이 완벽하면 데이터베이스는 안정적이죠. 하지만, 문제는 개발을 진행하는 중에 확장성을 고려한 스키마 설정이 그리 쉽지는 않다는 점입니다.
스키마의 변경은 큰 충격을 줍니다. 예를 들면, 앱의 사용자에 대한 정보를 입력 할 때 국내 사용자만을 타겟으로 삼다가 외국 서비스로 진출한다면 성과 이름으로 명확한 구분이 필요하고 회원가입한 나라 따른 앱의 언어 및 UI 형태를 달리하기 위해 국가또한 있어야 합니다.
인덱스의 순서가 바뀔 수 있습니다. 이러한 지속적인 스키마 관리가 필요한 점이 RDB입니다.
뿐만아니라 트랜잭션도 있습니다. 트랜잭션은 RDB의 작업 단위로써 한 작업을 정상적으로 혹은 아예 실행되지 않도록 보장해주며, 도메인에 맞는, 무결성이 지켜지는 RDB로 만들어줍니다.
하지만 트랜잭션은 성능에 영향을 줍니다. 정확히는 트랜잭션의 격리수준과 락 수준에 따라 달라집니다. 트랜잭션이 동작할 떄 어느 수준까지 데이터를 보여 줄 것인지, 어느 수준까지 락을 걸 것인지에 따라 쓰기 처리량은 매우 큰 영향을 받습니다.
추가적으로 인덱스라는 길잡이가 있습니다. 일반적으로는 row 기반 데이터베이스의 특성 상 어느 데이터를 찾기 위해서 모든 row를 뒤져야 할 수 있습니다. 그럴 때 자주 찾는 컬럼을 인덱스로 설정하면 해당 컬럼을 찾을때는 그 값에 빠르게 접근 할 수 있습니다.
하지만, 인덱싱도 쓰기에 영향을 줍니다. INSERT 혹은 UPDATE를 수행 할 때마다 계속해서 인덱스가 생성되기 때문에 디스크 용량도 많이 차지하고, 쓰기 효율도 나빠집니다.
2. NoSQL
NoSQL이 등장 한 이유는 기존의 RDB가 가지고 있던 문제점 때문입니다. RDB의 문제점은 다음과 같습니다.
- 대규모 데이터 셋을 다루기 힘들다.
- 낮은 쓰기 처리량
- 대체적으로 유료 서비스
- 스키마의 제한, 표현력이 부족한 데이터 모델
- 객체 지향 개발 방법과 어울리지 않는 데이터 모델
여기서 객체 지향 개발 방식과 어울리지 않는 데이터 모델이라는 점에 조금 더 주목 할 필요가 있습니다.
객체 지향적 개발은 일반적으로 인터페이스를 이용한 추상화, 객체 개념을 이용 한 재사용성 확보 등 많은 장점을 가지고 있지만, RDB는 그러한 개발 방식에 어울리지 않는 부분이 있습니다.
만약 제가 게임의 캐릭터에 대해서 모델링을 했다고 가정해보겠습니다.
캐릭터의 이름과 직업은 1개의 데이터를 가지고 있지만, 제가 습득한 스킬의 데이터는 N개로써 스키마를 분할해야 합니다. 간단한 예시로는 다음처럼 표현 할 수 있겠네요.
캐릭터 정보 DB ( 이름, 직업, 캐릭터 고유 ID ) 캐릭터 스킬 정보 DB ( 캐릭터 고유 ID , 스킬 ID ) 스킬 정보 DB ( 스킬 ID, 스킬에 대한 컬럼… )
캐릭터 정보 DB는 캐릭터 스킬 정보 DB와 1:N 조인을 해야하고, 캐릭터 스킬 정보 DB는 스킬 정보 DB와 1:1 조인을 해야 합니다.
스킬 정보 DB의 경우에는 고정 된 갯수의 정보를 담고 있으니 1:1 조인 시 오버헤드가 크지 않을 수 있지만, 1000만 다운로드 게임이라면 모든 캐릭터의 스킬 정보 DB에서 조인을 수행 할 경우 눈 앞이 아찔해 질 것 같습니다.
아마 그래서 모 대기업 게임 회사의 면접에서 NoSQL의 샤딩에 대한 문제가 나온 것 같습니다.
이 경우 만약 데이터가 JSON으로 저장되면 어떻게 될까요? 데이터 모델은 너무나 단순해집니다.
{
"유저 고유 ID" : {
캐릭터 이름 :
캐릭터 직업 :
캐릭터 스킬 : {
"스킬 명 1 " : 스킬 정보,
"스킬 명 2 " : 스킬 정보,
.
.
.
}
}
.
.
.
}
정말 심각 할 정도로 단순해집니다. 모델링은 끝났습니다.
이러한 데이터를 가지고 초기 유저들이 게임에 잘 적응 할 수 있도록 A 직업이 배우면 좋을 스킬트리를 추천하는 서비스를 개발하기 위한 데이터 파이프라인을 만든다면 타겟이 되는 유저의 고유 ID만 특정하면 거창한 조인이 없이 간단하게 원하는 정보를 얻을 수 있습니다. 예를들면 A-B 레벨 구간의 유저들의 스킬만 수집하면 되는 것 입니다.
하지만 다대다, 다대일의 데이터를 다룰때는 이러한 문서형 NoSQL은 큰 문제점에 봉착합니다. 왜냐하면 문서형 NoSQL은 일대다 형태의 데이터에 특화되어 있지만, 조인이 필요한 다대일의 데이터 형태에는 지원이 약하기 때문입니다.
방법은 있습니다. 바로 비정규화를 해버리면 됩니다. 일반적으로 데이터의 중복과 데이터 이상 문제를 해결하기 위해 사용하는 정규화를 없애면 조인의 필요성이 낮아집니다. 하지만, 그만큼 데이터의 중복은 늘어나고 그 처리는 애플리케이션 파트로 넘기는 일이 되버리기도 합니다.
3. 그래프형 데이터 모델
처음 들어보는 데이터 모델에 깜짝 놀랐습니다. 저자는 그래프형 데이터 모델을 언제 써야 하는지에 대해서 이렇게 말합니다. “다대다 관계의 복잡성이 높아질 때 그래프로 데이터를 표현하는 것이 쉽다”
그래프 모델은 기본적으로 정점과 간선으로 나누어져 있습니다. 간선은 방향성을 가지고 있고(일반적으로) 두 정점을 이어주는 역할을 합니다.
가장 자주 사용 할 수 있는 예시는 다음과 같습니다.
- 소셜 그래프 : 정점은 사람 간선은 관계
- 웹 그래프 : 정점은 웹 페이지 간선은 다른 페이지에 대한 링크
- 도로나 철도 네트워크 : 정점은 교차로 간선은 교차로 간 도로나 철도 선
그래프형 데이터 모델은 크게 2가지로 나눌 수 있는데 바로 속성 그래프 모델과 트리플 저장소 모델 입니다.
1 ) 속성 그래프 모델
먼저 정점과 간선에 속하는 데이터 형태를 알아보겠습니다.
정점이 포함하는 데이터는 다음과 같습니다.
- 정점을 식별 할 수 있는 식별자
- 유출 간선 집합
- 유입 간선 집합
- 속성 컬렉션 ( 키 : 값 )
간선이 포함하는 데이터는 다음과 같습니다.
- 간선을 식별 할 수 있는 식별자
- 간선이 시작하는 정점(꼬리 정점)
- 간선이 끝나는 정점(머리 정점)
- 두 정점 간 관계 유형을 설명하는 레이블
- 속성 컬렉션 ( 키 : 값 )
일단 기본적으로 그래프는 방향을 가지는 그래프라고 생각하면 될 것 같습니다. 이러한 속성 그래프 모델의 장점으로 저자는 3가지를 말했는데
- 정점은 다른 정점과 간선으로 연결되며, 특정 유형과 관련 여부를 제한하는 스키마는 없다.
- 정점이 주어지면 정점의 유입과 유출 간선을 효율적으로 찾을 수 있고, 그래프를 순회 할 수 있다. 정점을 따라 앞 뒤로 순회한다. - 정점과 간선이 모두 유입,유출 간선을 가지고 있기 때문에
- 다른 유형의 관계에 레이블만 변경하여 저장해도 데이터 모델을 깔끔하다. - 가족관계에 애완동물 및 친구 관계등을 추가해도 레이블이 다르면 구분이 가능하다.
2 ) 트리플 저장소
트리플 저장소에 대한 설명을 읽었을 때 생각한 것은 속성 그래프 모델의 간편화? 였습니다.
정점과 간선이라는 명확한 정의로 되어 마치 자료구조의 그래프를 구현하는 느낌이였던 속성 그래프 모델과는 다르게 트리플 저장소는 두 정점과 하나의 간선으로 정점과 간선의 관계를 3 단어로 표현하게 적는 방법입니다.
예를 들면 Lion2me - 읽는다 - 책 라고 말하면, Lion2me라는 정점의 속성은 {읽는다 : 책}입니다. 이 경우 꼬리 정점은 책이 됩니다.
일반적으로 데이터를 표현 할 때 일반적인 값은 문자열로 적고 간선의 경우 별도의 표시를 해서 구분하는게 좋다고 합니다.
결국 우리는 최선의 데이터 모델을 찾아야 한다.
RDB도 완벽하지 않고 문서형 NoSQL도 완벽하지 않습니다. 결국 어떠한 서비스를 개발 할 때 어느 부분은 NoSQL을 사용 할 수도, 어떤 부분은 RDB를 사용 할 수도 있습니다.
그러면 추구하는 데이터 모델은 무엇일까?
문서형 데이터 베이스의 스키마리스
저자는 정확히 말하면 스키마리스가 아닌 쓰기 스키마와 읽기 스키마 중 읽기 스키마를 가지고 있다고 말합니다. 여기서 쓰기 스키마는 RDB가 가지고 있는 모든 데이터는 명확하게 스키마의 구조를 따르고 있음이 확실한 구조라고 말하고, 읽기 스키마는 암묵적으로 데이터를 읽을 때만 해석되는 스키마라고 말합니다.
책의 예시를 보면, 사용자의 이름을 성과 이름으로 구분 할 때 문서 데이터베이스는 현재의 이름을 공백 기준으로 나누어서 첫 단어를 사용하면 됩니다. 그 값을 문서에 first_name으로 저장하면 성이 됩니다. 하지만, RDB의 경우에는 전체 데이터베이스에 대해서 마이그레이션이 수행됩니다. 결국 Alter table add column과 Update문이 사용됩니다. (MySQL은 Alter table을 하면 테이블을 복사해서 엄청 오래걸린다네요)
일단 UPDATE문이 수행되면서 많은 데이터가 동시에 쓰여지기 시작하면, 그 크기에 따라 서비스에 영향을 줄 가능성이 높습니다. 하지만 문서형 데이터베이스는 읽을 때 해석되는 스키마로 더 효율적입니다.
질의를 위한 데이터 지역성
처음 들어보는 부분이지만, 겨우 이해한 내용으로는 파티셔닝의 느낌이 들었다.
저장소 지역성이라는 말로 설명했는데, 문서 데이터베이스의 큰 단점 중 하나는 원하는 데이터의 크기가 어느정도이든 전체 문서 내용을 읽어야 한다.는 점입니다. 결국 유저의 정보를 담은 파일이 1개이고, 유저의 수가 100만이라면 A라는 유저의 정보를 알기 위해서 100만명의 유저 정보를 읽어야 합니다.
만약 게시글에 첨부 된 파일을 찾는 로직을 수행한다면 모든 게시글을 읽고 모든 파일의 경로가 담긴 메타데이터를 읽은 뒤에야 찾을 수 있습니다. 하지만 만약 게시글의 고유 번호의 해시값에 따라 10개의 폴더로 구분되어 있다면 어떨까요? 그 속도는 10분의 1로 줄어들 수 있습니다.
왜냐하면 저장소의 위치 상 읽어야하는 데이터 수가 10분의 1로 감소했으니까요.
이런 부분이 바로 데이터 지역성인 것 같습니다.
요즘에는 RDB에서도 JSON을 지원하고, NoSQL도 조인을 지원하기 위해 노력한다.
데이터를 위한 질의 언어
일단 명령형 질의 언어는 다루지 않습니다. 실제로 코다실을 사용 할 일은 없을 것 같기에 간단히 알고만 넘어갑니다.
선언형 질의
선언형 질의는 RDB의 일반적인 질의와 같습니다. 이 책에서는 명령형 질의에 비해 선언형 질의의 장점에 대해서 말하고 있지만, 저는 선언형 질의의 특징에 대해서 말합니다.
선언형 질의는 알고자 하는 데이터의 패턴, 즉 충족해야하는 조건과 변환에 대해서만 지정하면 됩니다. 이러한 이유로 병렬 처리에 이점을 가지고 있습니다. 정확히 이점이 있다기보다 명령형 질의보다 낫다고 하는게 좋을 것 같습니다.
또한 사용이 간단합니다. 질의 순서를 보장하지 않기 때문에 바뀌어도 상관이 없고, 잘 만들어진 쿼리 최적화기(optimizer)는 우리에게 한 줄기의 빛과 같습니다.
맵리듀스 질의
맵리듀스 질의는 많은 컴퓨터를 통해 대용량의 데이터를 다루기 위한 모델로 클러스터 환경에서 동작하기 위한 방식입니다. 저는 하둡의 맵리듀스만을 생각하고 있었는데, 지금은 다양한 NoSQL 과 심지어는 SQL에서도 지원하고 있다고 해서 놀랐습니다.
중요한 점은 맵리듀스의 질의가 되는 함수인 map, reduce는 순수 함수여야 한다는 점입니다. 동작을 수행하는데 있어서 입력 된 데이터만을 사용하고, 원본 데이터에 영향을 주지 않는 Side Effect가 없어야 하기 때문입니다.
그래프 관련 질의 - 싸이퍼
생전 처음보는 기묘한 질의라서 설명 된 예시를 적어보겠습니다.
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)
위 질의문을 쪼개어 각 문장을 분석해보면
(NAmerica(고유 식별자):Location(레이블) {name:'North America', type='continent'}(속성 컬렉션))
(Idaho)(식별자) -[:WITHIN](정점 간 관계)-> (USA)(식별자) -[:WITHIN](정점 간 관계)-> (NAmerica)(식별자)
정도로 말 그래도 그래프를 그리는 듯 합니다. 이 질의 형태는 SELECT 또한 독특합니다.
MATCH
(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name:'United States'}),
(person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'})
RETURN person.name
위 쿼리는 미국에 태어나서 유럽에서 살고있는 사람의 이름을 출력하는 코드입니다. 확실히 일반적인 RDB에서 국가나 도시등이 사람 정보와 복잡한 다대일 혹은 다대다 연결로 이어진 데이터베이스라면 이러한 그래프 쿼리는 효과적일 것 같습니다.
SQL에서도 자기참조 ( RECURSIVE 쿼리 )를 사용하면 가능하다고 합니다. 아직 경험해보지는 못했지만, 알아 볼 필요는 있을 것 같습니다.
그래프 관련 질의 : 스파클
스파클은 싸이퍼와 거의 흡사한 형태를 가지고 있다고 말하며, 간단한 예시로는 다음의 질의를 보면 됩니다.
(person) -[:BORN_ID]-> () -[:WITHIN*0..]-> (location) [ 싸이퍼 ]
?person :bornIn / :within* / :name "Europe". [ 스파클 ]
결론
애플리케이션에 맞는 데이터 모델을 찾자!