최근에 Markov Chain이라는 것을 듣게 됐다. 이게 사실은 확률론의 '확률 과정'이라는 곳에서 나오는 개념인데, 필자는 수학 찐따라 기본적으로 위키피디아에서 설명하는 정의는 이해하지 못한다.

첫 문장부터 확률 공간, 가측 집합, 가측 공간같은 소리를 하는데다, markov chain에 대해 잘 정리된 것 같은 글들은 수학적 배경이 너무 많이 필요해서 여기에 좀 정리해보려고 한다.

Markov Chain

Markov Chain은, Markov Property를 지닌 이산 시간 확률 과정(Discrete-time Stochastic Process)을 의미한다(라고 설명하고 있다). 무작정 정리하기 전에, 그냥 Markov Chain의 예를 직접 보도록 하자.

Markov Chain의 예와 이의 해석

위 그림의 s, c, r을 state(상태)라고 부르고, 각 state 사이에 이어진 유향 선분(화살표)에 대해 할당된 가중치들은 해당 상태 전이가 이루어질 수 있는 확률을 의미한다(확률론에서 항상 그러는지는 모르겠으나, Markov Chain에서는 확률을 표현할 때 0~100%에 대한 구간을 [0, 1]로 두어 대응시키고 있다). 위 그림의 경우, s에서 c가 될 확률이 0.3(30%)이고, c에서 r이 될 확률이 0.5(50%)같은 식으로 해석하면 된다. 조건부 확률을 떠올려서 P(s|c)=0.3, P(c|r)=0.5같이 봐도 되고. 그렇다면 위 markov chain을 기준으로 했을 때, 오늘 날씨가 맑음(s)이고, 내일 날씨가 비(r)이며 모레 날씨가 맑음(s)이 될 확률P(s|r) * P(r|s) = 0.1 * 0.4 = 0.04가 될 것이다.

다시 정리하면

Markov Chain은 그냥 어떤 state를 정의하고, 그 state들 각각에 대한 전이 확률을 정의해둔 채 이거 갖고 짝짝꿍 하는 거다. 다시 정리하면, 위처럼 sunny, cloudy, rainy같은 state(상태)가 존재하고, 각 state를 넘나드는 확률값이 존재해서 이게 chain처럼 구성되어 있고, 상태 전이 확률이 항상 현재 state에만 의존한다면(예를 들어 s → c라는 전이의 확률이 r에 의존하지 않는다면), 이를 Markov Chain이라고 부른다. 여기서 '상태 전이 확률이 항상 현재 state에만 의존하는 것'Markov Property를 나타낸다.

근데 뭐 sunny, cloudy, rainy 이런 것만 봐서 이게 도대체 얼마나 유의미한 것을 도출해낼 수 있을까 싶을텐데, real world에선 이런 곳에 쓰이고 있다.

  • Markov Chain에서, chain이 커지면(상태 전이 횟수가 늘어나면) 특정 state에 전이될 확률이 고정된 값으로 수렴한다고 한다. 웹 전체를 markov chain으로 두었을 때, '서핑이 오랜 시간 이루어진다'고 가정하면 특정 웹 페이지 X에 도달하는 것은 고정 확률이라고 볼 수 있다. Google의 Page Rank 알고리즘이 markov chain 이론을 기반으로 만들어졌다고 한다.
  • 안드로이드의 Google 키보드에는 'Google 키보드를 개선하기 위해 Google 어플리케이션에 입력하는 내용 공유'라는 옵션이 있다. 이게 켜져 있으면, 우리가 타이핑하는 단어들이 Google로 보내져서 Markov Chain에 반영되고, 이를 통해 Google은 단어 예측(Typing Word Prediction)을 수행한다.
  • 음성 인식, 추천 엔진 등 상태 전이 확률을 통해 뭔가 예측할 필요가 있는 곳들에서 Markov Chain을 자주 사용하고 있다고 한다.

Curryingn개의 인자를 가진 함수단일 인자를 가진 n개의 함수 체인으로 만드는 것이다. f(x, y, z)f(x), g(y), h(z)로 분리하는 것이다. 일반적으로 currying은 함수를 반환하는 함수 형태로 구현한다. 따라서 higher-order function이 직/간접적으로 가능한 언어가 currying을 구현하기 편하다. Currying은 특히 Haskell 입문서에서 자주 다뤄진다.

두 예제에서, 위 함수는 두 개의 인자를 한 번에 받아 그 합을 반환하고, 아래 함수는 한 개의 인자를 받아, 남은 인자 하나가 채워지면 합을 반환하는 새로운 함수를 반환한다. 따라서 두 함수는 각각 다르게 호출하게 된다.(sum(1, 3), sum(1)(3))

만약 인자 a에 대한 연산 비용이 비싸고, 인자 b에 채울 값이 지금 당장 없다면, 인자 b에 채울 값이 생길 때까지 시간이 남으므로 위의 커링은 유용할 수 있다. 그러나 위의 경우 인자 a에 대해 연산 비용이 없으므로(별도의 로직을 적용하지 않고 단지 가지고만 있을 뿐이므로), 연산 비용의 분산 면에서는 큰 의미가 없다.

따라서 currying은 함수의 특정 인자에 대해 연산 비용이 비싼 경우, 그 함수에 인자를 나누어 전달하여 함수의 로직을 점차적으로 수행할 때에 유용하다. 다만 내 경우에는 함수 자체가 계산 비용이 비싼 경우는 있었어도, 함수의 특정 인자에 한정되어 계산 비용이 비싼 경우는 없었기에 이런 상황이 그리 많지 않았고 따라서 메모이제이션으로 충분히 처리할 수 있는 경우가 더 많았다.

이에 더해 인자 a, b 간의 양방향 의존성이 있다면 currying을 적용하기 더 어렵다. 단지 인자 a가 인자 b에 단방향 의존한다면, 인자 b를 먼저 받아 로직을 일부 처리하고 인자 a가 채워질 때 클로저로 인자 b에 접근해 나머지 로직을 처리하면 된다. 그러나 애초에 두 인자가 같이 필요한 로직 뿐이라면 currying의 중간에 인자 여러 개를 받는 함수를 끼워두거나 애초에 a와 b 인자 모두가 채워졌을 때 로직을 수행하는 등의 방식으로 해결한다. 내 경우에는, currying을 Python의 ORM 라이브러리인 SQLAlchemy에서 read db와 write db에 대한 connection을 관리하기 위한 context manager 함수를 정의하는 데에 사용한 적이 있다.

위의 경우 read_session_factorywrite_session_factory는 session 함수의 engine_name에 각각 'read'와 'write'를 전달하여 반환받은 새로운 함수다. 그러나 engine_name 인자는 연산의 양에 큰 영향을 끼치지 않으므로, 그냥 함수로 분리해 내더라도 성능 상에 큰 문제가 생기지 않는다.

프론트엔드 API에서 자주 쓰이기도 하고, Go 내부 라이브러리에도 커링된 함수가 몇 가지 있다고 하지만 사실 이런 커링 기법을 완전히 적용할만한 경우를 찾지는 못했다. 하지만 이런 방식의 함수 작성은 함수형 프로그래밍과 클로저 등에 대한 개념을 강화하는 데에 도움을 주는 것은 맞는 것 같다.

'프로그래밍 > 언어론과 비슷한' 카테고리의 다른 글

Higher-order function  (0) 2019.02.06
Memoization  (0) 2018.09.11

Higher-order function은 Kotlin을 배우면서 마주친 개념이었다. 사실 higher-order function 자체는 이전에 내가 배웠던 다른 언어에서도 사용되던 개념이었는데, Kotlin 이전까지는 이에 대해 따로 설명을 하던 튜토리얼이 없었어서 Kotlin에서 처음으로 알게 되었다. Higher-order function은 고차함수, 또는 고계함수로 번역하며, 아래 두가지 중 하나 이상을 만족하는 함수를 의미한다.

  • 함수를 파라미터로 전달받는 함수
  • 함수를 리턴하는 함수

정의 자체가 그리 어렵지 않다. 그리고 함수가 값으로서 평가되기만 하면 사용할 수 있는 개념이라 일급 객체로서의 함수가 지원되는 언어는 higher-order function이 가능하다. 따라서 웬만한 프로그래밍 언어들에서 어렵지 않게 마주칠 수 있다. 아래는 Python의 예다.

위의 sum 함수는 덧셈 연산의 피연산자 a, b와 결과를 전달받을 함수인 callback을 인자로 받는다. 함수를 파라미터로 전달받으므로, 위 함수는 higher-order function이다.

위는 파이썬 데코레이터의 기본형이다. timeit 함수는 인자로 함수를 받고, wrapper 함수를 반환한다. 두 조건을 모두 만족하므로, 당연히 이도 higher-order function이다.

JavaScript도 함수가 값이기 때문에, higher-order function을 표현할 수 있다.

'프로그래밍 > 언어론과 비슷한' 카테고리의 다른 글

Currying  (0) 2019.02.08
Memoization  (0) 2018.09.11

Binary search tree(이진 탐색 트리)는 아래의 전제조건들이 존재하는 이진 트리다.

  • 모든 노드의 값은 유일하다.
  • 값들은 전순서가 있다(임의의 두 원소의 비교가 가능하다).
  • 어떤 노드의 왼쪽 서브트리그 노드보다 작은 값을 가진 노드들로 이루어져 있다.
  • 어떤 노드의 오른쪽 서브트리그 노드와 같거나 큰 값을 가진 노드들로 이루어져 있다.
  • 좌우 서브트리는 각각이 이진 탐색 트리여야 한다.

탐색

이진 탐색 트리에서 값 x를 가진 노드를 탐색하고자 할 때, 트리에 해당 값을 가진 노드가 존재하면 해당 노드를 반환하고, 존재하지 않으면 NULL을 반환한다고 가정해 보자.

  • 값이 root node와 일치하는 경우, root node를 반환한다.
  • x가 root node보다 작은 경우, 왼쪽 서브트리에서 재귀적으로 탐색한다.
  • x가 root node보다 큰 경우, 오른쪽 서브트리에서 재귀적으로 탐색한다.
  • 더이상 탐색할 서브트리가 없다면, NULL을 반환한다.

기본적인 탐색 알고리즘인 이진 탐색과 매우 유사하다.

삽입

  • 삽입을 하기 전, 탐색을 수행한다.
  • 탐색을 모두 마친 뒤, 삽입하려는 key와 일치하는 노드가 존재하면 삽입에 실패한다.
  • 일치하는 노드가 존재하지 않으면(위에 작성한 탐색 순서에 따라, NULL이 반환되면), 가장 마지막으로 탐색한 서브트리의 root node와 값을 비교한 뒤 작으면 왼쪽, 같거나 크면 오른쪽에 해당 노드를 삽입한다.

각 노드들은 값에 대해 unique해야 하기 때문에, 삽입 전 탐색 과정이 존재한다.

삭제

삭제도 삽입과 동일하게 먼저 탐색을 수행하고, 노드가 존재하는 경우에만 삭제를 수행한다. 삭제는 삭제 대상이 되는 노드의 자식 수에 따라 동작이 달라진다.

  • leaf node 삭제 : 해당 노드를 단순히 삭제한다. 가장 단순한 형태의 삭제.
  • child node가 하나인 노드 삭제 : 해당 노드를 삭제하고, child node를 그 자리에 대입한다.
  • child node가 두 개인 노드 삭제 : 해당 노드를 삭제하고, 왼쪽 서브트리에 있는 값 중 가장 큰 값, 또는 오른쪽 서브트리에 있는 값 중 가장 작은 값을 parent node와 연결시켜 준다.

삽입과 삭제가 일어나는 경우, 트리의 한 쪽이 비대해지는 경우가 생긴다. 이로 인해 높이 균형이 맞지 않는(unbalanced) 이진 탐색 트리는, 높이 균형이 맞는(balanced) 이진 탐색 트리에 비해 탐색에 대한 평균 노드 접근 수가 높다. 이는 시간복잡도에 영향을 미치므로, 삽입과 삭제가 일어나는 경우 트리를 높이 균형이 맞도록 변형시켜 트리의 높이를 작게 유지하는 self-balanced binary search tree(자가 균형 이진 탐색 트리)라는 ADT가 있다. 따라서 다음 몇 개의 글에서 self-balanaced binary search tree를 구현하기 위한 이론들에 대해 더 깊게 알아볼텐데, 따라서 Red-black tree처럼 조금 더 우리에게 익숙한 이름들이 보일 것이다.

'컴퓨터 기초 > 자료구조' 카테고리의 다른 글

Binary Tree의 순회  (0) 2019.02.01
n-ary Tree와 Complete, Perfect, Balanced, Skewed Tree  (0) 2019.01.30
Tree의 표현(representation)  (0) 2019.01.21
Tree  (0) 2019.01.18
Stack과 Queue  (0) 2019.01.16

맨날 뭐 결정만 하느라 지쳤으니, 이제 드디어 조금이라도 생산적인 작업을 해보자. API 스펙 설계와 문서화 방식 결정인데, 우리가 여태까지 의사결정한 결과물들이 이 작업의 기반이 되어 도움을 줄 것이다. 여기로 다시 끌어와 보면,

  • HTTP API 설계 원칙을 기반으로 API 스펙을 디자인하기로 했다.
  • JSON을 직렬화 포맷으로 결정했다.
  • Authorization 헤더로 인증 정보를 명시하기로 했다.
  • 인증 스키마에 JWT 기반의 Bearer를 사용하기로 했다.

이번 챕터는 글이 꽤 길어질 것 같아서, 두 편으로 나눠 진행한다.

도입 이유

'뭔가 하기 전에는 설계부터 해야지!'라는 의미 없는 이유라면 시도조차 안 했을텐데, 이 과정이 필요한 이유가 좀 있다.

  • 개발에 착수하기 전에 구조에 대해 고민할 시간이 생긴다.
  • 어차피 API 스펙을 프론트엔드에게 전달할 때는 문서로 정리해야 한다.
  • 실제로 로직을 작성하기 전에 스펙을 리뷰하는 단계가 생기고, 아직 코드를 작성하지 않았으니 변경 사항을 빠르게 반영할 수 있다.
  • 문서를 착실하게 작성해 두면, 커뮤니케이션으로 낭비하는 시간이 줄어든다.

작업 - API 스펙 설계

일반적인 프로젝트라면 기획서, 기본적인 와이어프레임, UI처럼 기능 명세를 살펴볼 수 있을만한 산출물이 나오고 난 상태여야 한다. 하지만 우리 프로젝트 팀 멤버는 가상 인물이므로 산출물같은 게 없으니 게시판 서비스에 필요할만한 기능들을 대충 떠올려서 설계해보도록 하자. HTTP API 설계 원칙에 따라 진행할텐데, 사실 이게 표준이 따로 있는 것이 아니고 조직마다 설계 원칙이 다르다. 우리는 Heroku 플랫폼 API를 개발한 조직이 정리한 디자인 가이드를 참고하겠다. 한국어 버전도 있으니 한 번쯤 읽어보도록 하자. 어떤 디자인 가이드를 사용할지에 대해서도 의사결정을 따로 했으면 어떨까 싶기도 했는데, 다들 뭐 비슷한 소리만 해서 깔끔히 정리된 거 하나 선택해서 참고해도 큰 문제 없을 것이라는 판단이다. 표준이 없다면 규칙은 조직이 납득 가능한 선에서 정하기 나름이니 말이다.

설계를 얼마나 구체적으로 할 지는 사람마다 다르지만, 나는 그냥 기능마다 엔드포인트와 함께 간단히 한두줄 정도로 정리해보려 한다. 필자는 원래 이런 것들을 notion이나 bear같은 메모 도구로 정리하곤 하는데, 글을 써야 하니 여기에 정리하도록 하겠다. 참고로 엔드포인트는 HTTP method + URI를 의미한다.

작업

정리한 API 스펙은 다음과 같다. {...}과 같은 URI는 path parameter를 뜻한다.

  • GET /check-duplicate/email/{email} : 이메일 중복체크 API
  • POST /signup : 회원가입 API. ID는 이메일을 사용하며, 이메일로 확인 링크가 전송된다. 이메일/비밀번호/닉네임을 받는다.
  • GET /verify : 회원가입 확인 링크 클릭 시 GET으로 호출하게 될 API. 이메일 전송 시 query string에 확인 링크의 ID를 담아 GET /verify?verify_id=am38gjbkeo같은 링크를 전달할 것이다. /verify/{verify_id}처럼 path parameter를 사용할 수도 있으나, GET 요청의 명시성을 늘리고 나중에 확인 링크에 더 많은 정보를 담게 될수도 있을테니 이렇게 했다.
  • GET /auth : 로그인 API. JWT 포맷으로 인코딩된 access token과 refresh token을 발급한다.
  • GET /refresh : access token refresh API. refresh token의 expire가 얼마 남지 않았다면 refresh token도 새로 만들어서 발급해 준다.
  • POST /board/categories : 카테고리 생성 API
  • GET /board/categories : 카테고리 목록 API
  • GET /board/categories/{category_id}/posts : 특정 카테고리의 게시글 목록 API
  • POST /board/categories/{category_id}/posts : 게시글 작성 API
  • PATCH /board/posts/{post_id} : 게시글 수정 API
  • DELETE /board/posts/{post_id} : 게시글 삭제 API
  • GET /board/posts/{post_id}/content : 게시글 내용 API
  • GET /board/posts/{post_id}/comments : 게시글의 댓글 목록 API
  • POST /board/posts/{post_id}/comments : 댓글 작성 API
  • DELETE /board/comments/{comment_id} : 댓글 삭제 API

모호함이 많다. 예를 들어, 게시글 목록 API에서는 페이지 개념이 들어갈 것인지, 페이지 당 게시글 갯수는 몇 개로 할 것인지, 회원가입 시 닉네임의 중복을 체크할 것인지, 비밀번호의 최소/최대 자릿수는 몇으로 할 것인지 등이다. 이런 건 기획 단에서 의사결정해 주어야 하는 부분이지만, 상황이 그렇지 않으므로 문서화 단계에서 적절히 이런 제약 사항들을 끼워넣을 것이다. API 처리 결과의 구분(이메일 중복 시 409, 게시글 삭제 권한이 없는 경우 403 등)도 문서화 단계에서 명시하겠다.

Tree의 순회는 트리에서 각각의 노드를 정확히 한 번만 방문하는 과정이다. Linked list나 array와 같은 선형 자료 구조는 논리적으로 한 가지의 순회 방법만이 존재하지만, 트리의 순회에는 많은 방법이 존재한다. 순회는 모든 n-ary tree에 대해 일반화될 수 있으나, 가장 자료가 많고 일반적인 binary tree의 순회에 대해서 알아보도록 하겠다.

순회

tree 내부의 tree를 sub-tree라고 부른다. 순회에는 이러한 sub-tree 개념이 등장한다.

전위(preorder) 순회(root-left-right)

  • 현재 node를 방문한다.
  • 왼쪽 sub-tree를 전위 순회한다.
  • 오른쪽 sub-tree를 전위 순회한다.

전위 우선 순회는 깊이 우선 순회(depth-first traversal)라고도 한다. 전위 표기법에서 자주 사용된다.

중위(inorder) 순회(left-root-right)

  • 왼쪽 sub-tree를 중위 순회한다.
  • 현재 node를 방문한다.
  • 오른쪽 sub-tree를 중위 순회한다.

후위(postorder) 순회(left-right-root)

  • 왼쪽 sub-tree를 후위 순회한다.
  • 오른쪽 sub-tree를 후위 순회한다.
  • 현재 node를 방문한다.

왼쪽 sub-tree 순회-오른쪽 sub-tree 순회라는 순서는 고정이고, root node를 언제 방문하는지에 따라 전위, 중위, 후위로 나뉜다. sub-tree들을 동일한 order로 순회하므로 재귀적인 특성이 강하고, 실제로 코드에 옮길 때도 재귀함수를 사용한다.

sub-tree를 순회하는 구조라 순회의 대상이 계속해서 바뀌기 때문에 조금 헷갈릴 수 있으나, 아래의 사진을 보면 이해에 조금 도움이 될 수도 있다.

레벨 순서(level-order) 순회

이는 sub-tree 개념이 들어가지 않고, 낮은 level부터 왼쪽에서 오른쪽 방향으로 순회한다. 너비 우선 순회(breadth-first traversal)라고도 한다.

위 tree를 전/중/후위, 레벨 순서 순회 각각의 방식으로 순회하면,

  • 전위 순회 : F-B-A-D-C-E-G-I-H
  • 중위 순회 : A-B-C-D-E-F-G-H-I
  • 후위 순회 : A-C-E-D-B-H-I-G-F
  • 레벨 순서 순회 : F-B-G-A-D-I-C-E-H

'컴퓨터 기초 > 자료구조' 카테고리의 다른 글

Binary Search Tree  (1) 2019.02.04
n-ary Tree와 Complete, Perfect, Balanced, Skewed Tree  (0) 2019.01.30
Tree의 표현(representation)  (0) 2019.01.21
Tree  (0) 2019.01.18
Stack과 Queue  (0) 2019.01.16

n-ary treedegree의 최대 수가 n인 tree를 의미한다. '모든 노드가 n개를 초과하는 자식 노드를 가지지 않는 트리'라고도 정의한다. k-way tree, k-ary tree, m-ary tree라고도 불린다. n이 2이면 binary tree가 되고, 3이면 ternary tree가 되는 식이다.

n-ary tree의 특징

높이가 h인 n-ary tree는 최대 n의 h제곱-1만큼의 노드를 가질 수 있다. 예를 들어, 높이가 3인 binary tree는 최대 7개의 노드를 가질 수 있다.

n-ary tree의 종류

complete n-ary tree

완전 트리라고 부른다. 마지막 level을 제외한 모든 level이 완전히 채워져 있으며, 마지막 level의 모든 노드는 가능한 한 가장 왼쪽에 있는 tree다.

perfect n-ary tree

포화 트리라고 부른다. leaf node를 제외한 모든 노드의 차수가 n이며, 모든 leaf node가 동일한 level을 갖는다. 어떤 트리가 perfect tree라면, complete tree이며 full tree다.(모든 노드의 차수가 n이므로)

balanced n-ary tree

균형 트리라고 부른다. 모든 leaf node의 level 차이가 최대 1인 트리를 말한다. 균형 트리는 트리의 주 목적인 '탐색'을 위한 AVL tree, red-black tree 와 같은 자료구조들의 기반이 된다.

skewed n-ary tree

사향 트리라고 부른다. root node를 제외한 노드들이 모두 parent node의 왼쪽에 있거나, 모두 parent node의 오른쪽에 있는 트리다.

'컴퓨터 기초 > 자료구조' 카테고리의 다른 글

Binary Search Tree  (1) 2019.02.04
Binary Tree의 순회  (0) 2019.02.01
Tree의 표현(representation)  (0) 2019.01.21
Tree  (0) 2019.01.18
Stack과 Queue  (0) 2019.01.16

이제 연산자라는 개념을 이야기한다. 연산자는 크게

  • 사칙연산을 위한 산술 연산자
  • 대소나 항등 평가 등 비교를 위한 관계 연산자
  • 불린 값을 피연산자로 두어 논리 연산을 하기 위한 논리 연산자
  • 하나의 피연산자에 대한 증가/감소를 위한 증감 연산자

로 나뉜다. 이번에는 이들 중 산술 연산자에 대해 알아볼 것이다.

산술 연산자

산술 연산자는 두 개의 값을 피연산자로 사용하고, 하나의 결과를 반환한다. 두 개의 항에 대해 적용되는 연산이므로, 이항 연산자라는 범주에 속한다. 사칙연산에 쓰이는 덧셈(+), 뺄셈(-), 곱셈(*), 나눗셈(/)표준 산술 연산자라고 부르고, 파이썬은 여기에 더해 문법적으로 몇가지 연산자를 더 지원하고 있다. 아래 코드는 표준 산술 연산자에 대한 내용이다.

수학에서 이야기하던 것처럼 n과 0을 곱한 결과는 0이고, 0을 n으로 나눈 결과는 0이다. n을 0으로 나누는 일은 불가능하므로 에러가 발생한다. 여기서 주목할 점은 덧셈, 뺄셈, 곱셈에 대해서 int와 float 간의 연산 결과는 float이며, 나눗셈의 경우엔 나누어 떨어지더라도 연산 결과가 float이라는 점이다. Python 2는 조금 다를 수 있다.(나눗셈 연산의 결과가 무조건 int) 나중에 가서 헛갈리는 경우가 간혹 있으니, 숙지하고 있는게 좋다.

표준 산술 연산자가 아닌 연산자들을 알아보도록 하자. 나머지(%), 거듭제곱(**), 소수점 버림 나눗셈(//)이 있다.

이것도 수학에서의 이야기와 동일하게, n의 0제곱은 1이다.

'프로그래밍 > Python' 카테고리의 다른 글

숫자 자료형과 리터럴  (0) 2019.01.25
파이썬의 타입 시스템과 빌트인 타입, 변수 선언  (0) 2019.01.23
주석  (0) 2019.01.07
Hello World, 세미콜론은 optional하다?  (0) 2019.01.03
개요와 설치  (0) 2019.01.01

이번에는 사용자 인증 방식을 결정하자. SNS나 우리가 만드려는 서비스처럼 계정 개념이 들어가고, 리소스가 특정 사용자에게 귀속되는 서비스라면 인증이 꼭 필요하다.

도입 이유

HTTP는 연결 지향 프로토콜인 TCP 기반임에도 불구하고, 대표적인 비연결 지향 프로토콜이다. 따라서 한 번의 요청-응답 사이클이 완료되면 연결을 종료하기 때문에, 동일한 클라이언트가 요청을 아무리 많이 하더라도 프로토콜은 이를 모두 독립적인 요청으로 인지한다. 이 때문에 클라이언트는 매 HTTP 요청마다 본인이 누구인지를 인지시킬 수 있는 인증 정보(credential)를 요청의 어딘가에 포함시켜야 하며, 서버 또한 클라이언트의 자원 접근을 허용하기 전에 이러한 인증 정보를 기반으로 인증 과정을 일차적으로 거쳐야 한다. 사용자 A가 작성한 게시글을, 다른 사용자가 마음대로 수정/삭제할 수 없게 만들어야 하기 때문이다.

의사결정 - 인증 정보의 위치

가장 먼저, 인증 정보를 HTTP 요청의 어디에서 관리할지를 결정하자.

배경과 요구사항

  • 모든 형태의 HTTP 요청에 다 사용 가능해야 한다. 예를 들어, GET 요청에서 사용할 수 없으면 안된다.
  • HTTP 표준에 맞춰지면 더 좋다.
  • 클라이언트 사이드에서, 쉽게 저장하고 HTTP 요청 단에서 쉽게 데이터를 실어줄 수 있어야 한다.

선택지

  • request body
  • 요청의 query parameter
  • Cookie 헤더
  • Authorization 헤더

의사결정

Authorizaton 헤더를 선택하겠다. 그 이유는,

  • 인증 데이터는 메타데이터 성격이 강하다. request body와 어울리지 않는다.
  • request body를 사용할 수 없는 메소드가 있다. GET, HEAD, DELETE, TRACE가 그렇다.
  • url의 ? 뒤에 붙는 query parameter는 고려해볼 만 하지만, 사용자 인증 하라고 Authorization 헤더가 표준화되어 있는데 굳이 query string을 써서 얻을 메리트가 없다.
  • Cookie는 헤더를 사용한다는 점에서 Authorization과 비교해볼 만 하다. 하지만 이것도 위에서 query parameter를 걸렀던 이유와 비슷하게, 인증이라는 맥락은 Authorization이 더 어울린다.
  • 내 주변만 그런 건지는 모르겠는데, 모바일 클라이언트들이 쿠키 기반의 인증을 싫어한다. 아마도 cookie store를 별도로 구현해야 되기 때문인 듯.

옛날에는 쿠키랑 세션을 이래저래 섞어서 썼던 기억이 있다. '자동 로그인'이라는 기능이 있어서, 이게 활성화되어 있으면 쿠키를 주고, 활성화되어 있지 않으면 세션을 줬다. 무슨 생각으로 그랬나 싶다. 코드

준비

MDN의 Authorization 헤더 문서를 읽어 보자.

의사결정 - 인증 스키마

이제 사용자가 로그인을 했을 때, 서버는 그 사용자를 나타내는 특별한 값을 만들어서 전달해 권한을 부여하고, 사용자는 나중에 Authorization 헤더로 그 인증 데이터를 보내준다는 것까지 결정이 되었다. '사용자를 나타내는 값'을 어떻게 만들어낼 지는 표준이 결정해 줄 것이다. Authorization 헤더에는 값에 대한 표준도 있으니까.

Authorization 헤더의 value는 <type> <credentials>처럼 생겨먹도록 하는 것이 표준이다. Bearer xmp98-cb35.potn6jz.zorj15gmb-이 한가지 예다. 인증 타입에 따라 credential을 만들어내는 방식이 정해져 있기 때문에 맘대로 할 수 있는 부분이 아니다. 표준을 따르지 않더라도 이유는 있어야 한다. 그러니 인증 스키마에 대한 의사결정을 진행하자.

배경과 요구사항

  • 표준을 따르지 않아도 괜찮지만, 충분한 이유와 대안이 있어야 한다.
  • 추후 확장 가능성을 위해 토큰 기반 인증 시스템이면 좋다. 모르는 단어라면 Velopert님의 토큰 기반 인증에 대한 소개를 읽어보자.
  • 충분히 암호화된 상태로 주고받을 수 있거나, 비밀번호와 같이 critical한 데이터를 값 내부에 포함시키지 않는 방식이어야 한다.

선택지

표준 상 Authorization 헤더의 값에는 RFC에 의해 표준화된 인증 스키마를 사용할 수 있게 되어 있다.

  • Basic
  • [비표준] OAuth 1.0a를 사용하는 Bearer
  • OAuth 2.0을 사용하는 Bearer
  • [비표준] JWT, 또는 JWT를 사용하는 Bearer
  • Digest
  • HOBA

의사결정

JWT을 사용하는 Bearer를 선택하겠다. 그 이유는,

  • Basic은 ID와 비밀번호를 base64 인코딩하는 방식이다. base64는 별도의 key 없이도 복호화가 가능한 인코딩이므로, 안전하지 않다.
  • OAuth 1.0a는 Bearer 인증 표준이 아니다. Bearer 스펙을 명시한 RFC 6750에는 큰 글씨로 'The OAuth 2.0 Authorization Framework'라고 되어 있기까지 하다.
  • Bearer에서 사용하는 OAuth 2.0 방식의 인증은 확장성이 매우 높다. 'Facebook 계정으로 로그인'과 같은 기능이 OAuth로 구현되었다. 되도록 이런 흐름에 낄 수 있다면 좋겠지만, OAuth 2.0은 자체 암호화를 지원하지 않기 때문에 HTTPS를 쓰는 것을 권고하고 있고, 돈이 들어가야 하는 부분이다. 인증 정책은 나중에 HTTPS 관련 비용 문제를 해결하고 나서 변경해도 괜찮을 것 같다는 판단이다. + 스펙 자체에서 명확하게 정의하지 않은 부분이 꽤 있어서 그만큼 고민이 깊어진다고 한다.
  • Bearer에 JWT를 사용하거나, JWT라는 타입을 쓰는 것도 표준이 아니다. 그러나 HTTPS 문제로 OAuth 2.0을 보류하게 되니, 대신 쓸 토큰 기반 인증 시스템으로 JWT가 가장 쓸만 하다.
  • JWT는 사용 사례가 많고, 거인의 어깨(잘 만들어진 라이브러리, 예제 등)가 잘 준비되어 있다.

그냥 아무 type이나 붙여서 값을 전달하거나, type 그거 명시해 봤자 딱히 쓸모 없는 것 같으니 type 안 붙여도 된다. DB 단에서 사용자와 매핑한 랜덤 문자열이나, 사용자 ID 자체가 인코딩된 문자열을 쓰는 등의 방식이다. 이번에 결정한 JWT도 그 연장선이다. 사실 경험 상 조직 내의 정책적으로 충분한 대안이 있다는 가정 하에, 인증에 관해서는 표준을 어겨도 그리 큰 문제는 없었던 것 같다.

준비

JSON Web Token 소개 및 구조라는 글을 읽고 구글링 이리저리 하면서 JWT를 조금 알아두자.

결론은 표준을 어기는 것으로 결정이 났다. JWT가 충분한 대안이 되어서 의사결정의 후회가 없거나 적었으면 좋겠다. 물론 HTTPS 문제를 극복하고 나서 OAuth 기반으로 인증 시스템을 변경하는 것도 이 컨텐츠에 포함시킬 계획이다.

지금까지

  • 버전 관리 시스템으로 Git을 사용하기 시작했다.
  • Git 웹호스팅 서비스로 GitHub를 사용하기 시작했다.
  • GitHub Issues와 Projects로 이슈 트래킹을 시작했다.
  • 개발 프로세스와 브랜칭 모델을 정립했다.
  • HTTP API 아키텍처 기반으로 API 스펙을 디자인하기로 했다.
  • JSON을 직렬화 포맷으로 결정했다.
  • Authorization 헤더로 인증 정보를 명시하기로 했다.
  • 인증 스키마에 JWT 기반의 Bearer를 사용하기로 했다.


숫자(number) 타입은 타입 개념의 가장 기본이 되는 자료형이다. 파이썬은 복소수(실수와 허수) 전체에 대해 빌트인 타입이 존재한다. Java와 같이 주류 프로그래밍 언어들 중 빌트인 숫자 타입에 허수부를 지원하지 않는 것들도 있으므로, 특별하다고 느껴질 수도 있다. 파이썬의 숫자 타입은 정수, 소수, 복소수 타입으로 나눌 수 있다.

정수(int)

수학에서 말하는 것처럼, 양의 정수, 음의 정수, 0을 다룬다. 별도의 최대값은 없다.

정수 리터럴

먼저, 음의 0(-0)은 없다. 당연한 논리지만, JavaScript에는 실제로 양의 0과 음의 0이 나뉘어 있기 때문에 언급하게 됐다.

파이썬의 정수 리터럴은 2진수, 8진수, 10진수, 16진수 표현이 가능하다. 10진수가 아닌 정수 리터럴을 정의하는 경우, 0 뒤진법에 해당하는 영어 명칭의 첫 글자(b(binary), o(oct), x(hex))를 명시한 후 값을 정의하면 된다. 그 예는 다음과 같다 : 0b110, 0o15, 0xF1F

가독성을 높이기 위해 리터럴에 언더스코어를 사용할 수 있다. 5000050,000으로 표기하는 것과 같은 맥락이다. leading, tailing, doubly 형태를 제외하고는 자유롭게 사용 가능하다. 예를 들어, _123, 123_, 12__3은 불가능하고, 12_3_456_7은 가능하다. 표기 상의 sugar일 뿐이므로, 실제로는 언더스코어가 제거된 상태로 다뤄진다.

소수(float)

소수부가 있는 수이다. 이전 글에서도 말했듯, 파이썬은 소수를 위한 타입으로 float만을 사용한다. 파이썬 버전마다 다르긴 하겠지만, 내가 사용하고 있는 Python 3.6.5에서는 소수 리터럴이 소수점 아래 15자리까지 표현할 수 있다.

소수 리터럴

e 표기법(지수 표현)을 사용할 수 있다. 그러나 음의 0이 없는 것과는 다르게 음의 0.0이 존재한다. 부동소수점을 사용하기 때문이다. 0이나 0.0의 지수 표현 결과는 0.0, -0이나 -0.0의 지수 표현 결과는 -0.0이 된다.

복소수(complex)

수학 기준으로는 복소수가 실수와 허수를 모두 포함하므로 complex 타입이 int나 float이나 모두 표현할 수 있다고 생각할 수 있으나, 여기서는 단지 허수부가 들어 있는 숫자를 다루기 위한 타입 수준으로만 생각하면 된다. 통계나 데이터 과학에 관련된 니즈가 많은 언어라 그런지는 몰라도, 빌트인으로 복소수 타입을 지원한다. 객체의 property로 실수부/허수부를, 절대값을 반환하는 빌트인 함수인 abs()에 넘기면 복소수의 크기를, conjugate 메소드를 통해 켤레복소수를 알아낼 수 있다.

복소수 리터럴

소수부+허수부 또는 허수부만으로 이루어지며, imaginary number의 약자 i 대신 j를 사용한다. 4번 라인부터는 파이썬의 복소수 타입 지원에 대한 추가적인 예제일 뿐이므로 프로퍼티, 함수, 메소드 개념을 이해하기 어렵다면 1, 2번 라인만 봐도 무방하다.

'프로그래밍 > Python' 카테고리의 다른 글

산술 연산자  (0) 2019.01.28
파이썬의 타입 시스템과 빌트인 타입, 변수 선언  (0) 2019.01.23
주석  (0) 2019.01.07
Hello World, 세미콜론은 optional하다?  (0) 2019.01.03
개요와 설치  (0) 2019.01.01

+ Recent posts