본문 바로가기
Coding/Python

MongoEngine EmbeddedDocument/Reference

by Hide­ 2019. 3. 18.
반응형

MongoEngine Embedded vs Reference


현재 파이썬으로 MongoDB와 연동하여 작업을 진행하고 있다. Raw query를 사용하는 사람은 당연히 없으므로 ODM(Object Document Mapper)을 찾아보던 중 Mongoengine이 가장 좋을 것 같다는 판단이 들어서 해당 ODM을 선택하여 개발중이다.


MySQL등 RDB계열을 사용하다가 처음 NoSQL으로 넘어온 사람들에게 가장 헷갈리는 부분이 바로 '관계를 어떻게 표현해야할까' 이지 않을까 싶다. 나같은 경우에도 기존 관계형 데이터베이스만 사용하다보니 NoSQL에서 관계를 어떻게 해야할 지 많은 고민이 됐다. 이 포스팅은 그에 관련하여 많은 정보를 담진 않을 것이고 실제 Mongoengine을 통해 어떠한 형태로 도큐먼트를 정의하고 쿼리를 날리는 지 기술적인 부분에 좀 더 집중할 것이며 퍼포먼스나 여러가지 측면에서 비교가 필요한 사람은 직접 구글에서 검색하여 찾아보는것이 나을 것이다. Mongoengine에서 Embedded와 Reference는 아래처럼 사용한다.


Embedded : EmbeddedDocumentField

Reference : ReferenceField


개인적인 생각으로 둘 중 어떠한 방법을 사용하던지 크게 다른점은 없는 것 같다. 다만 Reference를 사용하면 좀 더 유연하게 사용이 가능하달까. 몽고엔진 인 액션 책에 따르면, 자식 객체가 자신의 부모 객체 외부에 절대 나타나지 않는 경우라면 포함시키고 그렇지 않다면 별도의 컬렉션을 만들어 저장하라고 한다. 예를 들어 게시글/댓글의 구조로 생각해보자. 댓글이 게시글 내에서만 나타나고 어떠한 방식으로도 정렬할 필요가 없다면(게시물 생성 날짜 등) Embedded가 좋다. 하지만 어느 게시물에 대한 것인지 상관없이 가장 최근의 댓글을 보여주는 등 외부에 노출되는 경우라면 Reference가 낫다. Embedded는 성능이 약간 더 좋은 반면에 Reference는 유연성이 훨씬 뛰어나다.


Reference

먼저 Reference부터 사용하는 방법에 대해 살펴본다. 아래와 같은 형식으로 Comment, Article document 클래스를 생성한다.


from mongoengine import *


connect('test')


class Article(Document):
title = StringField()


class Comment(Document):
article_id = ReferenceField(Article, reverse_delete_rule=CASCADE)
body = StringField()

reverse_delete_rule=CASCADE는 MySQL에서 on_delete=CASCADE와 동일한 역할을 한다. 위와 같이 도큐먼트를 만들어놨으면 아래와 같이 Article과 Comment를 하나씩 생성해준다.


article = Article(title='hide').save()
comment = Comment(body='first comment')
comment.article_id = article
comment.save()

먼저 게시글을 하나 생성하고 해당 게시글을 댓글의 article_id에 넣어준다면 아래와 같이 생성된 게시글의 ObjectId값이 자동으로 담김을 확인할 수 있다.


이제 여기서 댓글에 저장되어있는 article_id를 통해 게시글에 접근하려면 어떻게 해야할까? 아래와 같은 쿼리를 날려주면 된다.


comment = Comment.objects(id='5c8f3d0e1b48d912cd3cbc39').first()
print(comment.article_id)

결과 : Article object


위처럼 댓글을 불러오고 바로 article_id를 찍어보면 게시글의 객체 자체가 넘어온다. 따라서 comment.article_id.title을 찍어보면, 연결되어있는 게시글의 title값이 나옴을 확인할 수 있다. (참고로 아까 위에서 CASCADE옵션을 줬는데 실제로 삭제되는걸 확인해봐야 한다. 몽고 쉘에서 삭제를 하면 안되고 .delete() 함수를 통해 댓글을 삭제해야 연결된 게시글이 연쇄적으로 삭제된다)


참고로 위와 같이 ReferenceField를 사용하게되면 댓글을 불러올 때 자동으로 연결되어있는 게시글까지 가져온다. 이를 Automatic de-reference라고 부르는데 만약 연결되어있는 게시글의 정보가 필요하다면 어쩔 수 없겠지만 댓글의 정보만 필요한 경우 퍼포먼스 측면에서 문제를 일으키게 된다. 따라서 댓글을 가져올 때 다음과 같은 방법으로 가져온다면 Auto dereference를 피할 수 있다.


comment = Comment.objects(id='5c8f477f1b48d913e135c073').no_dereference().first()
print(comment.article_id)

결과 : DBRef('article', ObjectId('5c8f3d0e1b48d912cd3cbc38'))


위와 같이 no_dereference()를 사용하는 방법도 있지만 애초에 필드 자체를 Lazy하게 돌아가도록 정의할수도 있다. 바로 LazyReferenceField()를 사용하는 것이다. 먼저 Comment 클래스의 article_id를 아래와 같이 LazyReferenceField로 수정한다.


class Comment(Document):
article_id = LazyReferenceField(Article, reverse_delete_rule=CASCADE)
body = StringField()

이제 댓글과 연결된 게시글을 출력시켜본다.


comment = Comment.objects(id='5c8f477f1b48d913e135c073').first()
print(comment.article_id)

결과 : <LazyReference(<class '__main__.Article'>, ObjectId('5c8f48ec1b48d914189684bd'))>


LazyReferenceField를 사용하여 연결된 게시글을 가져오고 싶을 경우 .fetch()를 사용해야 한다.


comment = Comment.objects(id='5c8f477f1b48d913e135c073').first()
print(comment.article_id.fetch())

결과 : Article object



Embedded

다음으로 Embedded형태로 사용하는 방법이다. 아래와 같이 도큐먼트를 정의한다.


from mongoengine import *


connect('test')


class Comment(EmbeddedDocument):
body = StringField()


class Article(Document):
title = StringField()
comments = ListField(EmbeddedDocumentField(Comment))

아까와 달라진점은, Comment 클래스에서 Document가 아닌 EmbeddedDocument를 상속받았다는 점이고 Article 클래스 내부에 EmbeddedDocumentField형태로 Comment 클래스가 담겼다는 점이다. ListField로 한번 감싸준 이유는, 댓글은 하나가 아닌 여러개가 될 수 있기 때문이다. 다음으로 아래의 코드를 통해 게시물을 생성하고 해당 게시물에 댓글을 삽입해본다.


comment = Comment(body='first comment')
article = Article(title='title')
article.comments.append(comment)
article.save()

먼저 Comment 객체를 하나 생성하고 Article객체를 생성한다. 그리고 위에서 우리는 ListField로 한번 감싸줬기 때문에 .append()를 통해 일반적으로 리스트에 추가하듯이 댓글을 넣어줄 수 있다. 다음으로 .save()를 통해 실제 게시글을 저장하면 된다. 결과물은 아래와 같다.



결론은, Embedded와 Reference 중 어떠한 방법을 선택할지는 설계하려는 서비스에 따라 다르기 때문에 정답이 없다는 점이다. 다만, 두가지 방법을 모두 사용할 줄 알고 원리를 이해하고 있어야 내가 구축하려는 서비스에 어떤 방법이 적합할 지 판단할 수 있을 것이다.