💻 프로그래밍/Python

Python 에서 몽고반점 말고 몽고디비(MongoDB) 다뤄보기

피트웨어 제이 (FitwareJay) 2021. 9. 26. 22:33

 

안녕하세요! 운동하는 개발자 Jay 입니다! 오늘은 몽고반점 ... 아 아니 몽고디비(MongoDB)에 대해서 알아보고 간단한 사용법 및 DDD(Domain Driven Development) 개념 중 RepositoryPymongo를 사용해서 간단하게 구현해 보겠습니다!

 

 

1.  What is MongoDB


저는 이번에 이직을 하면서 처음 MongoDB를 사용해봤는데요.
(아직 제대로 사용은 안해봤구 그냥 보기만 해 봤네요...ㅎㅎ 그래서 지금 블로그로 정리하면서 공부중)

NoSQL 종류 중 하나로  JSON과 같은 동적 스키마형 도큐먼트들(몽고 DB는 이러한 포맷을 BSON이라 부름)을 선호함에 따라 전통적인 테이블 기반 관계형 데이터베이스 구조의 사용을 삼가합니다. 이로써 특정한 종류의 애플리케이션을 더 쉽고 더 빠르게 데이터 통합을 가능하게 합니다.

(출처: 위키백과)

 

쉽게 말하면 RDB 처럼 스키마를 정하고 데이터를 저장하는 방식이 아닌 document 형태(json 같은)의 데이터를 그대로 저장하는 방식입니다. 그래서 스키마도 없고 자유롭게 원하는 데이터를 추가하고 가져올 수 있습니다. (물론 저장하고자 하는 모델(?)에 따라서 스키마처럼 저장 시 제약이나 그런 로직을 애플리케이션 레벨에서 수행하겠죠?)

 

일단 저는 docker-compose 로 로컬에 MongoDB 를 설치해서 연결했습니다.

1
2
3
4
5
6
7
8
9
10
11
  mongodb:          
    image: mongo    
    container_name: "mongodb" #
    restart: always # container를 실행할 때 항상 이미 수행중이라면 재시작을 수행합니다.
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
     MONGO_INITDB_ROOT_PASSWORD: root
    volumes:
      - ./mongo_db/data:/data/db
    ports:                
      - "27017:27017"  
cs

 

꼭 docker로 하지 않아도 편한 방법으로 하시면 됩니다! 

 

2.  MongoDB 연결해보기


python에서 쉽게 MongoDB를 사용할 수 있는 pymongo라는 라이브러리를 사용하겠습니다.

1
2
3
4
5
6
7
8
from pymongo import MongoClient
 
 
mongo_uri = "mongodb://(username):(password)@127.0.0.1:27017/"
client = MongoClient(mongo_uri)
 
print(client.list_database_names())
 
cs

 

듀토리얼을 보면 요런식으로 연결하여 list_database_names() 를 실행하면 데이터 베이스 리스트를 확인할 수 있습니다. 기본적으로 생성된 database인 admin, config, local 이 출력돼야 하며 한번 실행해보겠습니다!

홀리몰리~

InvalidURI 오류가 떠서 검색해 보니까 username, password quote 를 사용해서 파싱 해줘야 한다고 하네요.

1
2
3
4
5
6
7
8
9
10
from urllib.parse import quote
 
from pymongo import MongoClient
 
 
mongo_uri = "mongodb://(username):" + quote("(password)"+ "@127.0.0.1:27017/"
client = MongoClient(mongo_uri)
 
print(client.list_database_names())
 
cs

 

요렇게! 다시 수정하여 실행해보니

오호! 잘 나온다!

정말 기본적으로 생성되는 DB 리스트가 출력되었습니다! 그럼 실제로 우리가 MongoDB에 접속해서 한번 확인해볼까요?!

일단 저는 CLI 환경이 아닌, 좀 더 편하고 빠르게 보기 위해서 MongoDB Compass 라는 GUI 툴을 사용하기로 했습니다. CLI가 편하신 분은 CLI 환경에서 하셔도 됩니다!

 

MongoDB Compass 롤 접속해서 확인한 모습

와우! 정말 우리가 출력해봤던 DB 들이 잘 만들어져 있네요!

 

 

3. MongoDB 사용하기


이번엔 간단히 입력을 해보고, Repository Class를 만들어서 추상화하는 작업을 해보겠습니다.

test_db 생성 및 member collection 생성

test_db라는 데이터베이스를 생성하고 member라는 이름의 collection을 추가합니다. 여기서 collection은 RDB의 Table과 같은 개념이라고 보시면 됩니다! 

자 DB와 collection을 생성했으니 member라는 collection에 데이터를 넣어 보겠습니다.

insert_one 함수 설명

pymongo 듀토리얼을 보니 insert_one이라는 함수로 데이터를 넣어주네요! 똑같이 한번 해보겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
from urllib.parse import quote
 
from pymongo import MongoClient
 
 
mongo_uri = "mongodb://(username):" + quote("(password)"+ "@127.0.0.1:27017/"
client = MongoClient(mongo_uri)
database = client['test_db']
result = database['member'].insert_one({"name""제이""job""백엔드 개발자"})
print(result.inserted_id)
 
cs

 

DB, Collection 접근은 MongoClient가 정상적으로 커넥션 되었다면

1. client['database 이름']['collection 이름']
2. client.database_이름.collection_이름

 

이렇게 두 가지 방식으로 접근이 가능합니다. 자 그럼 데이터가 정상적으로 들어갔는지 확인해보겠습니다.

와우! 정상적으로 추가된 데이터

정상적으로 우리가 넣은 데이터가 저장되었습니다. 여기서 _id라는 필드에 있는 ObjectID는 RDS Primary key와 같은 역할을 하며 자동으로 생성해줍니다. ObjectID에 대한 자세한 설명은 여기를 확인해주세요.

자! 우리는 그럼 DDD의 개념 중 하나인 데이터베이스 접근을 위한(CRUD) Repository를 만들어 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MongoRepository:
 
    def __init__(self, conn=None, database_name=None, collection_name=None):
        self._conn = self._get_connection() if not conn else conn
        self._database_name = database_name
        self._collection_name = collection_name
 
    @staticmethod
    def _get_connection():
        mongo_uri = "mongodb://(username):" + quote("(password)"+ "@127.0.0.1:27017/"
        return MongoClient(mongo_uri)
 
    @property
    def database(self):
        return self._conn[self._database_name]
 
    @property
    def collection(self):
        return self.database[self._collection_name]
 
cs

 

MongoRepository라는 클래스를 만들었습니다.

특별한 건 없고 우리가 위해서 했던 MongoClient 접근을 클래스로 추상화시킨 것입니다.

1
2
3
4
mongo_repo = MongoRepository(database_name='test_db', collection_name='member')
print(mongo_repo.database)
print(mongo_repo.collection)
 
cs

 

database, collection을 print 해보면

 

요렇게 Database, Collection 접근이 정상적으로 이루어진 걸 볼 수 있습니다.

이제 여기에 insert_one, find_one, update_one 등 기본적인 CRUD 함수를 추가해 보겠습니다. 

(위 함수들은 pymongo에서 제공하는 함수입니다. 튜토리얼을 참고하세요)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MongoRepository:
 
    def __init__(self, conn=None, database_name=None, collection_name=None):
        self._conn = self._get_connection() if not conn else conn
        self._database_name = database_name
        self._collection_name = collection_name
 
    @staticmethod
    def _get_connection():
        mongo_uri = "mongodb://(username):" + quote("(password)"+ "@127.0.0.1:27017/"
        return MongoClient(mongo_uri)
 
    @property
    def database(self):
        return self._conn[self._database_name]
 
    @property
    def collection(self):
        return self.database[self._collection_name]
 
    def insert_one(self, data: dict):
        if not data:
            return
        return self.collection.insert_one(data)
 
    def find_one(self, _filter: dict):
        if not _filter:
            return None
 
        return self.collection.find_one(_filter)
 
    def update_one(self, _filter: dict, update_data: dict):
        if not update_data:
            return
 
        return self.collection.update_one(_filter, update_data)
 
cs

 

이렇게 MongoRepository에 CRUD를 위한 함수들을 추가했습니다. 특별히 구현한 건 없고 pymongo에서 지원하는 함수들을 클래스에 정의된 database, collection과 함께 사용할 수 있도록 했습니다.

 

이제 이 MongoRepository를 바로 사용하는 게 아닌 Member Collection을 위한 MemberRepository를 만들어 보겠습니다. 당연히 MongoRepository를 상속받아서 사용할 것입니다!

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MemberRepository(MongoRepository):
 
    def __init__(self):
        super().__init__(database_name='test_db', collection_name='member')
 
    def get_or_create_member(self, name: str):
        _member = self.find_one({'name': name})
 
        if not _member:
            self.insert_one({'name': name})
            _member = self.find_one({'name': name})
 
        return _member
 
    def update_job(self, member_id: ObjectId, job: str):
 
        if not job:
            return
 
        return self.update_one({'_id': member_id}, {'$set': {'job': job}})
cs


MemberRepository
에서는 member를 생성하거나 member정보를 가져오는 get_or_create_member 함수와 Member의 job을 업데이트할 수 있는 update_job 함수를 추가했습니다.

1
2
3
4
5
6
member_repo = MemberRepository()
member = member_repo.get_or_create_member(name='제이')
print(member)
member = member_repo.update_job(member.get('_id'), '프론트엔드 개발자')
print(member_repo.get_or_create_member(name='제이'))
 
cs

 

위 코드는 '제이'라는 member를 찾거나 생성해서 정보를 가져오고, update_job을 통해 job 데이터를 업데이트 하는 로직입니다.

아까 위에서 '제이' 라는 member를 추가했으니 새로 생성하진 않고, 찾아와서 업데이트하겠죠?

 

백엔드 -> 프론트엔드 개발자가 되버린 제이

와우! 매우 잘 되었습니다!!!



4. 마치며


위에서 크게 MongoDB의 장단점에 대해서는 설명하지 않았습니다. 이 부분은 여러분들이 한번 찾아보시길 권장드립니다.
(글 쓰기 귀찮아요 ~ 라고 할 뻔)
이번에 팀을 옮기면서 FastAPI와 DDD를 도입한 프로젝트를 보고 있는데, 처음에 MongoDB에 대한 개념을 모르니까 뭐가 뭔지 모르고 더 어렵더라고요! 그리고 pymongo라는 라이브러리에 대해서도 잘 몰랐고! ㅎㅎ 암튼 이번에 저희 팀 팀장님께서 설계하신 부분에 대해 설명 듣고 지금 한번 비슷하게 pymongo 듀토리얼 보면서 만들어보니까 어느 정도 이해가 된 것 같습니다!
여러분도 다른 개발자가 만들어 놓은 아키텍처나 함수, 클래스가 있으면 한번 따라서 만들어보고 직접 해보시면 이해하기 더 쉬울 거예요!

그럼 오늘도 즐거운 하루 보내세요!