발명과 발견

바벨의 도서관이라는 것이 있다. 호르헤 루이스 보르헤스의 소설 제목인데, 이 소설의 배경이기도 하다. 도서관에 있는 모든 책은 각각 400페이지이고, 한 페이지에는 1600자의 글자가 들어있다. 모든 글자는 알파벳과 공백( ), 콤마(,), 마침표(.)로만 구성되어 있다. 도서관에는 이 글자들로 조합가능한 모든 책이 나열되어있다. 대부분의 책은 의미없는 쓰레기고, 가치있는 책을 찾기 위해 사람들이 돌아다닌다.

누군가 도서관에서 ‘무한동력 배터리’의 제조법을 찾기 위해 평생을 바쳤다. 그는 수 많은 책을 살펴봤고 마침내 제조법이 적힌 책을 찾아내었다. 이 제조법을 아직 인류에게 알려지지 않은 것이다. 그는 발명을 한 것일까, 발견을 한 것일까?

이제 도서관 밖을 보자. 누군가 ‘무한동력 배터리’의 제조법을 찾기 위해 평생을 바쳤다. 그는 무수한 가능성을 열어둔 채 수 많은 실험을 했고 마침내 제조법을 찾아내었다. 이 제조법은 위의 제조법과 같은 것으로 역시 아직 인류에게 알려지지 않은 것이다. 그는 발명을 한 것일까, 발견을 한 것일까?

위의 물음과 아래의 물음에 대한 대답이 다르다면 발명과 발견의 차이는 무엇일까?

여러 가능성 중 무언가 만드는 방법을 찾는 것을 우리는 발명이라 불렀다. 그러나 우리 우주에 존재하는 원자의 종류와 개수는 유한하고, 물리법칙은 불변이다. 즉 무언가를 만들 수 있는 경우의 수는 유한하다. 도서관의 책들도 일정한 조건 하에 유한한 가능성의 집합이다. 이 책 더미 속에서 무언가 만드는 방법을 찾는 것을 발명이라고 할 수는 없을까?

도서관의 책과 마찬가지로 우리 우주 안에서 어떤 것이 존재할 수 있는지 없는지는 이미 결정되어있다. 우리가 배운게 틀리지 않았다면 현실에서 ‘무한동력 배터리’는 존재할 수 없다. 어떤 실험을 해도 만들 수 없고, 도서관에서 진짜 ‘무한동력 배터리 제조법’는 찾을 수 없다.

말하는 재미

요즘 친구 H군과 함께 영어로 말하고 듣는 연습을 하고 있다.

영어를 잘하는 이 친구는 군대 때문에 한국에 잠시 왔다가 졸업하기 위해 올해 11월에 다시 미국으로 건너갈 예정이다. 나는 H군이 미국가기 전까지 내 영어 연습을 도와주면 합당한 금액을 주겠다고 제안했고, 그는 돈은 됐고 만날때마다 맛있는거나 사달라고 하면서 딜이 성사됐다. 일주일에 세 번정도 만나는데 맛있는 걸로만 먹으러 다니다보니 매번 밥값만으로도 큰 지출이 발생하고 있다. 그래도 시간을 내준 H군이 고마워서 돈은 전혀 아깝지 않다.

영어를 연습하려면 말을 많이 해야한다. 말을 많이 하기 위해서 H군과 만나기 전에 무엇을 얘기할지 주제를 몇 가지 골라서 간다. 내가 준비한 주제라서 내가 더 많이 알고 있고, 자연스레 더 많이 말을 할 수 있다. 대화는 주제만 딱 얘기하고 끝나지 않고 우리만 알고 있는 이야기나 공통의 다른 관심사로 자유롭게 흐른다.

H군과의 영어 대화는 내게 편하다. 내가 틀린 영어를 쓰지 않을지 걱정하지 않고 자연스레 일단 뱉어본다. 내가 어떤 개드립을 쳐도 이상하게 보이지 않을 걸 알기에 이런 저런 생각을 말로 표현한다. 어떤 단어를 써야할지 모르는 경우엔 듣는 사람이 답답하든 말든 신경쓰지 않고 그 단어의 개념을 풀어서 설명해보려 애쓴다. (이게 더 연습된다는 핑계로 단어는 전혀 외우지 않는다)

나는 말하기보단 듣기를 더 좋아해서 내가 재미있어하는 것들에 대한 이야기를 잘 하지 않는다. 그런데 영어 연습을 하면서 말하기와 친해지려 노력해보니 이게 듣는 것만큼 재미있다는 것을 알았다.

Citizen cb3010-57e를 사다

뜬금없이 시계를 샀습니다. 신경쓰지 않아도 항상 정확한 시간을 보여주고, 너무 조심하지 않아도 스크래치가 잘 나지 않는 시계를 갖고 싶었습니다. 기계식 시계는 당연히 제외하고, 충전이 필요한 스마트워치도 제외하면 전자식 쿼츠 밖에 남지 않더라구요.

쿼츠 시계는 오차를 보정할 수 있는 방법이 많습니다. 전파를 받아서 시간을 보정하는 시계까지는 예전에도 봤었는데, 요즘은 스마트워치가 아니라 일반 시계에도 블루투스와 GPS가 들어갑니다. 아직 모듈의 크기가 커서 그런지 이런 시계들은 약간 크긴 큽니다. 보통 전파시계의 지름이 38~40mm정도 하는반면 GPS 시계들은 42~44mm의 지름을 가지고 있습니다. 손목이 얇은 사람들은 GPS 시계로 캡틴아메리카를 연출할 수 있습니다.

기계식 시계의 매력 중 하나는 배터리가 필요없다는 것입니다. 쿼츠는 시계가 가지 않으면 배터리를 교체해야 합니다. 그래서 이젠 태양빛으로 배터리를 충전합니다. 여러 브랜드에서 다양한 이름으로 부르지만 대체로 기능과 성능은 비슷한 것 같습니다. 책상 구석에 쳐박아두지 않는다면 시계를 차고 다니면서 충전되기 때문에 마르지 않는 구동전력을 얻을 수 있습니다.

스크래치가 잘 나지 않으려면 경도가 강해야합니다. 시계의 케이스로 스테인리스 스틸과 티타늄을 많이 씁니다. 둘의 무게는 차이가 크지만, 경도는 사실 비슷합니다. 그래서 많은 시계 회사들이 이 재료들의 경도를 높히는 기술을 보유하고 있습니다. 하지만 스테인리스는 여전히 무겁습니다. 스테인리스 스틸줄 시계를 찬다면 손목에 스마트폰 하나를 올리고 있는 것과 같습니다.

제가 원하는 조건을 가진 모델이 몇 가지 있습니다. 가볍지만 단단하고, 너무 크지 않고도 오차를 자동으로 보정해주는 태양광 충전 시계. G-SHOCK의 최상위 라인을 제외하면 가격도 비교적 합리적인 편입니다. (여전히 합리적이지 않습니다) 그 중에서 좋은 스펙을 가지고 있지만 디자인이 깔끔한 시티즌의 cb3010-57e를 구매했습니다.

스펙을 나열하자면…

  • 시계 전체 무게가 87g로 가볍습니다. 티타늄이 아니라 스테인리스 스틸을 사용했다면 140g쯤 되었을 것 같습니다.
  • 케이스 크기는 40mm로 둘레 16cm인 제 손목에 딱 적당합니다.
  • 비싼 시계에만 쓰인다는 자라츠 연마 기술을 적용했습니다. 이게 뭔가 찾아보니까 거울처럼 매끈하게 표면을 갈았다는 얘깁니다. 손목에 차보면 피부 주름까지 반사하는 걸 볼 수 있습니다.
  • 듀라텍트라는 경도 강화 기술을 적용해서 일반 티타늄 혹은 스테인리스스틸보다 약 6배 스크래치에 더 강합니다.
  • 매일 라디오 전파를 수신해 오차를 보정합니다. 그냥 두면 자동으로 수신을 시도하고, 수동으로도 할 수 있습니다. 영국, 독일, 중국, 일본, 미국의 수신국으로부터 전파를 받습니다. 높은 곳에서 남쪽을 바라보게 두면 수신률이 높아진다는데…
  • 태양광으로 충전합니다. 완충시 추가 충전없이 방전까지 2년이 걸린다고 합니다. 배터리 잔량도 확인할 수 있습니다. 충전 경고 기능과 과충전 방지 기능도 있습니다.
  • 절전기능. 빛이 없는데 움직임도 없으면 시계가 움직이지 않습니다. 절전모드가 해제되면 다시 정확한 시간으로 알아서 맞춰집니다. 처음에 배송받고 상자를 딱 열면 바로 이 기능을 볼 수 있습니다.
  • 퍼페추얼 캘린더. 다음달로 넘어갈때 날짜를 자동으로 조정합니다. 윤달까지 알아서 계산합니다.
  • 24개 도시의 세계시간을 볼 수 있습니다. 서머타임도 적용합니다.
  • 충격 감지 기능. 이 때 삐뚤어진 바늘을 자동으로 보정합니다.
  • 항자성 기능. 직류자계 4,800A/m까지 견딜 수 있습니다. 저는 무슨 말인지 모르겠습니다.
  • 사파이어 크리스탈에 무반사 코팅을 적용했다는데 잘만 반사합니다.
  • 야광 있구요.
  • 100m 방수됩니다.

이런 저런 기능이 있으면 시계 앞에 자랑할만도 한데, 앞은 깔끔합니다. 기능 자랑은 시계 뒷면에 해놨습니다. 좌측에는 용두와 버튼 2개가 있습니다. 버튼은 숨어있어서 앞에서 보면 보이지 않습니다. 버튼을 누르려면 뾰족한 게 필요합니다.

의도하진 않았는데 배송이 딱 2월 28일에 와서 날짜가 넘어가는걸 바로 볼 수 있었습니다. 열 두시 땡 하면 날짜가 넘어갑니다.

사진으로는 실물의 느낌을 담을 수가 없어서 안타깝습니다. 정말 작고 단단해보입니다.

Failure building an AWS Lambda custom runtime for Typescript with Deno

Now AWS Lambda supports custom runtime. And there is TypeScript runtime called Deno. Is it possible to run ‘.ts’ in Lambda using Deno? Let’s try it. Never mind the title.

What is Deno

Deno is a secure JavaScript / TypeScript runtime built on V8. It is led by Ryan Dahl who created Node.js. If you are interested in Deno, check his presentation at JSConf EU 2018.

What is a custom runtime

Simply, you can also run a function in any language that is not provided by AWS. A runtime is a program that runs a Lambda function. You can build your own runtime to run some other languages.

Building a custom runtime

Download Deno from Github repository, unzip it, and rename deno_linux_x64 to deno.

.
└─ deno

According to Custom AWS Lambda runtimes, the runtime is responsible for jobs including getting an event and a context, invoking the function, and handling the response or errors. Instead of implementing them, I forked lambci/node-custom-lambda and migrate bootstrap.js code to runtime.ts for Deno. Here is runtime.ts.

.
├─ deno
└─ runtime.ts

A custom runtime’s entry point is an executable file named bootstrap. Make a shell script file named bootstrap invoking runtime.ts.

bootstrap
1
2
#!/usr/bin/env bash
/opt/deno --allow-all /opt/runtime.ts
.
├─ bootstrap
├─ deno
└─ runtime.ts

Zip these files and create a Lambda layer. If you don’t have aws-cli, install it

1
2
3
4
5
6
7
$ zip -r layer.zip bootstrap deno runtime.ts
$ aws lambda publish-layer-version \
--layer-name deno_027 \
--description "Deno v0.2.7" \
--compatible-runtimes provided \
--license-info MIT \
--zip-file fileb://layer.zip

Add new file named function.ts, zip it, and create a lambda function. Replace ARNs to yours in below snippet.

1
2
3
4
5
6
7
8
9
$ echo "export const handler = () => ({ Hello: 'World!' })" >> function.ts
$ zip -r function.zip function.ts
$ aws lambda create-function \
--function-name deno-test \
--runtime provided \
--role arn:aws:iam::xxxxxxxxxxxx:role/lambda-role \
--handler function.handler \
--layers arn:aws:lambda:ap-northeast-2:xxxxxxxxxxxx:layer:deno_027:1 \
--zip-file fileb://function.zip

Now it is time to try.

1
$ aws lambda invoke --function-name deno-test response.txt

See the response.txt. Fail…

1
2
3
4
{
"errorType": "Runtime.ExitError",
"errorMessage": "RequestId: c25b323a-c386-44c6-b147-9273deee0ab9 Error: Runtime exited with error: exit status 1"
}

On the CloudWatch log, I see this message.

1
/opt/deno: /lib64/libc.so.6: version `GLIBC_2.18' not found (required by /opt/deno)

Deno requires GLIBC_2.18 which is not included in Lambda container environment. There is no way to install it to Lambda container environment.

I think ts-node might be a custom runtime, but I’m not attracted to that than Deno.

iOS만 11년... 하지만 구글 픽셀 3(Google Pixel 3)을 사다

아이폰 SE의 두번째 버전이 나오면 사려고 일 년을 기다렸다. 하지만 애플은 ‘그런거 없다’를 시전했다.

나는 핸드폰으로 게임도 안하고 영상도 거의 보지 않아서 작고 가벼운 폰이 필요하다. 그래서 아이폰 5를 6년동안 쓸 수 있었다. 여전히 속도와 카메라, 배터리 빼고는 불만이 없다.

아이팟터치부터 iOS만 11년을 썼다. 몇 년동안 아이폰과 iOS의 신기능들은 더 이상 나에게 새로움을 주지 않았다. 그저 좀 더 나은 아이폰을 위해서 익숙함에 130만원을 지불하고 싶진 않았다.

구매

미국 구글스토어에서 $799에 구매했다. 핸드폰 가격이 백만원에 달하는 2018년, 픽셀3도 이전의 픽셀2와 크게 변한게 없음에도 가격을 $150 더 받는다.

픽셀3은 한국에서 정식발매하지 않았다. 아마 앞으로도 하지 않을 것 같다. 수리를 받을 수 없어서 고장나면 끝이다. 고장나면 그냥 익숙함에 130만원을 쓸 껄하고 후회할지도 모른다.

픽셀3의 패키지는 아이폰보다 알찬 구성에 15W 충전기를 제공한다.

무게

픽셀 3는 최근 출시된 플래그십 폰 중에서 아마 제일 가벼운 폰이다. 여차하면 사오려고 대만가서 아이폰 XS를 꽤 오래 만져봤는데 무겁긴 하더라.

  • 아이폰 5: 112g
  • 아이폰 8: 148g
  • 픽셀 3: 149g
  • 아이폰 XS: 177g

픽셀3도 아이폰5에 비하면 여전히 많이 크고 무겁지만, 최선의 선택이다.

안드로이드

iOS와는 다르게 안드로이드는 제조사와 통신사의 입맛에 맞게 개량된다. 픽셀은 아이폰처럼 하드웨어와 소프트웨어가 같은 곳에서 만들어진다. 그러니까 픽셀을 사용하면 OS 개발 의도에 맞는 순수한 안드로이드를 경험할 수 있다. UI 뿐만 아니라 성능까지도. 그래서 애플처럼 구글도 램크루지가 되었다.

하지만 키보드와 스크롤은 적응을 잘 못하고 있다.

구글 홈과 구글 홈 미니를 사다


구입하려고 생각하고 있었는데 마침 한국 정식발매 소식에 바로 질렀다.

미니는 거실에 두고 부모님 음악감상용. 홈은 내 방에 두고 블루투스 스피커 + Hue 조명 조절용.

블루투스 스피커로 사용하거나 또는 다른 블루투스 스피커로 출력할 수 있기 때문에, 부모님 쓰시는거 봐서 마이크와 스피커가 좋은 홈을 거실로 옮기고 미니는 블루투스 스피커를 연결해서 내 방에 둘 예정이다.

Home Assistant와 함께 제대로 사용하려면 구글 홈에 내장된 구글 어시스턴트와 크롬캐스트 모두 연결하는게 좋다.

크롬캐스트와 연결은 아주 쉽지만 구글 어시스턴트 연결을 위해선 Actions on Google에서 프로젝트를 만들어서 연결해야한다.

연결을 하고나면 구글 홈에게 음성으로 명령해서 Home Assistant를 통해 Hue를 조정할 수 있다. Home Assistant를 통하지 않고 바로 연결할 수도 있지만 추후에 자동화 및 타 기기 연동을 위해선 모든 기기를 한 곳에서 관리할 수 있는 장치가 필요하다.

Hass.io에 커스텀 도메인 SSL 인증서를 설치하다

문제

Hass.io에서 DuckDNS 애드온은 SSL 인증서도 쉽게 설치할 수 있도록 해준다. 하지만 커스텀 도메인을 지원하지 않는다. DuckDNS를 쓰면서 커스텀 도메인으로 접속하면 이런 메시지를 볼 수 있다. SSL 인증서의 도메인과 접속하려는 도메인이 다르기 때문이다.

해결

커스텀 도메인의 SSL 인증서를 만들면 해결할 수 있다. Let’s Encrypt 애드온을 설치한다. 애드온 문서 앞에 DuckDNS 애드온과 함께 사용하지말라는 경고는 무시한다.

시작하기 앞서 hass.io가 설치된 머신의 80번 포트가 포트포워딩되어있는지 확인한다.

Let’s Encrypt 애드온의 config를 수정한다. certfilekeyfile의 이름을 변경한다. DuckDNS 애드온의 파일명과 같으면 안된다.

1
2
3
4
5
6
7
8
{
"email": "your@email.com",
"domains": [
"your.domain"
],
"certfile": "custom-fullchain.pem",
"keyfile": "custom-privkey.pem"
}

config를 저장하고, 애드온을 실행한다. log에서 진행상황을 볼 수 있다.

인증서 설치가 끝났으면, DuckDNS 애드온을 제외하고 ssl 인증서를 사용하는 모든 설정을 위에서 변경한 키 파일로 수정한다.

1
2
3
4
5
# configuration.yaml
http:
base_url: https://your.domain
ssl_certificate: /ssl/custom-fullchain.pem
ssl_key: /ssl/custom-privkey.pem

마지막으로 홈 어시스턴트를 재부팅한다. 인증서를 갱신하는 자동화도 마련하는 것으로 정말 끝.

원인

DuckDNS이 Let’s Encrypt로부터 SSL 인증서를 발급받기 위해 사용하는 도메인 소유 증명 방법으로는 커스텀 도메인을 소유 증명할 수 없기 때문이다.

Let’s Encrypt 문서를 보면 도메인 소유를 증명하는 방법은 dns-01과 http-01이 있다. DuckDNS는 dns-01을 사용하여 SSL 인증서를 발급받는데, 이를 위한 api도 제공하며 DuckDNS 애드온도 이 api를 사용한다.

dns-01로 커스텀 도메인을 인증하려면 Let’s Encrypt로부터 토큰을 받고, 도메인 제공 서비스에서 DNS TXT 레코드를 추가해야한다. Let’s Encrypt 애드온은 http-01로 도메인을 증명하기 때문에 별다른 추가 작업 없이도 커스텀 도메인도 소유를 증명할 수 있다.

잘 설계된 DynamoDB 앱은 단 하나의 테이블만 필요합니다

어그로 오지는 이 제목은 아마존 문서에서 발췌했다.

데이터를 어떻게 정의할 것인가

DynamoDB를 처음 접하면서 가장 어려웠던 부분이다. NoSQL 테이블 디자인에 대해 구글링하면서 가장 많이 봤던 문장은

서비스를 먼저 디자인하고 어떤 쿼리가 필요한지 파악한 후 테이블을 디자인한다.

서비스를 운영하면 기능을 변경하거나 추가할 필요가 생긴다. RDB라면 필요한 데이터를 미리 정의해두고 쿼리만 바꾸면서도 기능을 변경하거나 추가할 수 있다. 그런데 저 문장은 마치 NoSQL은 고정된 디자인을 가진 서비스를 위한 데이터베이스라고 말하는 듯 했다.

며칠을 고민하면서 겨우 실마리를 찾은 것 같다. RDB를 쓸 때도 필요하다면 쿼리를 바꾸는 것보다는 비용이 더 들긴 하지만 스키마를 변경할 수 있다. NoSQL을 쓰는 것도 필요하다면 쿼리를 바꿀 수 있다. 다만 이미 저장된 데이터는 기존 쿼리에 최적화되어 있으니, 새로운 형태의 데이터를 저장하면 된다. RDB와는 다르게 중복 저장도 환영이다.

자원이 많다면 가능한 모든 쿼리를 고려하여 데이터를 중복 저장할 수 있다. RDB를 사용할 때처럼 하나의 데이터타입에 하나의 테이블을 사용할 수도 있다. 그러나 DynamoDB의 과금 정책상 이 방법은 효율적이지 않다.

데이터를 어떻게 저장할 것인가

이제 간단한 게시판 서비스를 하나의 테이블만으로 만들 것이다. 물론 코드는 한 줄도 없고, 과정 속에서 서로 다른 형태의 데이터가 어떻게 하나의 테이블에 저장될 것인지 설명한다.


1:1

STORY: 사용자는 이메일로 계정을 생성하고 로그인할 수 있다

사용자 정보를 저장할 테이블을 하나 구상한다.

id (PK) createdAt (SK) email hashed_password
USER-1 createdAt user@email.com password

‘id’ 속성의 USER-1은 데이터의 타입도 함께 포함한다. 이것은 하나의 테이블에서 서로 다른 데이터를 구별하기 위한 방법으로 아래와 같은 형태를 가진다.

1
[DATA_TYPE]-[IDENTIFIER]

다음으로 이메일을 사용자 데이터와 따로 저장하기로 했다고 하자. 이제 새로운 데이터 형태를 하나의 테이블에 저장한다.

id (PK) createdAt (SK) email hashed_password
USER-1 createdAt EMAIL-1 password
EMAIL-1 email.com user@email.com

USEREMAIL은 1:1 관계다. 특정 사용자의 이메일을 불러오려면 먼저 USER-1을 쿼리하여 EMAIL-1을 얻고, 다시 EMAIL-1을 쿼리하여 'user@email.com‘을 얻는다.

하나의 테이블에 다른 데이터 형태가 있다보니 각 속성의 명칭과 내용이 다를 수 있다. 속성명을 일반화하자. NoSQL은 스키마가 정해져있지 않으니 새로운 속성이 필요한 데이터라면 얼마든지 속성을 추가할 수 있다.

PK SK Data hashed_password
USER-1 createdAt EMAIL-1 password
EMAIL-1 email.com user@email.com

USER-x로 이메일을 찾을 수 있지만, 이미 가입한 사용자가 맞는지 확인하려면 이메일로도 사용자를 찾을 수 있어야 한다. 이메일로 사용자를 찾을 수 있도록 GSI 추가한다.

PK (GSI-SK) SK Data (GSI-PK) hashed_password
USER-1 createdAt EMAIL-1 password
EMAIL-1 email.com user@email.com
1
2
3
4
5
6
# Find email by user
email_id = SELECT Data FROM TABLE WHERE PK="USER-1"
email = SELECT * FROM TABLE WHERE PK="EMAIL-1"

# Find user by email
user_id = SELECT id FROM GSI WHERE Data="user@email.com"

1:N

STORY: 사용자는 게시글을 작성할 수 있다

새로운 데이터타입 POST의 형태는 아래와 같다.

PK (GSI-SK) SK Data (GSI-PK)
POST-1 createdAt post body

사용자가 여러 POST를 가질 수 있도록 POST의 ‘Data’에 USER 키를 저장한다.

PK (GSI-SK) SK Data (GSI-PK) Body
USER-1 createdAt EMAIL-1
POST-1 createdAt USER-1 post body
POST-2 createdAt USER-1 post body
1
2
3
4
5
# Find user id by post
user_id = SELECT Data FROM TABLE WHERE PK="POST-1"

# Find post ids by user
post_ids = SELECT PK FROM GSI WHERE Data="USER-1"

다중 종속

STORY: 모든 게시글은 카테고리로 분류할 수 있다

카테고리 데이터타입 CATEGORYPOST와 1:N 관계가 있다. 하지만 POST 타입의 ‘Data’는 USER의 외부키를 저장하는 용도로 쓰이고 있기 떄문에 위의 디자인은 사용할 수 없다.

관계에 대한 새로운 데이터타입을 만든다. 기존 ‘Data’에 외부키는 새로운 관계 데이터로 옮긴다.

PK (GSI-SK) SK Data (GSI-PK) Body
USER-1 createdAt EMAIL-1
CATEGORY-1 category name
POST-1 createdAt post body
USER-1:POST createdAt POST-1
CATEGORY-1:POST createdAt POST-1

관계 데이터타입의 ‘id’ 속성 포맷은 다른 데이터타입과 명확하게 구별된다. 이것은 하나의 테이블에서 서로 다른 데이터를 구별하기 위한 방법으로 아래와 같은 형태를 가진다.

1
[PARENT_ID]:[CHILD_DATA_TYPE]

USER-1:POSTUSER-1이 소유한 POST에 대한 정보를 가지고 있다. 만약 USER-1POST-2도 소유하고 있다면 이런 데이터를 추가한다.

PK (GSI-SK) SK Data (GSI-PK)
USER-1:POST createdAt POST-2
1
2
3
4
5
6
7
8
9
10
11
# Find user id by post
user_id = SELECT PK FROM GSI WHERE Data="POST-1" AND PK=begins_with("USER")

# Find category id by post
category_id = SELECT PK FROM GSI WHERE Data="POST-1" AND PK=begins_with("CATEGORY")

# Find post ids by user
post_ids = SELECT Data FROM TABLE WHERE PK="USER-1:POST"

# Find post ids by category
post_ids = SELECT Data FROM TABLE WHERE PK="CATEGORY-1:POST"

데이터 상태

STORY: 게시글은 발행되지 않은 채로 저장할 수 있다

POST는 아직 발행하지 않은 ‘DRAFT’와 발행한 ‘PUBLISHED’의 2가지 상태가 있다. 정렬키 앞에 콜론(‘:’)으로 구분하여 상태를 저장한다.

PK (GSI-SK) SK Data (GSI-PK) Body
USER-1 createdAt EMAIL-1
POST-1 createdAt post body
POST-2 createdAt post body
USER-1:POST DRAFT:createdAt POST-1
USER-1:POST PUBLISHEDcreatedAt POST-2
1
2
# Find draft post ids by user
[ draft_post_ids ] = SELECT Data FROM TABLE WHERE PK="USER-1:POST" AND SK=begins_with("DRAFT")

별칭

STORY: 게시글은 질문과 답변 두 가지로 나눠진다.

사용자는 2가지 종류의 게시글을 소유하는데, USER-1:POST같은 방식은 소유한 게시글의 종류를 나눌 수 없다. 별칭을 사용하여 동일한 데이터 타입 간의 관계도 다르게 표현할 수 있다.

PK (GSI-SK) SK Data (GSI-PK) Body
USER-1 createdAt EMAIL-1
POST-1 DRAFT:createdAt post body
POST-2 PUBLISHED:createdAt post body
USER-1:question DRAFT:createdAt POST-1
USER-1:answer PUBLISHED:createdAt POST-2
1
2
# Find draft question post ids by id
[ draft_question_ids ] = SELECT Data FROM TABLE WHERE PK="USER-1:question" and SK=begins_with("DRAFT")

N:M

STORY: 사용자는 게시글은 태그를 추가할 수 있다

태그와 게시글은 N:M의 관계다. 위의 1:N 모델을 응용하면 N:M 관계를 쉽게 표현할 수 있다.

PK (GSI-SK) SK Data (GSI-PK)
POST-1 createdAt post body
TAG-1 createdAt tag name
POST-1:tag createdAt TAG-1
POST-1:tag createdAt TAG-2
POST-2:tag createdAt TAG-1
TAG-1:post createdAt POST-1
TAG-1:post createdAt POST-2
TAG-2:post createdAt POST-2

이 글의 제목과는 다르게 위의 디자인은 분명한 한계점이 있다. 데이터 모델을 추가하는 확장은 쉽게 할 수 있지만, 쿼리의 종류를 다양하게 할 수 없는 문제가 있다. 복잡한 어플리케이션에서는 분명 바로 쓰기 어려울 것이다.

단일 테이블은 괜찮다

하나의 테이블을 사용하는 DynamoDB 앱은 모든 프로비저닝 요소를 공유한다. 동시에 쓰이는 최대의 양만 파악한다면 쉽게 프로비저닝할 수 있다.

DynamoDB는 동시에 얼마나 많이 읽고 쓸 수 있을 것인지 설정한 양(이런걸 보통 프로비저닝이라고 한다)만큼 과금한다. 아마존에선 이것을 읽기 용량 유닛(RCU), 쓰기 용량 유닛(WCU)으로 부른다. RCU와 WCU는 테이블마다 설정해야한다.

테이블을 여러개 만들면 각각의 테이블마다 프로비저닝이 필요하고 아마존은 이 개수만큼 과금한다. 서비스 운영 초기에는 어떤 테이블이 얼마만큼의 프로비저닝이 필요한지 알 수 없다. 거의 접근하지 않는 테이블이라도 반드시 프로비저닝해야하고, 이는 과금의 요소가 된다.

단일 테이블은 별로다

낭비되는 인덱싱 데이터가 있을 수 있다. 앞선 예제에서 ‘Data’의 값으로 식별자가 아닌 값이 존재하는 항목은 인덱싱 테이블에서 쓸 모 없는 파티션키가 될 수 있다.

관리의 어려움도 동반할 수 있다. 각각의 데이터 형태와 정의를 다른 곳에 마련해야한다. 여러 사람 혹은 여러 팀이 협업하는 경우, 각 데이터의 접근을 코드에서 제한할 필요가 있다.


DynamoDB의 문서엔 단일 테이블을 사용한 예와 복수 테이블을 사용한 예가 공존한다. 나처럼 NoSQL에 생소한 사람들은 단일 테이블 예제가 더욱 낯설텐데, 이 글을 보고 도움이 되었으면 좋겠다.

라즈베리파이에 Home Assistant를 설치하다

집에서 놀고 있는 라즈베리파이에 hass.io를 설치한다.

내가 가지고 있는 라즈베리파이는 메모리가 512메가인 model B 버전이다. 이 모델은 와이파이가 없어서 유선랜을 연결해주어야 한다.

내 방에 들어오는 인터넷 선은 바로 데스크탑으로 연결된다. 이걸 데스크탑 뿐만아니라 라즈베리파이에도 연결해야한다.

집에서 놀고 있는 iptime 공유기를 가져다가 스위칭허브 모드로 전환하고 연결한다.

SD카드에 모델에 맞는 이미지를 다운받아 설치한다. 다른 모델의 이미지를 사용하면 라즈베리파이가 부팅이 되지 않는다.

라즈베리파이에 SD카드와 이더넷 선을 꼽고 전원을 연결한다. 그리고 http://hassio.local:8123/ 에 접속하면

20분 정도 기다려 달라고 한다. 구형 모델이라서 그런지 30분 걸렸다.

계정을 생성. 이런 류의 컨트롤 시스템은 망 외부에서도 조작할 수 있도록 대부분 계정으로도 연동한다.

필립스 휴 브릿지를 연결.

이제 홈 어시스턴트를 통해서도 전구를 조작할 수 있다.

필립스 휴 3.0 스타터 킷을 사다

스마트홈 구축의 입문격이라는 필립스 휴를 샀다.

몇 년 전에는 분명 스타터 킷이 삼십만원 정도 했던 것 같은데, 지금은 그 가격의 반도 안한다.

상자 사진

전구 세 개와 휴 브릿지가 들어있다. 저 브릿지를 통해서 각 전구를 조종한다.

상자 연 사진

hue 앱으로도 조종할 수 있고, 애플 홈으로도 조종할 수 있다. 시리에게 요청할 수도 있다.

앱 처음화면

앱애서는 간단한 루틴 설정이 가능한데,

앱 루틴 화면

내가 제일 원했던 기능인 기상 루틴을 설정했다. 유독 겨울에는 아침에 어두워서 알람을 들어도 일어나기 힘들다. 겨울을 미리 준비하기 위함이라고 스스로에게 구매를 핑계삼았다.

앱 기상 루틴 화면

알람 시간 삼십분 전부터 서서히 밝아지다가 알람이 지나고 10분 후에 자동으로 꺼지도록 세팅했다.

애플 홈킷과도 연동할 수 있는데, 휴 앱에서 설정하면 동기화하는 방식이다.

홈 앱 화면

  • “불 꺼줘”
  • “내방 불 켜줘”
  • “환하게 해줘”
  • “거실 조명 보라색으로 바꿔줘”

같은 명령을 시리에게 시킬 수도 있다.

시리 명령

전구는 세 갠데, 내 방에 소켓은 책상 스탠드 하나 뿐이다. 이제 스탠드를 사야한다.

책상

원래는 아침에 일어날 때 좀 더 기분 좋게 일어나고 싶어서, 일출을 시뮬레이션하는 필립스의 wake up light를 구매할 생각이었다. 하지만 wake up light는 매 번 자기 전에 알람을 켜줘야하는 불편함이 있어서 이걸 자동화할 방법을 찾다가 없길래 Hue로 눈을 돌렸다.