ROM (Resource-Objective Mapping) 디자인 논의

11 views
Skip to first unread message

Hong MinHee

unread,
Mar 10, 2009, 11:20:27 PM3/10/09
to vlastic-d...@googlegroups.com
김우승 님이 몇가지 내용 때문에 개인적으로 메일을 보내신 건데, 문제된다고 판단되는 부분을 뺀 다음 메일링리스트로 올립니다.
공개적으로 논의할만한 사항이라고 생각해서요. 일단 우승 님이 이야기하신 이슈가 있고, 그에 더해서 제가 최근에 생각한 이슈가
하나 더 있습니다. 먼저 전자에 대한 제 생각을 이야기하겠습니다.

리소스 객체 매핑의 구현에 있어서, 현재 가장 이슈가 되는 사항이 네임스페이스 문제라는 것은 분명합니다. 저도 이 부분에 대해
여러가지 고민을 하고 있습니다. ROM의 의도는 애초에 웹 애플리케이션을 REST에서 말하는 HTTP 리소스 관점으로 구성하는
동시에, 그것을 자동적으로 해당 언어의 객체 모델로 봤을 때도 자연스럽게 다룰 수 있는 인터페이스를 만들어내는 것이겠죠.
(그리고 이것은 ROM의 의도이긴 하지만, 그 의도 자체가 “왜” 필요한지에 대해서는 설명하지 못하는 말이기도 합니다. 이에
대해서는 아래에서 이야기하겠습니다.) 우리가 작성하는 Vlastic 웹 프레임워크는 Python 3.0이라는 언어로 작성되고
있고, Python은 그 특유의 객체 모델을 지닙니다. 그리고 거기서 풍기는 냄새도 제법 독특한 편입니다.

(항상 그런 것은 아니지만) Python 객체는 대개 접근자 따위 없이 내부 애트리뷰트를 직접적으로 접근 가능한 plain
object의 모습을 하고 있습니다. C++/Java 등에서 접근자를 쓰는 이유는, 특정 프로퍼티에 접근하는 행위에 추후 다른
행위가 추가될지도 모르기 때문이겠죠. 내부 애트리뷰트를 공용(public)으로 두고 디자인하면, 나중에 클라이언트 코드에서
해당 애트리뷰트에 접근하는 부분을 모두 수정해야 합니다. 특히 라이브러리 같은 경우라면 클라이언트 코드에 대한 제어권이 전혀
없기 때문에 이런 문제는 더욱 심각해집니다. 하지만 C#이나 Python 같은 경우 어차피 프로퍼티라는 좋은 기능이 있으니
처음부터 그런 걱정을 하지 않아도 되기 때문에, 언어들 사이에 이런 객체 디자인에 대한 패턴이 다르게 형성되지 않았을까 하는
생각을 합니다.

그래서 다른 언어로 ROM을 구현했다면 각 리소스의 자식 리소스(그러니까, /child와 같은 형태로 접근되는 것 말입니다)의
접근을 객체로 표현할 때,

using vlastic;

class person: rom::resource<person> {
picture_list get_pictures() const { ... }
post_list get_posts() const { ... }
};

이렇게 작성하는 편이 (아마도) 자연스러웠을 것이고, HTTP 메서드(GET, POST, PUT, DELETE 등…)에 대해
rom::resource::get(), rom::resource::post() 같은 멤버 함수를 정의하기도 더 쉽지 않았을까
하는 생각을 합니다. 자식 리소스의 접근에 대해서는 멤버 함수에 get_이라는 접두사가 항상 붙게 되어 네임스페이스 문제가
사라졌겠죠.

(HTTP의 GET, POST, PUT, DELETE 같은 메서드와 OOP에서의 객체 메서드를 구분하기 위해 이하에서 후자는
멤버 함수라고만 말하겠습니다.)

from vlastic import rom

class Person(rom.Resource):
@property
def pictures(self):
...

@property
def posts(self):
...

제가 생각한 것은 이런 식으로 객체의 애트리뷰트/프로퍼티를 자식 리소스의 접근으로 사용하는 거였는데, 문제는 Python
객체 모델은 Lisp-1 vs Lisp-2 debate[1]에서 말하는 Lisp-1에 해당하기 때문에, 즉, 객체의 멤버
함수도 사실은 해당 이름의 애트리뷰트로 함수 객체가 들어있을 뿐이므로 resource.get(), resource.post()
같은 식으로 HTTP 메서드를 위한 별도 멤버 함수를 만들 경우 /get, /post에 해당하는 자식 리소스를 만들 수가
없게 된다는 점입니다. 명백하지만 어려운 말을 쓰자면, 저희가 필요한 셀은 두 구역(HTTP 메서드, 자식 리소스 이름)인데
맵핑할 Python 객체에는, 지금까지는, 일단 셀이 하나(애트리뷰트)인 것처럼 보이죠.

그런 면에서 우승 님이 제안하신 방법은 이 문제를 꽤나 우아하게 해결했다고 생각합니다. 첨자 연산자는 확실히 또다른
네임스페이스 셀이죠. 게다가 get/set/del에 대한 확실한 통제권도 우리에게 있고요. 그렇지만 Python 객체와
리소스를 자연스럽게 맵핑한다는 점에서 “자연스러움”은 많이 떨어진다고 생각합니다. 그래서 저는 어떻게든 애트리뷰트를 사용하는
방향이 맞을 것 같아요.

문제는 리소스에 대한 HTTP 메서드들을 어떻게 연결하냐인데, 이게 구현 상에 크게 문제가 된다면 그냥 inspect 모듈을
써서 프레임을 해킹하는 게 나을 수도 있다는 생각이 듭니다. 클라이언트 코드에서는 쓰기 편하니까요.

애트리뷰트를 사용할 경우에 또 다른 문제가 있는데, if나 for와 같은 Python 예약어를 URL로 노출할 수 없다는
점입니다. 정확히 말하면 노출할 수 있습니다. __getattr__이나 __getattribute__를 쓰고, 대입할 때는
setattr 따위를 쓰면 되겠죠. 하지만 이런 경우에는 if를 when 같은 단어로 바꾸는 게 낫다고 봅니다.

if가 꼭 if여야하는 경우가 있는데, 태그라거나 사용자 아이디일 수도 있죠. 후자에서는 Python 예약어는 사용 못하게
한다거나 중복된다는 식으로 에러를 내서 막을 수 있는데 태그는 그러면 안되겠죠.

/tags/if

이걸 맵핑하는 방법은 어떤 것이 있을까요? 제가 처음에 생각했던 해결 방식은 꽤 지저분합니다. 자연스러운 객체로 보이지도 않구요.

from vlastic.rom import Resource

class Home(Resource):
def __init__(self):
self.tags = TagList()

class TagList(Resource):
def __getattribute__(self, tag):
return Tag(tag)

class Tag(Resource):
def __init__(self, name):
self.name = name

...

main = Home()

/tags/for라는 URL로 접근하게 되면 getattr(main.tags, "for")를 시도한다는 거죠. 내부적으로는
어차피 getattr만을 사용하니 사실은 getattr(getattr(main, "tags"), "for")가 맞겠죠. 참고로
__getattr__과 달리 __getattribute__는 해당 객체에 존재하는 애트리뷰트를 무시하고 가장 먼저 호출됩니다.

제가 문제가 된다고 생각하는 것은 main.tags는 TagList 인스턴스이고, TagList는 일종의 문자열을 키로 가지는
유사 딕셔너리로 보는 것이 맞은데, 어느 나라 Python 딕셔너리가 dict.key 형식을 사용하냐는 거죠. 전혀 자연스럽지
않죠. dict["key"]가 훨씬 자연스럽습니다. 애트리뷰트와 달리 첨자 연산자는 그 안에 어떤 것이 들어갈지는 런타임에
결정된다는 코드의 의도를 충분히 드러냅니다. 객체 모양만 봤을 때도 훨씬 자연스럽고요.

결국 비슷한 결론이긴 한데, 제 생각에는 DictionaryResource라는 인터페이스 클래스를 제공해서, 그것을
서브클래싱할 때는 __getitem__ 멤버 함수를 구현하도록 하는 게 좋을 것 같습니다. 이걸 상속 받으면 자식 리소스를
찾을 때 애트리뷰트는 전혀 뒤지지 않고, 오직 첨자 연산자에서만 찾게 됩니다. 태그 이름이나 사용자 이름을 주소로 사용하는
경우 훨씬 자연스러울 것 같습니다.

다른 분들 생각은 어떤지 알고 싶네요.

그리고 또 하나, (글이 길어지지만,) 제게 떠오른 이슈가 있습니다. 애초에 제가 리소스는 Python에서 일반적인 객체로도
사용할 수 있어야 한다고 생각했던 것은 테스트 코드를 작성하기 쉽게 하기 위해서였습니다. 예를 들어,
/tags/abc/authors?page=3를 테스트하는데,

authors = AuthorList()
req = http.Request("GET", "/tags/abc/authors?page=3")
req.back_component = ["tags", "abc"]
result = authors(req)
assert "<strong>abc</strong>" in result
assert "Total authors: <strong>37</strong>" in result

이렇게 하기 보다는,

result = main.tags["abc"].authors(page=3)
assert "abc" == result["tag_name"]
assert 37 == result["total_authors"]

이쪽이 훨씬 간결하고, 편하고, 수정이 용이하지 않겠습니까. 그래서 여러가지를 생각하고 있었는데, 그 중에 하나가

@resource
def authors(self, page=1):
...
return {"tag_name": self.tag.name, "total_authors": ...}


이런 식으로, 쿼리 문자열의 각 키들을 Python 키워드 인자로 맵핑해주는 기능입니다. 그런데 곰곰히 생각해보니 리소스
객체는 request를 받고 response를 내주는 함수 객체여야 하는데 키워드 인자를 받아서 dict를 내주는게 가능하냐는
겁니다. 아직 해결된 것은 아닌데 키워드 없이 request 객체 하나만 전달될 경우에는 resource 함수로 작동하고,
키워드 인자가 들어오거나 인자가 없으면 일반 메서드처럼 작동하게 오버로딩되게 하면 어떨까 하는 아이디어가 있긴 합니다. 더
좋은 방법이 있을 것 같은데, 생각이 나지 않네요.

[1] http://www.nhplace.com/kent/Papers/Technical-Issues.html

2009년 3월 2일 (월) 오후 7:38, 김우승 <lohas...@gmail.com>님의 말:
> http://hg.ruree.net/~dahlia/vlastic 를 보시면 홍민희씨가
> 대대적으로 수술하신 rom이 있습니다.
> 어제 홍민희씨와 작업을 하면서 많은 것을 느꼈고, 다른 대안도 이것 저것 생각해보았습니다.
>
> 그리고 오늘 오전에 그 느낌들을 정리해서 내린 제 결론은
> 생각만큼 객체구조가 Request 를 처리하는 Resource 구조에 적합하지 않다는 점입니다.
>
> 가장 중요하면서 계속해서 고민해온 점이 이름공간 충돌문제라는 점은 다들 익히 아실 겁니다.
> 이 문제의 발생은 결국 객체의 이름공간이 하위객체들과 messeage 처리자(즉 method)의 이름공간이 분리되어 있지 않다는 점에서
> 시작합니다.
> 알다시미 HTTP message는 이를 분명히 분리하고 있지요.
> GET /BBS/QnA 이것은 /BBS/QnA/GET 이 되지는 못한다는 것입니다. 만일 이것이 가능하다면
> BBS.QnA.GET 형태를 표현할 수도 있겠지만 알다시피 QnA가 하위객체를 가진다면 그 하위객체의 이름이 GET이 되는 것을
> HTTP message에서는 막지를 않지요. 더 큰 문제는 중간 어느 path component에서도 HTTP METHOD의 이름을 쓸
> 수 없다는 것입니다.
> 실용적으로 이미 널리 쓰이는 /Blog/post/1 의 의미하는 바는 blog의 post 모음중 첫번째를 찾자는 것인데
> Blog.post.1로 객체치환형태가 되면서 중간 POST를 처리하는 message 처리자인지 아니면 POST를 말하는 중간 객체인지 알
> 수 없게 됩니다.
>
> 또 한가지는 programming 언어는 풍부한 구문을 위하여 많은 참조자의 이름에 많은 제약이 따른 다는 것입니다.
> 당연히 keywords 즉 if, else, elif, import 형태는 객체참조이름에 쓸 수 없습니다. 그리고 상수형태도 쓸 수
> 없지요.
> /Blog/tag/if 가 의미하는 바는 if라는 tag가 붙은 모든 post 들일겁니다. 하지만 Blog.tag.if 는 Python이
> 받아들일 수 있는 문법이 아니지요.
> Blog.post.1은 단지 METHOD와 이름 충돌만이 아니라 1이라는 상수가 객체참조이름으로 쓸 수 없다는 점도 문제가 됩니다.
> 거꾸로 이야기하자면 resource에는 상수가 없기 때문에 resource 이름에 제약이 가해지지 않았기 때문이지요. :-(
>
> 객체구조가 resource 구조를 표현 못하는 것과 반대로 resource로 인하여 객체가 갖는 장점을 활용하지 못하는 경우도 있습니다.
> 같은 class 인 객체들이 서로 다른 객체임을 인지하게 할 수 있는 member 들을 사용하기가 곤란해집니다.
> 즉 OnlineDocument 라는 class가 있다고 합시다. 그렇다면 BBS와 Blog는 OnlineDocument를 상속받은
> class들일 수도 있고
> OnlineDocumnet의 객체로서 몇가지 다른 member들을 가지고 있을지도 모르겠습니다.
> Member는 그것이 객체이든 method이든 같은 기원을 가지는 것으로부터 구별을 하게 해주는 특징을 부여해 줍니다.
> 하지만 Resource 이름들의 무제약으로 인하여 이런 객체들의 member 이름들을 주기가 곤란해지게 됩니다.
> 만일 member와 동일한 이름의 resource가 만들어지면 기존 객체의 member는 지워지기 때문이죠.
> 결국 우리는 기존에 사용하던 풍부한 속성을 가지고 다양한 message를 처리하는 객체를 잃어버리게 됩니다.
> 우리는 본래 /BBS/Obama 와 /Blog/Obama가 보여주는 응답객체 혹은 문서구조가 다를 수 있기를 바랍니다.
> 즉 다른 화면을 기대하죠. 하지만 resource 이름의 무제약은 객체에 다양한 속성을 주기 어렵게 만듬으로써 이를 어렵게 만듭니다.
>
> 결국 우리들은 모두 복합 pattern에 의하여 객체구조와 Resource 구조의 유사성을 보고 동일하게 표현할 방법을 찾았지만
> 기존 programming 언어(Python)가 가지고 있는 많은 특징들로 인하여 언어와 통합되면서 많은 제약을 가지게 된
> 객체구조에 resource 구조를 그대로 적용할 수 없음을 깨닫게 되었습니다.
>
> 그럼 해결책은 무엇일까요? 방법은 언제나 간접층을 두는 것입니다.
> 결국 우리는 resource 구조를 객체구조에 그대로 적용시키지 말고 method 호출 형태를 두어야 한다는 것입니다.
>
> 즉 /Blog/Obama는 Blog.Obama가 아니라 Blog.child("Obama")나 Blog.child["Obama"]가 되어야
> 합니다.
> /Blog/tag/if는 Blog.child("tag").child("if") 혹은
> Blog.child["tag"].child["if"]가 되어야 하는 것이죠.
>
> 하지만 이래서야 기존 생각했던 resource -> 객체 라는 ROM의 쉬운 표현장점이 많이 퇴색해진 느낌입니다.
> 물론 장점은 남아있습니다. 하지만 기존의 생각이 잘 이루어진다면 10점 만점에 10점이라면
> 이것은 10점 만점에 5점에 지나지 않습니다.
> 컴퓨터 과학의 모든 문제는 간접층을 하나 더 둠으로써 해결되지만 또한 모든 성능문제는 간접층을 제거함으로써 해결됩니다.
> 사용성도 마찬가지입니다. 새로운 newbie들은 이 정도의 표현장점으로 새로운 framework을 배우려 할까요?
> 결국 우리는 타협점을 제시해야겠죠. ^_^
>
> 저는 이 글을 쓰는 도중(정말이예요!!!) 아주 적절한 기존 system을 찾았습니다. 바로 JavaScript 객체입니다.
> JavaScript 객체는 아시다시피 hash table입니다. Python의 dict죠.
> JavaScript에서 o.a는 o["a"]와 같습니다. JavaScript의 객체는 method도 가지고 있죠.
>
> Python의 __getattribute__, __getattr__, __setattr__, __getitem__ 은
> JavaScript 객체와 동일한 방식으로 Python 객체를 만들수 있게 해줍니다.
> 이제 우리의 /Blog/tag/if 는 Blolg.tag.if가 될 수도 있고 Blog["tag"]["if"]가 될 수도 있습니다.
> 저는 Blog["tag"]["if"]가 기본이 되어야 한다고 생각합니다. 각 객체의 고유 member들이 의도치 않게 훼손되지 않기
> 위해서죠.
> 예를 들어 Blog.tag 또는 Blog["tag"]는 sort method를 가지고 있다고 합시다. 당연히 정렬된 tag 목록을
> 반환해주는 method지요.
> 그리고 tag들 중에는 당연히 sort라는 tag를 쓸 수도 있습니다.
> 만일 client programmer가 Blog.tag.sort 라고 한다면 이것은 tag들을 sort하는 정렬 method를 참조한다고
> 봐야 합니다.
> 만일 sort tag를 가지고 있는 post목록을 얻고 싶다면 Blog.tag["sort"]라고 해야겠죠.
> 우리는 객체의 고유 member를 활용하는 형태의 programming 기법에 익숙하기 때문입니다.
> 반대로 있을지 없을지 알 수 없는 있어도 언제 나타날지 알 수 없는 resource는 뒤로 미루는게 맞지 않을까요?
> 우리 programmer들은 각 method들마다 test를 해야합니다. 그런때마다 간접 method를 경유해야 한다면 많이 불편하겠죠.
> 따라서 객체의 고유 member가 우선이 되어야 하는 것이 정답인 것이죠.
> 또한 resource 표현도 보다 일관성 있게 됩니다.
> 즉 Blog.tag.if는 syntatic sugar로 두고 기본은 Blog["tag"]["if]를 두고 이것을 문서에 먼저 표현합니다.
> 그렇게 함으로써 기존 Python 객체구조와의 혼돈을 막습니다.
> 또한 /Blog/post/1 도 처음 쓰는 사람들은 Blog["post"]["1"]을 사용할 것이기 때문에 문제될 것이 없습니다.
>
> 한번 더 생각해야 하는 것은 __setattr__ 입니다.
> 우리는 여기서 선택의 기로에 왔음을 알 수 있습니다.
> 일관성을 위해서라면 get처럼 set도 객체 고유 member가 우선시되어야 합니다.
> 하지만 우리가 member를 직접 set할 필요가 있을까요?
> Prototype based OOP 라면 당연히 필요합니다.
> 하지만 Python은 엄연히 class라는 keyword를 지원하잖습니까?
> 따라서 class에서 만들어진 객체들이 member들을 추가한다던가 혹은 변경하는 것이 자연스러워 보이지는 않습니다.
> 그래서 저는 __setattr__은 child resoucre를 변경 혹은 추가하는 것으로 두고
> 객체고유 member는 set_member라는 method를 두어서 set 하는 것을 추천합니다.
> 하지만 이것은 일관성이냐, 아니면 실용성이냐를 두고 좋고 싫고가 좀 갈릴 것 같네요.
>
> 어쨌든 문서에서는 Blog["Obama"] = "Obama is the president of USA"를
> Blog.Obama = "Obama is the president of USA" 보다 먼저 보여줘야겠죠.
> 우리들의 2MB는 syntatic sugar 따윈 안 먹히는군요.. ^_^
>
> 저는 HTTP METHOD에 대응되는 객체 method를 위한 decorator는 이제 효용성이 많이 없어졌다고 생각합니다.
> 제 생각에 decorator의 첫 출현 목적은 의미있는 이름을 method에 HTTP METHOD에 대응성을 보여주는 것이었지만
> Resource를 상속받는 class 이름만 잘 지어도 method에 의미는 다른 의미는 불필요하다고 생각하거든요.
> 원래 REST 철학이 METHOD를 필수적 요소만 남기는 것이잖습니까? 그 의미 외에 더 이상의 의미는 불필요할 것 같습니다.
> 저는 그냥 Blog.GET이 GET /Blog를 처리했으면 하군요. 홍민희씨는 모두 대문자로 쓰는 것이 Pythonic 하지 않다고
> 했지만
> 그렇기에 더욱 HTTP METHOD에 대응됨을 부각시켜 보여주는 것이 아닐까 합니다.
> 하지만 아직 이 문단의 글은 확신이 들지는 않습니다.
>
> 구현적 측면에서는 Resource class 내에 child_resource를 두고 이녀석을 dict로 만들고 __getitem__
> 처리를 위임하는 것이 좋을 것 같습니다.
> 당연히 Resource.__dict__를 그대로 써서 member들의 훼손이 우려가 되면 안되기 때문이죠.
>
> 그럼 이야기를 마칩니다.
>
> 이 mail은 언젠가 rationale 문서로 만들면 좋겠다는 생각이 드는군요.. ^_^

김우승

unread,
Mar 11, 2009, 6:41:22 AM3/11/09
to vlastic-d...@googlegroups.com
아무래도 이 부분은 제가 맡고 있었던 것이다보니 제가 가장 큰 관심을 가질 것 같네요. 홍민희님의 글을 읽으면서 저도 이해가 안 가는 부분이 있었습니다만 강준수님과 최종열님도 읽어보시고 궁금한 점이 있으시면 물어보시고, 저희가 생각하지 못했던 idea가 있으시면 말씀해주시면 감사하겠습니다.
 
그런데...

 
그래서 다른 언어로 ROM을 구현했다면 각 리소스의 자식 리소스(그러니까, /child와 같은 형태로 접근되는 것 말입니다)의
접근을 객체로 표현할 때,

  using vlastic;

  class person: rom::resource<person> {
      picture_list get_pictures() const { ... }
      post_list get_posts() const { ... }
  };

이렇게 작성하는 편이 (아마도) 자연스러웠을 것이고, HTTP 메서드(GET, POST, PUT, DELETE 등…)에 대해
rom::resource::get(), rom::resource::post() 같은 멤버 함수를 정의하기도 더 쉽지 않았을까
하는 생각을 합니다. 자식 리소스의 접근에 대해서는 멤버 함수에 get_이라는 접두사가 항상 붙게 되어 네임스페이스 문제가
사라졌겠죠.
 
그 이름도 유명하신 CRTP(Curiously Recurring Template Pattern)이군요... -_-. 저는 이 녀석의 활용도를 아직 잘 파악하고 있지를 못해서... Mixin을 나타내는 거라고 들었는데 그렇다면 다중상속과도 연관있겠습니다만 C++에서는 상속을 통해서 구현되니 다중상속을 보이는 또다른 형태는 되지 못하겠군요. 어쨌든 흥미로운 code 였습니다.
  
 
제가 생각한  것은 이런 식으로 객체의 애트리뷰트/프로퍼티를 자식 리소스의 접근으로 사용하는 거였는데, 문제는 Python
객체 모델은 Lisp-1 vs Lisp-2 debate[1]에서 말하는 Lisp-1에 해당하기 때문에, 즉, 객체의 멤버
함수도 사실은 해당 이름의 애트리뷰트로 함수 객체가 들어있을 뿐이므로 resource.get(), resource.post()
같은 식으로 HTTP 메서드를 위한 별도 멤버 함수를  만들 경우 /get, /post에 해당하는 자식 리소스를 만들 수가
없게 된다는 점입니다. 명백하지만 어려운 말을 쓰자면, 저희가 필요한 셀은 두 구역(HTTP 메서드, 자식 리소스 이름)인데
맵핑할 Python 객체에는, 지금까지는, 일단 셀이 하나(애트리뷰트)인 것처럼 보이죠.
 
제가 생각했던 고민은 한가지 문제를 더 포함하고 있습니다. 이전의 e-mail에도 써놓았듯이 그것은 객체의 고유특징을 나타내는 member 변수와 member 함수에 대한 것이죠. HTTP METHOD 처리자가 길어지거나 공통적인 code를 포함하고 있다면 따로 member 함수로 뺄 필요성이 있습니다. 이 경우 또다시 object.__dict__ 이름영역을 같이 사용하게 됩니다. 또한 다음과 같이 객체에 state를 둔다면
 
class TagList(Resource):
    def __init__(self, logo):
        self.logo = logo
 
class Tag(Resource):
    def __init__(self, name):
        self.name = name
 
aboutCS = TagList("cs.png")
aboutCS.logo = Tag("LOGO Programming Language")
 
aboutCS 라는 TagList는 그림으로 나타내기 위해 필요한 logo라는 member 변수가 다시 LOGO Programming Language를 나타내는 logo resource와 이름충돌을 일으키게 됩니다. 사실 객체의 state는 처음 신촌에서 만났을 때 resource를 나타내는 class들의 method들이 static method가 아니고 객체 method 여야 하는가에 대해서 의문을 제기했던 홍민희님의 생각과도 연결되어 있습니다. 제가 생각하기에는 현재 형태에서 동일 class의 여러객체가 만들어질 가능성이 충분히 있고, 그렇기에 객체의 member 변수는 필요하다고 봅니다.
 
이런 형태에 대해서 여러가지를 생각해 보았지만 흔히 벌어지는 일은 아니더군요. 이런 문제는 URL의 결정권이 program의 end user에게 있는 경우에 발생한다고 볼 수 있습니다. 그리고 그런 대표적인 경우가 바로 tag인 거고요.
 
결국 framework를 만드는 우리는 framework를 활용하는 client programmer만이 아니라 client programmer가 작성한 program을 활용하는 end user까지 어느 정도 고려하지 않으면 안되기 때문에 좀 골치가 아픈 거라고 할 수 있습니다. 그리고 우리가 골치가 아파야 하는 이유는 이 문제야 말로 end user를 생각하는 client programmer를 생각하는 vlastic의 핵심고민이기 때문이죠.
 
가만히 생각해보면 사실 HTTP METHOD 처리자 함수들은 별 문제없이 기존의 Python 객체 member 들과 어울립니다. Class 기반의 OOP에서 기반 class의 member들을 함부로 건드리지 않거나 재정의해서 사용하는 것은 기존 관행과 상충되지 않기 때문이죠. 하지만 어떤 이름인지 알수 없는 member가 runtime에 등장하는 것은 언어 hacking이 아니고서는 기존 class 기반 OOP 관행과는 어울리지 않기에 자식 resource를 위한 이름공간은 분리하는 것이 좋다고 생각했습니다.
 
여기까지가 Python에 대해서 충분히 알지 못하는 제 생각이었습니다만 inspect module을 잘 안다면 뭔가 다른 해결책이 나올 수도 있을 것 같긴 합니다. 걱정스러운 점은 제가 그걸 할 수 있겠냐라는 점이군요. Library Reference 고작 한쪽이긴 합니다만 제대로 사용하지 않을 경우 문제가 많을 것 같아서요.
 
걱정되는 또다른 문제는 결국 client programmer가 자식 resource인가 아니면 객체의 고유 member인가를 구분해야 하는데 결국 client programmer도 그 구별작업에 참여를 해야 한다는 거죠. 정의구역이 정해지는 member 함수와는 달리 member 변수는 이 작업을 하기가 어렵습니다. Decorator는 함수에 붙일 수는 있어도 대입문에 적용할 수는 없잖습니까?... 결국 기존 Python 객체 활용법과는 달리 특정 member 함수(전에 보낸 e-mail에 적었듯 set_member같은)를 통해 적용하는 정도를 생각해 볼 수 있습니다. 제가 생각하기에 가장 적절한 방법은 class decorator가 아닌가 싶습니다.
 
  제가 문제가 된다고 생각하는 것은 main.tags는 TagList 인스턴스이고, TagList는 일종의 문자열을 키로 가지는
유사 딕셔너리로 보는 것이 맞은데, 어느 나라 Python 딕셔너리가 dict.key 형식을 사용하냐는 거죠. 전혀 자연스럽지
않죠. dict["key"]가 훨씬 자연스럽습니다. 애트리뷰트와 달리 첨자 연산자는 그 안에 어떤 것이 들어갈지는 런타임에
결정된다는 코드의 의도를 충분히 드러냅니다. 객체 모양만 봤을 때도 훨씬 자연스럽고요.

결국 비슷한 결론이긴 한데, 제 생각에는 DictionaryResource라는 인터페이스 클래스를 제공해서, 그것을
서브클래싱할 때는 __getitem__ 멤버 함수를 구현하도록 하는 게 좋을 것 같습니다. 이걸 상속 받으면 자식 리소스를
찾을 때 애트리뷰트는 전혀 뒤지지 않고, 오직 첨자 연산자에서만 찾게 됩니다. 태그 이름이나 사용자 이름을 주소로 사용하는
경우 훨씬 자연스러울 것 같습니다.
 
결론 쪽으로 가서 두 개의 기반 class를 사용하자는 의견은 상당히 흥미롭습니다. 두 개를 모두 제공하는 것이 꼭 필요하냐는 생각을 해볼 수 있습니다만 사실 우리들은 1.0을 내놓기가 애매한 상황이니까 현재로서는 충분히 실험해보는 것도 좋은 것 같고요. 우리들이 framework를 사용해보고 두 개 중 하나가 필요성이 떨어진다면 1.0이나 2.0에서 제거할 수도 있겠지요.
 
확실히 root["blog"]["post"] 형태는 모양새 때문에 10점 만점에 8점도 주기가 좀 아깝습니다. 하지만 root.blog.tag.if 가 안되는 기존 객체구조형태도 제가 생각하기에는 9점을 주기에는 아깝습니다. 활용도에 따라 client programmer가 선택해서 쓴다면 좋은 것 같습니다.
 
그리고 또 하나, (글이 길어지지만,) 제게 떠오른 이슈가 있습니다. 애초에 제가 리소스는 Python에서 일반적인 객체로도
사용할 수 있어야 한다고 생각했던 것은 테스트 코드를 작성하기 쉽게 하기 위해서였습니다. 예를 들어,
/tags/abc/authors?page=3를 테스트하는데,

  authors = AuthorList()
  req = http.Request("GET", "/tags/abc/authors?page=3")
  req.back_component = ["tags", "abc"]
  result = authors(req)
  assert "<strong>abc</strong>" in result
  assert "Total authors: <strong>37</strong>" in result

이렇게 하기 보다는,

  result = main.tags["abc"].authors(page=3)
  assert "abc" == result["tag_name"]
  assert 37 == result["total_authors"]

이쪽이 훨씬 간결하고, 편하고, 수정이 용이하지 않겠습니까. 그래서 여러가지를 생각하고 있었는데, 그 중에 하나가

  @resource
  def authors(self, page=1):
      ...
      return {"tag_name": self.tag.name, "total_authors": ...}


이런 식으로, 쿼리 문자열의 각 키들을 Python 키워드 인자로 맵핑해주는 기능입니다. 그런데 곰곰히 생각해보니 리소스
객체는 request를 받고 response를 내주는 함수 객체여야 하는데 키워드 인자를 받아서 dict를 내주는게 가능하냐는
겁니다. 아직 해결된 것은 아닌데 키워드 없이 request 객체 하나만 전달될 경우에는 resource 함수로 작동하고,
키워드 인자가 들어오거나 인자가 없으면 일반 메서드처럼 작동하게 오버로딩되게 하면 어떨까 하는 아이디어가 있긴 합니다. 더
좋은 방법이 있을 것 같은데, 생각이 나지 않네요.

 
동일하지는 않습니다만 저도 지금 비슷한 고민을 하고 있습니다. 우선 back_component는 제가 약간 잘못 표기했었는데 back_components(복수형) 객체구조를 따라 resource 를 찾아가는 형태에서 중간에 이를 가로채어서 남은 path component들을 인자로 활용하는 방법에 사용해야 한다고 생각했던 부분입니다.
 
예를 들어 GET /calendar/2003/10/20 처리하기 위해서
 
class Calendar(Resource):
    def __call__(self, request):
        self.get(*request.back_components)
 
    def get(self, year, month, day):
        ...
 
calendar = Calendar()
caleandar(Request("GET /2003/10/20")
 
의 형태를 생각하고 있었습니다.
 
홍민희님이 작성하신 code가 제 생각과는 좀 다른 것 같아 일단 적어보았고요.
 
Query 문자열에 대해서 keyword 인자로 넘겨주자는 생각은 저도 했습니다만... 만일 end user가 "GET /calendar/2003/10?day=20" 을 request 한다고 하면 이건 error로 처리해야 하는 것이 아닌가라고 생각했습니다.
 
하지만 __call__에서 self.get(*request.back_components, **request.query) 형태로 get을 호출한다고 할때 문제없이 호출되기 때문에 이건 좀 결함이 있는 것이 아닐까라는 생각이 들었죠. 물론 한번 더 중간과정을 거쳐서 error를 낼 방법은 있습니다만...
 
그런데 이런 생각을 하기 전에 우리는 결국 request는 일단 넘기고 봐야 합니다. 왜냐하면 request에 각종 header가 들어있고 이것은 Resource를 상속받아 핵심이 되는 get, post HTTP METHOD 처리 함수를 작성하는 client programmer에게 꼭 필요하기 때문이죠.
즉 self.get(request, *request.back_components, **request.query) 형태를 쓰는 것은 중복이 아닌가 싶습니다.  Django에서는 자식 resource에 대해서만 인자로 받고 query에 대해서는 그냥 request["GET"]과 request["POST"] 형태를 씁니다만 저는 그냥 get(request): ... 로 끝내는 것이 simple하고 좋지 않을까 생각합니다. 이후 client programmer가 request.front_components나 request.current_component, request.back_components를 알아서 사용하는 것이 가장 완결성있고, 일관성이 있을 것 같거든요. Header만 또 하나의 인자를 줄 수도 있겠습니다만 어차피 request 정보 중 METHOD 이외에는 모두 필요한 상황에서(왜냐하면 METHOD는 객체 member함수를 의미하므로... ^_^) 그걸 분해해서 넘기는 것은 좋은 방법이 아니라고 생각합니다.
 
결국 HTTP 1.0과 HTTP 1.1의 header가 있는 request message를 위해서는 반드시 request는 넘기고 봐야 합니다. 홍민희님이 생각하신 code는 HTTP 0.9의 Simple Request에는 어울려 보입니다. 하지만 그것을 위해 중복작업을 하는 것은 제가 생각하기에 그닥 경제성은 없어 보이네요.

Hong MinHee

unread,
Mar 11, 2009, 12:26:29 PM3/11/09
to vlastic-d...@googlegroups.com
request 객체가 필요한 경우가 웹 애플리케이션 만들 때 그렇게 많진 않다고 봅니다. 그럼 그렇다고 아예 request
접근 기능을 빼버리자고 하면 안되겠죠. 너무 길어질까봐 사실 빼먹은 내용이 있는데, function annotations
[PEP 3107] 기능을 이용해서 type mapping도 해줄 생각입니다. (전에 언급했던 것 같기도 한데요.)

@resource
def authors(self, page: int = 1):
...

실제 query string은 결국 바이트열이므로 page에 해당하는 값은 int형 1이 아니라 bytes형 b"1"이
들어오지만, 위와 같이 정의하면 int형으로 자동 변환되게 됩니다. 이건 뭐 {type: converting_function}
형식의 딕셔너리를 어디에 두던가, 스폐셜 멤버 함수 이름을 정해서 그걸 호출하게 한다던가... 여러가지 방법이 있을 것 같습니다.
pickle 등이 어떻게 해결했는지 확인해보죠.

아무튼 이런 상태에서 실제로 request가 필요하면 이런 식으로 정의하면 될 것 같네요.

@resource
def authors(self, request: http.Request, page: int = 1):
...

http.Request 타입의 경우 type mapping을 해주는게 아니라 실제 request 객체를 전달하는 예외가 있어도
문제가 없을 것 같습니다. form 따위로 http.Request 객체를 (어떻게 직렬화할지 상상이 안되지만) 바이트열로
직렬화해서 넘기고 받고 할 일은 없을 거라고 봅니다. 이렇게 할 경우 한 가지 문제가 자동적으로 해결되는데요, 테스트를 작성할
때 명시적으로 "이건 request 객체가 필요한 것"이라고 알려줄 수 있고, 그런 경우에만 TypeError를 낼 것이기
때문입니다. (제가 하는 의도가 제대로 전달이 될런지 모르겠군요.)

2009년 3월 11일 (수) 오후 7:41, 김우승 <lohas...@gmail.com>님의 말:


> 아무래도 이 부분은 제가 맡고 있었던 것이다보니 제가 가장 큰 관심을 가질 것 같네요. 홍민희님의 글을 읽으면서 저도 이해가 안 가는
> 부분이 있었습니다만 강준수님과 최종열님도 읽어보시고 궁금한 점이 있으시면 물어보시고, 저희가 생각하지 못했던 idea가 있으시면
> 말씀해주시면 감사하겠습니다.
>
> 그런데...
>
>>
>> 그래서 다른 언어로 ROM을 구현했다면 각 리소스의 자식 리소스(그러니까, /child와 같은 형태로 접근되는 것 말입니다)의
>> 접근을 객체로 표현할 때,
>>
>> using vlastic;
>>
>> class person: rom::resource<person> {
>> picture_list get_pictures() const { ... }
>> post_list get_posts() const { ... }
>> };
>>

>> 이렇게 작성하는 편이 (아마도) 자연스러웠을 것이고, HTTP 메서드(GET, POST, PUT, DELETE 등...)에 대해

Reply all
Reply to author
Forward
0 new messages