명령형 사고에서 관계형(데이터베이스) 사고로 전환하는 과정을 다룬 글입니다. 루프·조건문 중심의 사고와 데이터베이스의 집합·값 중심 사고의 차이, 이를 둘러싼 도구와 설계·성능·모델링 관점, 흔한 함정과 올바른 습관을 예시와 과제로 안내합니다.
데이터베이스는 소프트웨어 엔지니어의 성장 과정에서 이상한 자리를 차지한다.
초보 엔지니어는 자신의 의도를 루프와 조건문으로 표현하고 그것을 함수로 조직하는 데 많은 노력을 들인다. 마침내 어느 정도 유창해졌다고 느낄 즈음, 데이터베이스와 마주하게 된다. 한동안은 간단한 조회와 단일 행 삽입 정도만 할 수 있을 만큼만 주변을 어슬렁거리다가, 어느 순간에는 깊이 뛰어들어야 한다.
그들은 SQL 또는 해당 데이터베이스의 질의 언어를 배운다. MongoDB 같은 것을 질의하는 일은 친숙함의 환상을 준다—그냥 JSON일 뿐이잖아!—하지만 그것은 정말로 환상일 뿐이다. 어느 순간 더 복잡한 무언가를 시도하면, 힘들게 습득한 구조적 프로그래밍의 기술들이 작동하지 않게 된다.
여기에는 깊은 이유가 있다. 루프와 조건문은 프로그램을 구성하기 위한 수학적으로 완전한 빌딩 블록이지만, 가능한 유일한 집합은 아니다. 이런 집합은 셋이 있다:
각각의 빌딩 블록은 단순하다. 반 시간 만에도 배울 수 있다. 하지만 아무도 루프와 조건문을 반 시간 만에 효과적으로 쓰게 되지는 않으며, 한 체계에서 의도를 표현할 수 있다고 해서 다른 체계로 곧장 옮겨지는 것도 아니다. 많은 초보 프로그래머들은 “처음에 어떤 언어를 배우든 상관없다. 하나만 배우면 된다. 기술은 전이된다.”라는 말을 듣는다. 그것은 거짓말이다. 유용하고 중요하기도 한 거짓말이긴 하다. 왜냐하면 초보 프로그래머가 할 수 있는 가장 중요한 일은 언어 하나를 골라 루프와 조건문을 씀으로써 배우는 것이기 때문이다. 하지만 어쨌든 거짓말이다. 그리고 대부분의 프로그래머 커리어에서 그 사실이 처음 드러나는 곳이 데이터베이스다.
그 말은, 많은 젊은 엔지니어들이 데이터베이스를 자기 도구 상자에 몇 가지를 더하는 정도로 기대하며 다가서지만, 실제로는 사고방식을 다시 배선하라고 요구받는다는 뜻이다. 예상과 달라서 악몽처럼 느껴진다. 이 지점에서 도망치는 사람도 많다. 그 덕분에 데이터베이스에 정말 유창한 능력을 갖춘다는 것은, 그것이 할 수 있게 해주는 일 때문만이 아니라 아마 언제나 일이 있을 것이라는 점에서도, 엄청나게 가치 있는 기술이 된다.
또한 소프트웨어를 설계하는 방식과 손이 닿을 수 있는 소프트웨어의 종류도 바뀐다.
과제: 프로그래밍을 배우던 때를 돌아보자. 배운 기본 빌딩 블록들을 나열하고, 그걸 실제로 사용하는 방법을 익히는 과정이 어땠는지 생각해 보라. 이를 바탕으로, 데이터베이스를 정말 잘 다룬다는 것이 무엇을 의미하는지에 대한 기대치를 세워보라.
나의 아버지는 여러 가지로 대장장이 같은 면이 있지만, 그중에서도 목수의 대가다. 부모님이 이사할 때 질량과 부피로 가장 큰 범주를 차지하는 것은 아버지의 목공소일 것이다. 나는 많은 것을 배웠고 차고에 그럭저럭 괜찮은 가정용 목공 장비를 갖추고 있지만, 효율적으로 해낼 도구가 없는 일도 많다. 폭 18인치의 판재를 손대패로 반듯하고 평평하게 만드는 데 몇 시간이 걸릴 것이다. 그보다는 판재를 차 지붕 캐리어에 묶어 아버지 집으로 가서, 플래너/조이너에 1분도 안 걸려 통과시키는 편이 훨씬 덜 걸린다.
도구는 가능한 것을 바꾼다. 무엇을 할 수 있는지와 주어진 시간 안에 무엇을 할 수 있는지 모두를.
소프트웨어 엔지니어에게 데이터베이스는 컴파일러나 기능이 풍부한 운영체제와 어깨를 나란히 하는 가장 큰 파워툴 중 하나다. 한 번 익숙해지면, 데이터를 저장하고 질의하는 거의 모든 소프트웨어를 작성하는 일이 더 적은 노력으로 가능해진다. 전 세계 모든 데이터 과학자가 SQL을 아는 데에는 이유가 있다.
종종 더 효율적이기도 하다. 디스크에서 정확히 필요한 서브셋만 읽어오거나, 네트워크 전송량을 줄이려고 요약을 미리 계산하는 코드를 직접 손봐서 최적화할 수도 있다. 하지만 그건 많은 시간이 든다. 괜찮은 컴파일러가 나온 뒤로는 가장 중요한 어셈블리 코드 일부를 제외하면 아무도 수작업으로 튜닝하지 않게 된 것처럼, 데이터베이스 덕분에 가장 까다로운 데이터 로딩과 처리 코드를 제외하면 아무도 수작업 최적화를 하지 않는다. 그리고 설령 하더라도, 보통은 자기들의 특수한 경우를 위해 관계형 데이터베이스를 재구현하는 꼴이 된다. 예컨대 비디오 게임에서의 데이터 지향 설계 같은 흐름이 그렇다. (그 링크의 책은 데이터베이스를 한 번도 언급하지 않지만, 내가 본 관계형 시스템에서 일하는 법을 다룬 책 중 최고에 속한다.)
데이터베이스가 요구하는 방식으로 사고하는 데 시간을 충분히 들여 전환해 두면, 소프트웨어를 설계할 때 거의 습관처럼 손이 가는 도구가 된다. 머릿속에서 설계의 특정 지점에 데이터베이스를 자신 있게 끼워 넣고 그 주위로 모양을 잡는다… 설령 그 자리에 실제로 MySQL이나 Cassandra 같은 데이터베이스를 쓰지 않더라도 말이다. 목수도 큰 파워툴 옆에 수많은 수공구를 가지고 있다. “아냐, 그냥 파일시스템의 파일이면 돼. 이름과 접근 방침만 정해두자.”라고 말했던 경우도 많았다. 하지만 그 선택은 ‘데이터베이스’라고 라벨 붙은 정신적 슬롯의 구현에 관한 것이었다.
과제: 의식하지 않고도 프로그래밍에서 사용하는 다른 파워툴을 돌아보라. 파일시스템. 네트워크 스택. HTTP 서버. 격리된 프로세스와 가상 메모리. 컴파일러. 그것들이 존재함을 처음 알게 되었을 때 어땠는지, 그리고 그것들을 쓰도록 적응하는 과정이 어땠는지.
이건 내가 젊은 엔지니어들에게 농담처럼 하는 말이다. 여기에 조금 더 냉소적인 따름정리가 있다: “데이터베이스를 살려둘 수 있는 자는 거기서 결코 벗어나지 못할 것이다.” 하지만 그 밑바탕 생각은 옳다:
기본적인 데이터베이스 유창함을 쌓는 일은, 주의를 어디에 기울일지 결정하는 데 불균형하게 가치가 높다.
내가 페이스북에서 일할 때, 한 친구가 메시지를 보냈다. “내가 함께 일하는 팀이 쿼리 때문에 고생을 하고 있어. 제품을 론칭했는데, 성능이 받아들일 수 없을 만큼 느려졌어.” 그래서 팀원 몇 명과 함께 회의실에서 두어 시간을 보냈다.
이건 “그 팀이 데이터베이스를 작업 큐로 쓰고 있었는데…”로 시작하는 무용담이 아니다. 그런 이야기는 많다. 그 경우 잠금을 제대로 처리하는 일이 분명하지 않기 때문이다.
무슨 일이 벌어지는지 10분쯤 파악하고 나니, 그들이 필요한 것은 한 열에 대한 인덱스라는 사실이 드러났다. 이 사람들은 숙련된 프로그래머였다. 명령형 환경에서 훨씬 가시밭길 같은 성능 문제들을 잘 풀어왔다. 루프와 조건문에서 컴퓨터가 할 법한 작업 덩어리로 매핑하는 감각은 탄탄했지만, 데이터베이스의 빌딩 블록에서 컴퓨터가 하는 일로의 매핑은 놓치고 있었다.
그래서 그 매핑에 대해 40분쯤 이야기하며 몇 가지 예시를 함께 풀었다. 그쯤 되었을 때 그들 중 한 명이 외쳤다. “아, 그러면 이 열에 인덱스만 만들면 O(1)이네요!” 다른 한 명이 중얼거렸다. “아, 그렇지.” 그 시점에서 그들이 가장 원한 것은 회의실에서 나가 그 인덱스를 추가하는 일이었다.
몇 주 뒤, 별다른 접촉 없이 내가 그중 한 명에게 메시지를 보냈다. “이봐, 그거 잘 됐어?” 돌아온 답은 기본적으로 고맙다, 삶이 아주 좋아졌다, 지난주에도 또 하나의 느린 쿼리를 해결했다는 내용이었다. 그리고 그들로부터 다시는 연락이 오지 않았다.
내일부터는 데이터베이스의 빌딩 블록을 효과적으로 쓰기 위해 필요한 가장 큰 사고방식 변화들을 파고들기 시작하겠다.
과제: 최근에 정기적으로 도움을 청하던 인프라나 기술 중, 이제는 자신 있게 결정을 내리게 된 마지막 사례는 무엇이었나? 그 변화를 이룬 지식과 노하우는 무엇이었나?
오늘의 성가신 선문답은 다음과 같다:
데이터를 놓아라
이제 덜 난해한 설명을 해보자.
명령형에서 관계형 프로그래밍으로 넘어갈 때 첫 번째 큰 전환은 데이터를 어디에 두느냐이다. Python, C, Java 등에서 함수를 작성할 때, 우리가 작업하는 데이터 조각은 바로 눈앞에 있다. 그 데이터에 이름을 붙였고, 그 이름에 주문을 걸 수 있다. 곰곰이 생각해 보면(혹은 가벼운 판타지를 쓴다면) 악마 소환과 불길하게 닮아 있다.
def nth_word(s, n):
words = s.split(' ')
return words[n]
혹은, 무언가의 컬렉션이 있다면, 우리는 그것을 다룰 때 컬렉션의 각 원소에 한 번에 하나씩 이름을 붙여 처리한다.
for x in xs:
...
어느 쪽이든, 우리는 데이터에 손잡이를 붙여 다룬다.
여기에는 또 다른 가정이 있다: 변수에 붙어 있는 데이터는 그 값과 별개의 정체성을 지닌다. 만약 숫자 5를 가리키는 변수가 두 개 있다면, 그것은 숫자 5의 서로 다른 인스턴스다. 그래서 Java 프로그래머는 문자열에 대해 =와 .equals()를 구분해야 한다. 전자는 두 변수가 같은 정체성을 가리키는지 확인하고, 후자는 동일한 문자 시퀀스를 가리키는지 확인한다. Scheme은 eq?, eqv?, equal?, = 등으로 더 깊게 들어간다.
요약하면, 우리는 다음의 세 가지 핵심 가정으로 명령형 프로그래밍을 한다:
데이터베이스로 오면, 이 가정들은 모조리 뒤집힌다.
단일 값을 집어오고 싶다면, 그것이 컬렉션의 유일한 원소가 되도록 필터링할 수 있는 어떤 측면을 부여해야 한다. 어떤 데이터 조각이 특정 속성을 갖는지 확인하고 싶다면, 그 데이터를 식별하는 측면과 만족해야 할 속성을 함께 필터링하여 0개 또는 1개의 행을 얻는다.
다음 작업들에서 데이터를 놓는 방법을 생각해 보자:
명령형: 사용자 이름, 비밀번호, 사용자 이름에서 비밀번호 해시로의 맵이 변수로 전달된다. 이 사용자 이름에 해당하는 비밀번호 해시를 새 변수에 선택하고, 제공된 비밀번호를 해시하여 둘을 비교한다.
관계형: 사용자 이름과 비밀번호 해시가 있는 테이블을 제공된 사용자 이름과 제공된 비밀번호를 해시한 값이 일치하는 행으로 필터링한다. 일치하는 행을 얻으면 로그인 유효, 아니면 무효다.
명령형: 사용자를 설명하는 구조체를 가리키는 변수가 전달된다. 그 구조체에서 사용자의 이름에 해당하는 필드를 선택한다.
관계형: 사용자(또는 다른 엔터티)에 대해 보통은 무의미한 정수 같은 식별 측면을 부여하고, 그 식별자와 이름을 나타내는 열로 사용자 테이블을 필터링하여 일치하는 행을 얻는다.
명령형: 숫자를 가리키는 두 변수를 만들고, 리스트의 각 숫자에 임시로 이름을 붙인다. 그 숫자를 두 변수의 값과 비교한다. 둘 중 하나보다 크면, 둘 중 작은 것을 버리고 이 값으로 바꾼다.
관계형: 테이블을 크기 순으로 정렬하고, 처음 두 행만 남기도록 필터링하며, 데이터베이스가 이를 합리적이고 성능 좋게 처리한다고 믿는다.
특정 데이터 조각에 대한 참조를 놓는 일은 어려울 수 있지만, 한 번 이해하면 해방감이 있다. 당신이 신경 쓰는 어떤 데이터든, 그저 거기 떠 있으며 당신이 아는 측면들을 통해 접근 가능하다.
과제: 최근에 했던 몇 가지 프로그래밍 작업을 가져와 보라. 어디에서 단일한, 이름 붙은 정체성에 의존하고 있는지 식별하라. 그것을 데이터의 측면들에 대한 테이블 필터링 관점으로 다시 생각해 보라.
데이터베이스에 명령형 기법을 우겨 넣으려 한다는 가장 큰 신호 중 하나는, 배열이나 JSON 같은 합성 형식을 열에 넣어두고 명령형 프로그래밍에서처럼 그 안에서 값을 선택하려 하는 것이다. 이게 언제나 그런 건 아니고, 뒤에서 몇 가지 주의점을 이야기하겠지만, 우선 핵심 문제부터 보자:
JSON이나 배열 같은 구조화된 형식을 열에 넣는 것은, 보통은 그것을 명령형 프로그램의 일부인 것처럼 조작할 수 있는 손잡이를 제공하려는 시도다.
예를 들어, 사용자에게 권한을 부여하는 경우를 생각해 보자. 관계형 프로그래밍에 익숙하지 않은 사람은 id, 사용자명, 비밀번호, 부여된 권한의 배열을 열로 두는 사용자 테이블을 만들고 싶은 유혹을 느낄 수 있다. 이는 명령형 프로그래밍에서 데이터 접근에 대해 우리가 확인한 세 가지 가정을 모두 건드린다:
이 구성은 어떻게 사용할까? 행을 질의하여 데이터를 가리키는 변수를 얻고, 그 데이터에서 권한을 선택한 다음, 원하는 권한을 찾기 위해 권한들을 순회할 것이다.
이를 관계형으로 하면 어떨까? 대체 가정을 다시 떠올려 보자:
우리가 원하는 컬렉션은 사용자 id와 권한이라는 두 열을 가진 테이블이다. 그 테이블에 행이 존재한다는 사실이 해당 사용자가 그 권한을 가졌음을 나타낸다. 특정 사용자 id와 권한으로 테이블을 필터링하면, 행이 있거나 없다.
PostgreSQL 같은 일부 데이터베이스는 배열이나 JSON 객체의 원소를 검사하는 편의 함수를 제공한다. 데이터베이스에 아직 충분히 익숙하지 않다면 이게 매력적으로 보이겠지만, 대부분 당신이 원하는 바가 아닐 것이다. 동작은 하지만, 데이터베이스가 그 검사를 얼마나 효율적으로 만들 수 있는지에 한계를 둔다. 배열 열을 쓰면, 항상 행을 선택하고 배열을 순회해야 한다. 별도의 테이블을 쓰면 두 열이 복합 기본 키를 이루고, 대부분의 데이터베이스에서 가장 최적화된 연산 중 하나인 직접 인덱스 조회로 행의 존재 유무를 확인할 수 있다.
주의점이 있고, JSON 덩어리를 데이터베이스에 그냥 집어넣고 싶은 때도 있다고 했다:
사실 이건 주의점이 아니다. MongoDB와는 JSON 유사 형식으로 상호작용하지만, 그렇다고 JSON 유사한 것의 필드를 열처럼 다루지 못하는 것은 아니다. 여전히 필터링된 컬렉션으로 생각할 수 있다.
Cassandra 같은 조인을 지원하지 않는 NoSQL 데이터베이스에서는, 종종 행에 배열 등 구조화된 데이터를 넣는 것 외에는 대안이 없다. 이런 경우, 우선 조인이 있는 관계형 환경이라고 생각하고 설계한 뒤, 필요할 때 조인을 구조화된 열 질의로 대체하는 식으로 구조화된 필드를 추가하라.
때로는 데이터베이스에 불투명한 데이터를 밀어 넣는 게 편리할 수 있다. 예컨대 다른 시스템에서 의미를 갖지만 우리가 이해하지 못하는 블랍을 저장하는 경우다. 이때는 구조가 무엇인지 몰라서, 해당 구조의 일부를 선택하는 질의를 할 것이라 기대하지 않는다.
과제: 최근에 작업한 코드의 데이터 구조를 살펴보라. 선택하거나 순회하던 컬렉션을, 필터링하는 테이블로 바꿀 수 있도록 그 데이터 구조를 어떻게 관계로 재구성할지 생각해 보라.
우리는 이미 단일 값을 선택하도록 조직된 데이터를 데이터베이스로 가져오지 말자는 이야기를 했다. 더 극단적인 경우로, 같은 종류의 데이터에 완전히 다른 테이블을 만드는 경우도 가끔 본다.
예를 들어, 서로 다른 책 컬렉션을 가진 사용자들이 있다고 하자. 정상적인 방식은 사용자에 대한 단일 테이블과 책에 대한 단일 테이블을 갖는 것이다. 특정 데이터 조각에 대한 손잡이를 붙이는 식으로 생각하면, 특정 사용자의 책들에 대한 손잡이를 갖고 싶어져서 각 사용자마다 다른 책 테이블을 만드는 유혹을 받을 수 있다.
보통은 누군가가 그런 일이 벌어지기 전에 말린다. 하지만 내가 한 번 상대했던 어느 작은 회사는 각종 센서에서 데이터를 수집하고 과거 데이터를 검색할 수 있게 하는 일을 했다. 그들은 센서마다 별도의 테이블을 만들었다. 센서가 이동하거나 교체될 때마다 새 id를 부여하고, 예전 테이블은 삭제하고 새 테이블을 만들었다. 나는 이렇게 말했었다. 고객이 센서를 재구성할 때마다 그 센서의 과거 데이터를 몽땅 삭제하는 셈이라고. 돌아온 대답: “네, 그래서 디스크 사용량이 정말 줄어요.”
정말 뭐라고 답해야 할지 모르겠더라.
그 회사는 고객마다 별도의 데이터베이스를 만들기도 했다. 이게 항상 나쁜 아이디어는 아니다. 소프트웨어 전체, 데이터베이스까지 고객별로 완전히 병렬적이고 독립적인 사본을 운영한다면, 고객 간 격리가 꽤 쉬워진다. 이 경우는 데이터베이스만 쪼갰다.
데이터베이스만 쪼개는 것도 항상 나쁜 건 아니다. 데이터를 서로 다른 머신으로 나누는 것은, 하나의 머신에 다 안 들어갈 때 데이터베이스를 확장하는 전형적인 방식이다. 이를 “샤딩(sharding)”이라 한다. 하지만 “고객 1명당 샤드 1개”가 정답인 경우는 드물다. 이 회사에서는 AWS 비용의 절반 정도가 각 고객 한 명만을 위해 띄워둔 데이터베이스들의 유휴 컴퓨트와 비어 있는 디스크였다.
이 모든 게 당시에는 꽤 괴로웠지만, 요점을 아주 잘 보여준다. 데이터베이스의 데이터 조각에 손잡이를 억지로 붙이려 하지 말라. 원하는 답으로 필터링해서 나아가라.
과제: 이 모든 센서의 데이터를 효율적으로 샤딩하려면 어떻게 할지 생각해 보라. 데이터를 어디로 보낼지 결정하는 키로 무엇을 쓸 수 있을까? 샤딩을 해도 여전히 컬렉션을 필터링하는 관점으로 추론할 수 있을까?
명명된 손잡이로 특정 데이터 조각을 붙들고 있는 명령형 사고를 데이터베이스로 끌고 오다 보면, 또 다른 방식으로 길을 잃는다.
많은 데이터베이스 입문자들은 다음과 같이 표기된 외래 키를 보고
this_column REFERENCES other_table(other_column)
“아! 이건 명령형 언어에서의 레퍼런스나 포인터 같구나.”라고 생각한다. 프로그래밍 경험이 전부 명령형이었다면 합리적인 추측이다.
하지만: 외래 키는 질의와는 아무 상관이 없다. 그것은 우리가 할 수 있는 변경을 제약하는 수단이다. 위 선언에서 this_column에 사용할 수 있는 값은 other_table의 other_column에 있는 값뿐이다. 예를 들어, 사용자가 가질 수 있는 권한에 대한 테이블과, 사용자에게 권한을 할당하는 테이블이 있고, 후자가 권한 테이블을 참조하는 외래 키를 갖고 있다고 하자. 그러면 존재하지 않는 권한을 사용자에게 할당하는 일을 막는다.
이건 사람들을 헷갈리게 하는 사소한 포인트지만, 이것과 유사한 많은 혼란을 막을 수 있는 일반적인 휴리스틱이 있다. 우리가 명령형 코드를 작성할 때, 값 주위에는 세 가지 범주의 “것들”이 있다:
x는 정수다” 또는 “x는 Frobnicator의 인스턴스다” 같은 진술로, 메모리를 어떻게 해석할지를 알려준다.데이터베이스의 범주는 아주 다르다. 먼저, 우리가 다루는 모든 것이 ‘테이블’ 타입이라는 사실부터 보자. 행을 필터링하고, 파생된 열을 계산하고, 테이블을 조인한 결과는 또 다른 테이블이다. 어떤 테이블이든(이론상) 그것을 만들어낸 연산을 거슬러 올라가 추적할 수 있다. 예를 들어 다음 쿼리의 결과는
SELECT id, name, organization
FROM password_auth
LEFT JOIN users ON users.id = password_auth.id
WHERE password_auth.hash = '5gS32jaksk'
AND users.email = 'boris@madbaboon.com'
하나의 테이블이지만, 그것을 password_auth와 users 테이블로 거슬러 올라가 추적할 수 있다. 그중 하나가 뷰였다면, 그 뷰가 읽는 테이블들로 더 거슬러 올라갈 수 있다. 테이블을 거슬러 내려가다 보면 어느 시점에는 바닥(ground)에 닿게 되고, 사용자에 의해 데이터가 삽입되고 다른 테이블로 정의되지 않은 테이블들을 만나게 된다.
명령형 언어에서의 위 세 범주에 대응하는 데이터베이스의 두 범주는 ‘기저 테이블(ground table)’과 ‘비기저 테이블(non-ground table)’이다. 비기저 테이블을 생각할 때의 “것들”은 전부 테이블을 조작해 다른 테이블을 만들어내는 것들이다. 기저 테이블을 생각할 때의 “것들”은 데이터의 정확성과 무결성을 제약하고 검사하는 것들이다. 일반적으로 SQL 명령은 이 두 범주 중 하나에 들어맞는다. 그러니 CREATE TABLE 명령에서 외래 키를 보게 되면 이렇게 생각하라:
이 지점에서 명령형 직관을 수정하고, 외래 키가 무엇을 하는지 찾아봐야 한다는 것을 알게 된다.
여담으로, ‘기저 테이블(ground table)’이라는 용어는 데이터베이스 문헌에서 찾기 어렵다. Prolog에서 훔쳐온 말이다. Prolog에서는 테이블 대신 항(term)이라는 말을 쓰고, Prolog 프로그래머들은 기저 항(ground term)과 비기저 항(non-ground term)이라고 부른다. 데이터베이스 세계에서 보통 쓰는 용어는 기저에 해당하는 “데이터 정의(data definition)”와 비기저에 해당하는 “데이터 조작(data manipulation)”이다.
왜 흔한 용어 대신 다른 언어에서 용어를 빌려올까? 배우기 쉬워지기 때문이다. 특히, 인덱스는 데이터 정의일까 데이터 조작일까? 인덱스는 스키마나 테이블에 허용되는 값을 바꾸지 않으므로 데이터 정의처럼 느껴지지 않는다. 그렇다고 조작의 일부도 아니다. 쿼리가 사용하는 대상이기 때문이다. 반면 기저/비기저의 구분에서는 인덱스가 분명히 기저 범주다. 인덱스는 전적으로 기저 테이블 위에, 그리고 그것을 기준으로 정의되기 때문이다. 내 머릿속에서는 “데이터 정의”를 “기저”, “데이터 조작”을 “비기저”로 번역해 스스로를 헷갈리지 않도록 한다.
과제: 트랜잭션과 뷰를 생각해 보라. 그것들은 기저일까 비기저일까? 뷰의 경우, 공통 테이블 식(CTE)의 전역 버전이라고 생각하면 도움이 될 수 있다.
명령형 프로그램을 작성할 때 우리는 정신적 템플릿을 층층이 쌓고 순서를 짠다. 예를 들어, 리스트에서 음수가 아닌 모든 수의 합을 구하는 작업을 생각해 보자.
우리는 리스트에 대해 어떤 집계를 하고 있다. 루프가 필요하다는 걸 안다. 그래서 뼈대를 세운다.
for x in xs:
...
그리고 집계를 하려면 반복 간에 집계 상태를 담아둘 변수가 필요하다. 그래서 그것도 추가한다.
agg = ...
for x in xs:
....
집계는 합이니, 그 이름을 바꾸자.
sum = ...
for x in xs:
...
빈 리스트를 넘기면 합은 0이어야 하니 초기값이 정해진다.
sum = 0
for x in xs:
...
우리는 음수가 아닌 정수만 원한다. 음수 원소는 건너뛸 것이다. if ...: break를 뼈대로 세우고, 조건은 x가 최소 0 이상인 것이다.
sum = 0
for x in xs:
if x >= 0:
break
...
그리고 집계를 갱신하는 단계가 있다. 이 경우는 꽤 단순하다.
sum = 0
for x in xs:
if x >= 0:
break
sum += x
이런 방식이 명령형 프로그래밍에서 꽤 전형적이다. 우리는 시작점과 원하는 종착점이 있고, 학습을 통해 축적한 여러 가능한 수들을 갖고 있다. 체스 선수처럼 앞을 내다보고 수를 고른다. 체스 선수와 달리 우리는 되돌아가 다시 시도할 수 있다.
데이터베이스에서 쿼리를 작성할 때는 반대 방향으로 간다. 우리가 원하는 최종 값들의 테이블에서 시작해, 바닥(ground)에 닿을 때까지 뒤로 빈칸을 채운다. 같은 작업을 생각해 보자.
우리가 원하는 최종 테이블은, 테이블의 한 열에서 음수가 아닌 수들의 합을 담은 단일 행/단일 열의 테이블이다. 우리는 테이블의 합을 선택하여 그것을 얻는다.
SELECT sum(n) FROM ...
우리가 원하는 행들은 어디에서 오는가? 음수가 아닌 행만 가진 테이블에서 온다.
SELECT sum(n) FROM (
SELECT n FROM ...
WHERE n >= 0
)
그리고 아마 우리가 숫자를 뽑아오는 xs라는 테이블이 있을 것이다.
SELECT sum(n) FROM (
SELECT n FROM xs
WHERE n >= 0
)
실전에서는 진행하면서 이것을 다음처럼 접을 것이다.
SELECT sum(n)
FROM xs
WHERE n >= 0
하지만 밑바탕의 사고 과정은, 우리가 원하는 테이블의 구조를 채우기 위해, 기저를 향해 아래로 향하는 방향성 비순환 그래프를 구축해 가는 것이다.
과제: 최근에 했던 프로그래밍 작업 몇 가지를 가져와, 그것을 어떻게 했는지 돌아보라. 명령형 프로그래밍에서 당신의 ‘수’ 또는 템플릿 목록은 무엇인가? 각 템플릿의 목표는 당신의 위치를 어떻게 개선하려는 것인가? 그 목표 중 얼마나 많은 것이 관계형 환경에서도 의미가 있는가? 의미가 있는 것들은 관계형으로 어떻게 표현할 수 있는가?
지금까지의 내용을 요약하자.
첫째: 관계형 프로그래밍은 명령형 프로그래밍에서 큰 전환이다. 당신은 명령형 프로그래밍을 위한 정신적 도구들을 배우는 데 수년을 보냈다. 그중 일부는 전이되므로 관계형 프로그래밍을 배우는 데 그만큼 오래 걸리지는 않을 것이다. 하지만 많은 것들은 전이되지 않기에, 그래도 학습 곡선은 있을 것이다.
전환을 하면서, 우리가 데이터를 집어드는 방식에 대한 세 가지 핵심 가정을 바꿔야 한다:
데이터베이스를 배우는 사람들이 겪는 가장 큰 문제는 명령형 가정에 매달리는 데서 온다. 이는 배열이나 JSON을 열에 밀어 넣으려 하거나, 특정 데이터 조각의 손잡이 느낌을 만들려고 테이블을 쪼개는 식으로 나타난다.
우리가 다루는 “것들”의 범주도 바뀐다. 타입과 타입에 맞는 값, 값을 검사·조작하는 코드로 데이터를 조작하던 대신, 우리는 기저 테이블과 비기저 테이블을 갖는다. 기저 테이블에서는 데이터 무결성과 정확성을 신경 쓴다. 비기저 테이블은 다른 테이블들로부터 구성해 우리가 실제로 원하는 결과를 얻는다.
이 다른 범주들은 프로그래밍 접근법에도 나타난다. 초기 상태에서 원하는 상태로 체스 두듯 ‘수’를 두는 대신, 원하는 테이블에서 시작해 기저에 닿을 때까지 소스를 채워 넣는다.
데이터베이스를 배우는 데는 세 가지 큰 조각이 있다. 보통 배우는 순서는 다음과 같다:
대부분 데이터베이스에서 튕겨 나오는 사람들은 첫 번째에서 그렇다. 지금쯤이면 연습이 필요하겠지만, 뇌를 어떻게 재배선해야 하는지 알 수 있는 위치에 있을 것이다.
축하한다. 가장 큰 허들이 넘어갔다.
(또한, 이제 그 허들을 넘겼으니 다른 [프로그래밍 언어 계열]로의 전환도 덜 고통스러울 것이다. 한 번 해봤으니, 어떤 느낌이어야 하는지 안다. 다음 휴가에는 Haskell이나 Forth처럼 정말 색다른 것을 배워보는 것도 고려하라.)
두 번째 파트인 성능은 보통 불필요하게 복잡해진다. 그중 80%~90%는 모든 데이터베이스에 공통이며, 올바른 정신 모델만 자리 잡으면 간단하다. 명령형 언어에서 리스트를 필터링하고 합을 구하고 락을 걸고 푸는 법을 안다면, 데이터베이스 성능에 대해 추론하는 법을 배울 수 있다.
세 번째 파트인 데이터 모델링은 훨씬 더 철학적이 된다. 우리 세계의 어느 부분에 대해서도 단 하나의 기호적 모델이 있는 것은 아니며, 더 자연스럽거나 더 올바른 모델이 있는 것도 아니다. 돌아보면 이미 알고 있는 사실이다. 당신은 세계에 대한 습관적 모델을 갖고 있지만, 때로는 그것이 무너져 당황하게 된다. 그런 상황에서는 무슨 일이 일어나는지, 무엇에 주의를 기울여야 하는지에 대한 단서를 찾고, 사용하려던 모델이 현실과 맞지 않는 지점에서 마음이 불편함을 속삭인다. 데이터 모델링을 배우는 것은 이러한 자연스러운 반응을, 눈앞에 있지 않은 도메인으로 향하도록 도구를 써서 이끄는 일이다.
« 홈