Semantics and optimizations

Twitter에서 pjax의 시멘틱에 대한 얘기가 나왔다. pjax의 기본적인 아이디어는 이렇다.

The idea is you can’t tell the difference between pjax page loads and normal page loads. On complicated sites, browsing just “feels faster.”

그래서 나는 지금까지 pjax의 용도가 단순히 페이지 로딩의 체감 속도를 투명하게 향상시키는 것이라고 생각했다. 실제로 pjax는 XHR을 이용해 비동기적으로 요청했다가 대기 시간이 길어지면 그냥 일반적인 하이퍼링크 클릭처럼 페이지 이동을 시켜버린다. pjax의 덕을 제대로 본 경우에는 페이지의 다른 부분은 변하지 않고, 실제로 내용이 바뀐 부분만 갱신된다.

내부 구현은 HTML 5에서 추가된 history.pushState() 메서드를 이용해 브라우저 히스토리를 조작하는 식이다(이때 주소창의 내용도 갱신한다). 브라우저가 history.pushState() API를 지원하지 않는 경우에는 작동하지 않는다. 그럼 그럴 때는 어떻게 할까?

When pjax is not supported, $('a').pjax() calls will do nothing (aka links work normally) and $.pjax({url:url}) calls will redirect to the given URL.

그냥 일반 링크처럼 작동한다. 그럼 pjax의 특징을 정리해보자.

  1. 사용자가 보기에는 일반 링크처럼 작동하지만 체감 속도가 조금 더 빠르다.
  2. 여의치 않으면 좀 느려도 원래 했던대로 폴백한다.

이걸 좀더 일반화하면 다음과 같이 정리할 수 있다.

  1. 시멘틱(semantic)을 유지하며 성능을 개선한다.
  2. 가정(assumptions)에 부합하지 않을 경우의 general counterpart도 마련하다.

이건 요약하자면, 최적화(optimization)의 정의나 다름없다. 그래서 내가 보기에 pjax는 일반 HTML 링크 이상의 기능이 아닌, 링크의 단순 최적화다. history.pushState() API는 투명하게(즉, 사용자가 보기에는 일반 링크와 똑같은 시멘틱으로 여겨지게) 작동하도록 만들기 위한 구현 디테일이라고 생각한다. 만약 다른 방식으로도 HTML 링크의 시멘틱을 유지하면서 체감 속도를 개선할 수 있다면 pjax는 그 방식을 써서 구현됐을지도 모른다.

그러나 Twitter에서 이야기해보니 pjax는 history.pushState()를 통해 구현되었기 때문에 상태를 언제나 유지하는 것이 바람직한데, 여의치 않으면 카운터파트로 폴백하는 동작 때문에 문제가 될 때가 있다고 생각하시는 분들도 계신다. 이를테면 음악 재생을 하는 페이지에서 pjax를 쓴다는 것은 페이지 상태를 유지하기 위해서인데 의도치 않게 카운터파트가 작동해서 음악이 중단되면 어떡하냐는 이야기다. 그렇게 생각할 수도 있겠구나 싶다. 확실히 pjax는 이름에서부터 구현 디테일에 대한 인상을 심어준다.

어떤 기술이 최적화냐 부가적인 기능이냐를 판단하는 여부는, 그 기술의 동기, 즉 용도를 어떻게 보느냐에 따라 달라진다. 설명을 위해 또 다른 예를 들어보겠다.

예전에 LangDev에서 강성훈 씨가 이런 주장을 한 적이 있다: “TCO(tail call optimization)는 TCO가 적용되지 않았을 때의 카운터파트 시멘틱과 직교적(orthogonal)이지 않기 때문에 컴파일러가 암시적으로 적용하면 안된다.” 워낙 파격적이고 생각도 해본 적 없는 주장이라 채널의 다른 사람과 말이 많았다. 강성훈 씨의 생각은 이렇게 요약할 수 있다. TCO는 TCO가 적용되지 않았을 때 일어나는 스택 오버플로우(stack overflow)에 대한 시멘틱을 훼손하기 때문에 암시적으로 일어날 경우 디버깅이나 유지 보수에 혼란을 만든다. 이를테면 TCO의 존재를 모르는 언어 사용자가 TCO가 우연히 잘 적용되고 있는 재귀 함수를 작성하여 사용하고 있었는데, 이 함수는 원래대로라면 스택 오버플로우를 발생시킬 수 있는 코드였다. 만약 이 함수를 같은 언어의 다른 구현체(컴파일러)에서 컴파일을 하게 되거나, 함수의 내용을 약간 바꿔서 TCO의 암시적 적용이 무효화되고 카운터파트로 폴백되어 스택 오버플로우가 나기 시작했다고 해보자. 사용자는 이 버그를 어떻게 생각해야 하는가? 어째서 이러한 문제가 갑자기 일어나기 시작했는지 추리할 방법이 있을까?

내 생각에는 이 주장 역시 프로그래밍 언어가 명세에서 함수 호출의 시멘틱을 어떻게 정의하느냐에 따라 맞는 얘기가 되기도 하고, 틀린 얘기가 되기가 될 수도 있다고 본다. 만약 해당 언어에서 함수 호출이 스택과 같은 것을 언급하지 않고 컴파일러의 구현 디테일로 치부한다거나 (이렇게 되면 일반 함수 호출에서 C 스택을 사용하는 것은 컴파일러의 구현 문제가 된다) 함수 호출을 컨티뉴에이션(continuation)을 이용해 정의하면 암시적인 TCO는 말 그대로 최적화일 뿐 시멘틱을 훼손한다고 볼 수는 없게 된다. 하지만 스택과 스택이 다 찼을 때의 런타임 오류 핸들링을 언어 기능으로 본다면 TCO는 특정 케이스에 언어 명세에 의거해서는 틀린 예측을 낳으므로 시멘틱을 훼손하는 버그로까지 여길 수 있다.

다시 pjax 얘기로 돌아가자면, pjax는 처음 만들어졌을 때 그 동기와 용도에 대해 딱히 천명한 적은 없다. 즉, pjax가 애초에 일반 링크의 체감 속도를 최적화한 것인지, 아니면 페이지의 상태를 유지하면서 일반 링크의 기능을 가져오려고 한건지 알 수는 없다. pjax의 용도를 전자로 본다면, pjax가 자동으로 카운터파트로 폴백을 해서 상태가 초기화되는 것은 의도적으로 무시한 부수 효과라고 볼 수 있을 뿐만 아니라, 오히려 상태가 유지되는 것이 history.pushState() API를 써서 생긴 부수 효과라고 봐야 한다. 하지만 pjax의 용도가 후자라고 생각한다면 pjax가 여의치 않다고 상태를 때때로 잃어버리는 현상은 부수 효과도 뭣도 아니고 그냥 버그가 된다.

하지만 나는 pjax가 때때로 상태를 잃어버리는 것이나 TCO가 때때로 스택 오버플로우를 내지 않는 것이 버그라고 여겨지지는 않는다.

3 days ago

(github|bitbucket)-distutils

StyleShare 만들 때 배포를 신속하게 만들고 싶었지만 실제로는 그렇게 빠르게 되지 않았다. 패키징을 하는데 시간이 가장 많이 들었던 부분은 바로 의존성 해결. 정확히는 PyPI가 그렇게 빠릿하게 응답하지 않아서 모든 패키지를 다운로드하는데 몇분씩 걸리다보니 당장 몇초 안에 배포를 하고 싶은 상황에서 매우 답답했던 기억이 난다. 애초에 pipeasy_install전체 의존성 그래프를 선형화시켜서 하나씩 받는 게 문제라고 보지만, 어쨌든 그랬었다. (해결은 pip에서 제공하는 캐시 옵션을 여러가지 켜서 어느 정도 했지만 여전히 인덱스 속도가 느려서 그 이상으로 시간을 줄이지는 못했다.)

저걸 해결하기 위해서는 아니지만, 그 이후에 패키지 메타데이터를 PyPI에 올리더라도 패키지 파일 자체는 CDN이 물려있는 곳에서 호스팅하는 게 낫겠다는 생각을 계속 해오고 있었는데, 마침 Bitbucket이나 GitHub은 저장소 별로 다운로드 서비스를 제공하고 있고 둘 다 Amazon CloudFront (AWS의 저가형 CDN) 서비스의 가호를 받고 있어서 속도가 서울에서도 꽤나 괜찮게 나왔다. 그래서 패키지 릴리즈할 때 아예 그쪽에 올리게 하는 걸 만들어보자, 하는 아이디어로 최근 두 개의 패키지를 만들어서 올렸다.

참고로 둘 다 PyPI 패키지 명이고, 자기가 배포하려는 패키지의 setup.py 파일 안에 다음과 같이 setup_requires를 명시하면 쓸 수 있다.

setup(
    name='YourPackageName',
    version='1.2.3',
    ...,
    setup_requires=['github-distutils >= 0.1.0']
)

실제로 이걸 써서 릴리즈하면 PyPI 페이지에는 이렇게 올라가고:

PyPI TypeQuery page

저장소 다운로드 페이지에도 이렇게 올라간다:

Bitbucket TypeQuery downloads

4 days ago
HTTP Link header

10년도 더 됐지만, HTTP 표준으로 제안된 헤더 중에 Link라는 게 있다. 링크에서 짐작할 수 있다시피 HTML <link> 태그와 같은 용도로서, Content-Typetext/html이 아닐 때도 <link> 태그에 담기는 내용들을 메타데이터로 전송할 수 있게 해준다. 예를 들어 다음과 같은 식이다.

Link: <http://dahlia.kr/>; rel=canonical

이걸 실제로 쓰는 곳이 있을까 싶은데, GitHub API가 이걸 페이지네이션 용도로 쓰고 있다. 확실히 페이지네이션을 저렇게 하면 응답이 좀더 깔끔해진다. 무슨 뜻이냐면, 예를 들어 어떤 긴 목록을 반환하는 리소스가 있다고 했을 때,

HTTP/1.1 200 OK
Content-Type: application/json

[...]

형태로 주는 쪽이 깔끔하겠지만, 페이지네이션 정보가 필요하면 다음과 같이 목록을 무언가로 더 감싸야만 한다.

HTTP/1.1 200 OK
Content-Type: application/json

{'objects': [...], 'nextUrl': 'http://example.com/objects/?from=...'}

이런 경우에 GitHub처럼 Link 헤더를 쓰면 되겠다.

HTTP/1.1 200 OK
Link: <http://example.com/objects/?from=...>; rel=next
Content-Type: application/json

[...]

게다가 rel 속성에는 원래 nextprev 같은 것들이 들어갈 수 있다. 그러고 보니 Opera 같은 브라우저는 실제로 rel 속성이 nextprev인 경우에 네이베이션 버튼들이 해당 시멘틱을 따라서 작동했던 걸로 기억한다.

덧. 상관 없는 얘기지만 HTTP API를 만들 때 응답이 JSON이고 인증이 필요한 경우, 응답이 리스트이면 보안상 문제가 될 수 있다. 간단히 설명하면 전혀 다른 사이트에서 <script> 태그로 해당 리소스를 불러오는데, 그 직전에 해당 페이지에서 Array 함수를 덮어씌우면 리스트 리터럴이 해당 생성자를 호출하게 되어 있기 때문에 JSONP 쓰는 것 마냥 값을 받아올 수 있다.

This was posted 5 days ago. It has 1 notes.
docopt

doctest와 비슷한 아이디어인데, doctest도 그렇고 암시적인 것보다 명시적인 것을 선호하는 Python 철학에는 별로 좋은 아이디어로 여겨지지 않는 모양. 하여간 발상 자체는 참신한 것 같고, 간단한 스크립트 만들 때는 괜찮지 않을까 싶다.

이런 종류의 아이디어로는 Go 언어에서 쓰는 날짜 포매팅 방식도 있다. 예를 들어 strftime(3)으로는 %Y-%m-%d %H:%M:%S라고 써야 하는 것을 자연스럽게 2006-01-02 15:04:05라고 쓰면 그것으로부터 포맷을 뽑아내자는 아이디어. 그러니까, January, Jan, 01, 1 등은 무조건 월을 뜻하고, 15, 03, 3은 무조건 시간을 뜻하는 식.

This was posted 5 days ago. It has 0 notes.

좋은 프로그래머는 연장 탓을 아주 많이 해야한다

다른 직종은 모르겠지만 프로그래머는 연장 탓을 해야 한다. 프로그래머 자신이 연장을 만드는 사람들이라는 생각해보면 답은 매우 명확하다. 바이올린 연주자가 바이올린을 만드는 것은 아니고, 최고의 검객이 최고의 대장장이는 아니지만, 프로그래머는 거의 유일하게 자기가 쓸 연장을 스스로 만드는 직종이다. Linus Torvalds가 BitKeeper를 대체하기 위해 git을 만들었다는 것을 생각해보자. 좋은 연장을 만들지 않거나 찾아서 쓰지 못하면 좋은 프로그래머 아닌 건 확실하다.

1 week ago