• 2010-09-24

    Google Spreadsheet API Note

    스프레드시트 리스트 읽어오기

    client = gdata.spreadsheets.client.SpreadsheetsClient()
    
    feed = client.get_spreadsheets()
    

    내부적으로 다음과 같은 URL 을 사용한다.

    http://spreadsheets.google.com/feeds/spreadsheets/private/full

    특정 스프레드 시트의 워크 시트 리스트 읽어오기

    feed = client.get_worksheets(spreadsheet_key)
    

    내부적으로 

    http://spreadsheets.google.com/feeds/worksheets/<키>/private/full

    특정 워크 시트를 리스트 기반으로 읽어오기

    이건 gdata.spreadsheets.client 에서 지원하지 않는다.

    url = "https://spreadsheets.google.com/feeds/list/%s/%s/private/full" % (spreadsheet,worksheet) 
    feed = client.get_feed(url, desired_class = gdata.spreadsheets.data.ListsFeed)
    

    이렇게 읽어온 feed.entry 에서 적당한 필드를 잘 읽어내야 한다. 내부적으로는 XML 이므로 다음과 같이 읽을 것. 잘 안읽히면 feed 를 그대로 출력해서 어떤 값이 있는지 보면 된다.

    • entry.id.text
    • entry.title.text
    • entry.content.text: ListsFeed 로 읽어왔을 때 row 의 값들이 문자열로 들어가 있다.
    • entry.get_elements(namespace=GSX_ELEMENT): 컬럼은 gsx: 태그로 들어가 있으므로, iterating 할 때에는 이걸 써야 한다.
    • entry.get_value(column name): 이미 이름을 알고 있을때..

    핵심은,

    • 구글 스프레드 시트의 모든 문서, 워크시트, row, cell 들은 유니크한 URL 을 가진다. 
    • 적당한 URL 로 요청하면 ATOM XML 등으로 리턴한다.
    • gd_client.get_feed 에 URL 을 넘기고 desired_class 를 잘 지정하면, 적당한 파이썬 객체로 래핑된다.
    • 어떤 XML 이 리턴되는지만 잘 파악하면 끗.
  • 2010-08-16

    create_deep_key

    parent 가 있는 키 속성을 bulkloader.yaml 을 통해 export/import 할 경우, create_foreign_key + key_id_or_name_as_string 로는 부족하다.

    • 부모 키 : AAABBBCCCDDD
    • 자식 키 : AAABBBCCCDDDEEEFFF

    따라서,  부모의 키값과 자식의 키 아이디 또는 이름을 별도의 컬럼으로 내보내고, 반대로 가져올 때에는 하나로 합쳐서 원래의 키값을 복구해야 한다.

    property_map:
      - property: __key__
      external_name: Activity
      import_transform:  transform.create_deep_key((‘Visit’, ‘Visit’), (‘VisitActivity’, ‘Activity’))
      export:
      - external_name: Visit
      export_transform: transformhelper.extract_deep_key(1)
      - external_name: Activity
      export_transform: transformhelper.extract_deep_key(2)
    

    이때 만약 키가 아니라 id/name 을 내보내고 싶다면, transform.key_id_or_name_as_string_n(N) 을 사용할 것.

  • 2010-08-12

    Google App Engine Tips

    http://boxcatstudio.files.wordpress.com/2010/08/20100413120411_38823.png

    구글 앱 엔진으로 개발하면서 학습한 내용을 정리합니다. 이론적으로 혹은 실제 서비스할 때 잘못되거나 틀린 부분이 있을 수 있음을 미리 알려 드립니다.

    개요

    검증 및 마이그레이션의 책임은 '프로그래머'가 진다.

    관계형 데이터베이스의 경우, 스키마 및 그 정합성을 검증할 책임이 데이터베이스 엔진에 있는 반면, 구글 데이터스토어는 분산 및 속도 향상을 위해, 스키마와 검증의 책임이 프로그램 쪽으로 넘어와 있다. 이게 무슨 뜻이냐 하면, 같은 모델(테이블)에 서로 다른 필드로 구성된 객체(Entity,열)가 들어갈 수 있다는 뜻이다. 따라서 데이터스토어는 임의의 데이터 집합을 쿼리해서 읽어왔을 때, 그 안에 어떤 데이터가 들어가 있을지 모르기 때문에, 마이그레이션 역시 프로그램이 책임지고 구현해야 한다. 새로 속성(Property,컬럼)을 추가하거나, 삭제하려면 해당 모델에 속한 모든 객체들을 찾아서 하나씩 변경해줘야 한다.

    예를 들어, 어떤 모델의 속성이 더이상 필요없어져서 삭제하려면, 무려 소스 코드에서 모델의 부모 클래스를 바꿔서 저장한 후, 서버에 터미널을 열고 수동으로 해당 속성들을 지우고 다시 돌아와서 코드를 롤백하는 등의 수고로움이 필요하다.

    이런 이유로, 가급적이면 앱엔진에서 서비스중에, 모델의 특정 속성 이름을 바꾸는 일은 피해야 한다.

    트랜잭션으로 묶을 객체들은 '생성' 시점에 부모를 지정해야 한다.

    관계형 데이터베이스에서는 서비스 이후에도 적당히 쿼리 최적화와 마이그레이션을 통해서 새로 트랜잭션을 추가할 수 있다. 그런데, 데이터스토어는 좀 다르다. 왜냐하면 일단 만들어진 객체의 키값은 절대 변경될 수 없는데, 트랜젹션으로 묶을 객체들은 생성 시점에 미리 같은 부모 객체를 지정해서, 같은 엔티티 그룹으로 만들어 줘야 하기 때문이다. 아마도 분산 트랜잭션을 피하기 위해서, 같은 그룹에 속한 객체들을 동일한(또는, 가급적 가까운?) 물리적인 노드에 두기 위함일테다.

    또한 동시에 여러 명이 같은 데이터를 변경하는 등의 충돌을 방지하려면, 주의깊게 부모 엔티티를 고르거나 아예 이런 일이 없도록 설계하는 것이 중요하다. 이런 제약으로 인해, 개발 시점에 어떤 트랜잭션이 필요할 것인지를 미리 예측하고, 코드 레벨에서 부모 객체를 미리 만들어둬야 한다. 구체적인 예를 들자면, 어떤 플레이어에 속한 모든 아이템, 도전과제, 퀘스트 같은 하위 객체들은 플레이어를 부모로 가지는 게 정신 건강에 좋다는 정도가 되겠다.

    이처럼 앱엔진 개발은 유지 보수의 책임이 데이터베이스가 아니라 프로그래머의 영역이 된다는 점을 항상 염두에 둬야 한다.

    키 이름(key_name), 아이디(id)의 용도를 잘 구분해야 한다.

    모든 객체는 시스템에서 유일한 키 값을 가진다는 점은 비교적 이해하기가 쉽다. 그런데, 키 이름(문자열)과 아이디(정수) 중 하나만을 가져야 한다는 점은 처음에 좀 헷갈렸다. 과연 어떤 객체에는 키 이름을 쓰고, 어디에는 아이디를 자동 생성해야 하는지가 할까? 처음엔, 해석을 잘못해서 키 이름이 시스템 전체에서 유일한 줄 알아서 많이 헤맸었는데, 알고보니 같은 모델(정확하게는 엔티티 그룹)안에서만 유일하면 된다는 것을 늦게서야 알게 되었다.

    결론부터 말하자면, 내가 맞게 쓰고 있는 건지는 모르지만, 개발 시점에 미리 생성해둬야 하는 정적인 객체들(아이템 정보, 맵 정보, 밸런싱 데이터 등 유일성을 개발 시점에 파악할 수 있는 객체들)에게는 유일한 키 이름을 할당해서, 향후 빠른 변경 및 접근을 할 수 있게 하고, 런타임에 생성하기 때문에 유일성을 미리 파악하기 힘든 다른 수많은 객체들(계정, 플레이어, 아이템 인스턴스, 도전 과제 인스턴스 등)은 자동으로 아이디를 할당받게 하는 게 좋다.

    그런데, 개발을 위해서 동일한 코드를 기반으로 서로 다른 app-id를 가진 구글 앱엔진 서버(Development - Test - Production)를 운영할 경우, 자동으로 만들어져서 아이디를 가진 객체들 - 특히나 ReferenceProperty 로 관계가 맺어진 넘들-을 다른 서버로 옮기는 것이 거의 불가능하다는 점에 유의해야 한다. 왜냐하면 객체의 키값에 이미 app-id 가 들어가 있기 때문에 CSV 등으로 백업해서 복원할 경우, 만약  기존에 동일한 id 를 가진 객체가 존재하면 그냥 덮어써버리기 때문이다. :(

    쿼리(datastore_v3.RunQuery)와 읽기(datastore_v3.Get)의 차이를 이해해야 한다.

    ReferenceProperty 를 이용하면, 1:N 의 관계를 표현할 수 있다. 아래의 코드처럼

    class Player(db.Model):
      pass
    class Item(db.Model):
      player = db.ReferenceProperty(Player,collection_name="items")
    

    과 같이 정의된 경우, player.items 라는 역참조가 자동적으로 만들어진다. 문제는 이게 코드를 읽을 때에는 왠지 캐싱이 될 것처럼 보이지만, 내부적으로 datastore_v3.RunQuery 라는 RPC가 호출되기 때문에 매번마다 다시 데이터스토어에서 읽어온다는 점이다. 무조건 appstat 을 활성화시켜 두고, 조금이라도 반응 속도가 느린 요청이 있으면 항상 확인하는 버릇을 길러야 한다.

    반대로 이미 키값을 알고 있는 item 의 소유자 이름을 읽어오려면, item.player.name 처럼만 하면 된다. 그런데 내부적으로 datastore_v3.Get 요청은 특정 필드만 읽어오는 기능을 지원하지 않기 때문에, 항상 모든 속성들이 다 읽어지게 된다. 더욱 무서운 점은 그냥 item.player 나 item.player.key() 라고만 해도 Get 요청이 이루어진다는 거다. 만약 참조하는 객체의 하위 속성이 아니라 오직 키값만 알고 있으면 되는 경우에는 model_name.reference_prop.get_value_for_datastore(entity) 를 이용하면, 코드는 더러워지지만 불필요한 요청을 피할 수 있다.

    특히 루프 안에서 참조키의 하위 속성을 읽는 짓은 가급적 피하는 것을 권장한다.

    for item in Item.all().filter():
      if item.player.hp == 0: # bad bad
      pass
    

    팁 & 트릭

    Compare-And-Set Memcache

    • 동시에 여러 요청에서 memcache에 있는 카운터 변수를 읽고 증가시킬 경우 같은 경쟁 조건을 위한 API
    • get() 이나 get_multi() 로 읽을 때 for_cas=True 파라미터를 지정하면, memcache client 내부에 각 키에 대한 timestamp 를 저장해둔다
    • 저장하려면 set() 이나 set_mutli() 대신 cas()/cas_multi() 를 사용하면, 읽어올때 저장해둔 timestamp와 비교해서 동일할 경우에만 저장된다. 다를 경우 실패하기 때문에 retry 루프를 이용해야 한다.

    see also Compare-And-Set in Memcache by Guido

    Pagenation

    using offset

    Query.fetch(limit, offset)을 이용하면 간단히 페이징 네비게이션을 구현할 수 있다.

    • item per page: K
    • total page: query.count() / K
    • current page: 1-based. from request
    • offset = (page-1) x K

    으로 데이터를 읽어올 수 있다. 문제는 offset 이라는게 실제로 데이터스토어에서 읽으면서 skip 하기 때문에 느리다는 점. 이를 위해서 cursor 가 도입되었다.

    using cursor

    result, end_cursor, more = ndb.Query.fetch_page(limit, start_cursor, end_cursor)
    

    를 이용하면 start_cursor ~ end_cursor 사이의 아이템 limit 개를 읽어올 수 있다. 또한 더 읽어올 데이터가 있는지, 다음 번에 읽을 커서는 무엇인지도 쉽게 리턴해준다. offset 과는 달리 커서는 한번에 해당 객체까지 찾아가므로 보다 빠르게 페이징을 구현할 수 있다.

    이때 커서는 urlsafe 문자열로 바꿔서 클라이언트로 보내서 다음번 요청때 사용하게끔 한다.

    문제는 특정 커서로부터 이전 몇 개 - 예를 들면 fetch_page(-N, cursor) 같은 건 아직 지원하지 않는다. 따라서 next - next 이동은 간편한 반면 특정 시점에서 prev - prev 이동은 매번마다 이전 쿼리의 시작 커서를 알고 있지 않는 한 불가능하다는 점이다.

    해결책이라면

    1. 이미 이전 페이지는 브라우징을 했다는 전제 하에, 이전 페이지 이동은 순수 자바스크립트로 구현한다.

    Taskqueue

    task name

    name 은 유니크해야 한다. 이미 큐잉 되어 있으면 Already Exist, 실행된 적이 있으면 TombstonedTaskError 가 발생한다. 즉, 이미 실행되고 사라져도 시스템은 그걸 기억하고 있으며, 오직 내부적으로 중복 큐잉 및 실행을 방지하기 위한 거다. (또는 나중에 취소하기 위함)

    queue.purge 를 실행해도 이름 자체는 남아있다. ㅎㅎㅎ

    중복 실행 방지

    1. 메모리 캐시 또는 DB에 LOCK 을 걸어두거나
    2. 커스텀 태스크 큐에 넣고 실행하되 아예 모두 날려버리는 방법이 있다. 커스텀 큐는 queue.yaml 에 정의되어야 한다. rate(몇 초마다 큐를 체크하는가?), bucket_size(한번에 몇 개의 태스크를 처리하는가?)만 주의하면 된다.

    appcfg.py 자동화

    예전엔 --passin 파라미터를 이용하면 패스워드를 커맨드라인에 지정할 수 있었는데 어떻게든 패스워드가 노출되기 때문에 보안적으로 위험이 있었다. 요즘에는 --oauth2 를 이용해서 비교적 안전하게 인증을 할 수 있게 되었다.

    참고 자료


    계속 추가할 예정입니다 :)

  • 2010-08-10

    jquery removeClass 를 이용해서 css sprite 교체하...

    css sprite 는 특정 DIV 에 CSS 스프라이트 클래스를 설정해서 배경 이미지를 보여주는 방식인데, 문제는 이전에 어떤 클래스 이름이 들어가 있는지 알 수 없다는 거다. 즉 정규식으로 클래스 이름을 검색해서 스프라이트 클래스만을 찾아서 지우고 새 스프라이트 클래스를 넣어야 한다.

    .removeClass(function(index,class_str){
      var class_list = class_str.split(' ');
      for ( var i = 0 ; i < class_list.length ; i ++ ) {
      var cls = class_list[i];
      if ( cls.match(/unit-.+?_(L|R)/) ) {
      return cls;
      }
      }
    }
    

    주의할 점은, 이 콜백 함수가 $.each 처럼 기존 클래스 명 하나하나마다 호출되는게 아니라, 그냥 공백으로 구분된 클래스 이름 문자열을 파라미터로 넘겨서 한번만 실행된다는 점이다. (index 는 왜 있는 거지;;;)