현재 내가 진행중인 프로젝트는 장고 하나의 서버에서 모놀리식으로 작동하고 있었다. 최근에 쿠버네티스를 도입하게 되어 이 기회에 MSA로 넘어가려는 시도를 하였다. 기획자에게 더 많은 사용자를 수용하고 성능도 더 나아지며 관리포인트가 줄어든다는 점을 들어 설득하여 기간을 할당받는데 OK를 받았다. 그렇게 MSA로 전환하기 시작한 여정이 시작된다.
장고에서 FastAPI로..
MSA로 넘어가며 장고를 포기하고 FastAPI를 사용하게 되었는데 그 이유는 2가지가 있다.
- 서드파티 등 장고의 풍부한 기능을 사용하지 않는다.
- MSA 서비스마다 장고로 띄우기엔 너무 무겁다.
처음부터 "뭐야 장고 정말 안좋네? FastAPI ㄱㄱ"한건 아니고 나름대로 문제를 해결하려고 시도는 했었다. 인스타그램같은 경우도 장고를 사용하고 있어서 본인들의 장고 사용 방식 소개 영상을 봤는데 테세우스의 배마냥 마개조를 해서 아예 다른 프레임워크를 만들어 놨다. 공식문서도 잘 안 돼있어서 "우리 만든건데 쓰려고? 쓸 수 있으면 써봐"느낌이어서 쓸 수도 없었고 똑같이 마개조 할 수 없었다. 그래서 비동기도 지원하고 더 가벼운 FastAPI로 넘어가게 되었다.
ORM을 사용해야할까요?
FastAPI로 넘어가면서 가장 먼저 마주친 문제는 ORM이었다. 장고는 강력한 내장 ORM을 제공하기 때문에 편리하게 사용할 수 있다. (가끔 복잡한 쿼리를 처리할 때는 독이 되기도 한다!) 특히 DDL 마이그레이션 기능이 정말 매력적이었다. 그런데 이미 FastAPI로 넘어온 시점에서 장고 ORM은 사용할 수 없고 다른 방법을 찾아야 했다.
파이썬에서 ORM으로 유명한 라이브러리 중 SQLAlchemy라고 있다. 이걸 사용한다면 장고의 ORM을 흉내낼 수 있을 것이다. 그래서 팀원과 고민하던 중 두 가지 이유로 도입하지 않기로 했다.
- 마이크로 서비스로 분리된 프로젝트마다 모두 ORM을 위한 스키마를 작성해야 하고 컬럼 하나가 변할 때마다 모든 프로젝트의 스키마를 변경해야 한다.
- N단 조인, 집계 등 복잡한 쿼리를 처리하기 힘들다. (실제로 장고로 작성할 때 느끼고 있던 점이었다.)
그래서 서로 합의 하에 "Raw쿼리로 작성하죠!"로 결론이 났고 앞으로 분리되는 서비스는 모두 Raw 쿼리로 작성하게 되었다.
Raw쿼리의 문제점
Raw쿼리를 사용하게 되면서 무지성으로 쿼리를 작성할 수 있었는데 몇 가지 불편한 점이 있었다. 바로 쿼리가 Programmable하지 않다! 예를 들어 A테이블에서 value가 10이하인 데이터를 불러오려면 아래와 같은 쿼리를 쓴다.
cursor.execute("SELECT * FROM A WHERE value <= 10")
OK 이정도는 무난하죠. 근데 특정 조건을 만족하면 2023년 이후에 만들어진 데이터만 불러온다는 조건이 추가되면?
if condition:
cursor.execute("SELECT * FROM A WHERE value <= 10 AND created_at >= '2023-01-01'")
else:
cursor.execute("SELECT * FROM A WHERE value <= 10")
이런 조건이 $N$개라면 나올 수 있는 경우의 수가 $2^N$개이다. ㄷㄷ 불행하게도 우리 프로젝트는 이런 케이스가 굉장히 많아서 if문으로 분기하기 힘들었다. 그래서 임시방편으로 다음과 같은 방법으로 넘어갔다.
query = ""
if condition:
query = " AND created_at >= '2023-01-01'"
cursor.execute(f"SELECT * FROM A WHERE value <= 10 {query}")
딱 봐도 안좋아보인다. ORM을 사용했다면 이런 문제는 없었을 것이다.
또한 참 애매하게 많이 쓰이는 쿼리가 많았다. 완전히 같은 쿼리라면 함수로 빼서 재사용이라도 하겠는데 WHERE조건이 하나 다르다든지 JOIN조건이 다르다든지 해서 재사용하기 힘들었다. 그래서 함수로 빼지도 못하고 통째로 API에 들어가서 코드 길이도 길어지고 비즈니스 로직과 Persistence 레이어가 View(JAVA에서의 Controller)에 쌩으로 들어있었다. 덕분에 API하나가 100줄을 웃도는 상황이 발생했다. 물론 내 지식이 아직 부족하여 그렇지 Raw쿼리도 위 문제를 해결할 수 있는 방법이 있었을 것이다.
MSA에서 데이터베이스 설계하기
이건 뭔가 잘못되고 있다는 것을 느끼고 ORM을 다시 고려하게 되었다. 2번 문제는 어떻게든 열심히 처리하면 내가 힘들겠지만 해결되긴 한다. 1번 문제를 어떻게 해결할지 몰라서 고민을 좀 하다가 다른 팀과 지인에게 관련해서 어떻게 처리하는지 물어봤다.
아 우리는 MSA로 쪼개진 서비스마다 데이터베이스가 나눠져있어~
왼쪽이 아니라 오른쪽 모양이었던 것이다. (DB 인스턴스가 분리될 필요는 없다) 이러면 1번 문제도 해결된다. 그럼 이제 궁금한 점이 생긴다.
Q. 서버 2에서 DB 1에 있는 데이터에 접근하고 싶으면 어떡하지?
A. 서버 1에서 필요한 데이터를 제공하는 API를 열어 놓고 API 통신을 한다!
Q. JOIN연산은?
A. api로 가져온 데이터로 where검색하면 JOIN한 효과가 나지!
Q. 필요한 데이터가 생길때마다 API 만들어야됨?
A. 그래야죠..
Q. 그럼 서버 1이 터지면 우짬?
A. 유감..
실제로 느슨한 결합에 위배된다. 그럼 다음과 같은 방법을 제시할 수 있다.
DB1에서 특별히 많이 사용되는 테이블만 뽑아서 DB2에 추가하자!
그렇게 DB1과 DB2에서 테이블 A를 공유한다고 하자. 그러면 서버 2에서도 DB 1에 있는 테이블 A에 접근할 수 있게 된다. 하지만 R은 상관 없지만 CUD할 때 DB1, DB2에 모두 적용해야 한다. 둘 중 하나라도 실패하면? 나머지 하나도 실패해야 한다. 해당 기능을 수행하는 것이 분산 트랜잭션인데 여기에 복잡한 분산 트랜잭션을 넣는다? 글쎄... 아니면 Event Driven으로 해서 CQRS패턴을 적용하면 된다는데 이는 굉장히 어렵다고 한다.
다행히 현재 사용중인 PostgreSQL에서 CDC라는 기능을 지원해서 이런 문제는 해결할 수 있었다.
위 방법은 테이블을 공유해서 1번 문제는 여전히 남아 있었지만 그나마 해결이 가능했고 선택지 중 하나가 되었다.
공유 모듈 사용하기
또 다른 방법으로 나온 것은 공유 모듈이다. JAVA의 NEXUS같은 게 파이썬에도 있냐고 하던데 용어가 생소해서 처음에는 이해가 되지 않았다. 그래서 "오~ 자바 저런거도 돼? 엄청나네?"라는 반응을 했는데 알고보니 PyPI였다. ㅋㅋ PyPI에 private한 모듈을 올려서 모든 프로젝트마다 pip로 설치하여 사용하는 것이다. 그러면 이제 ORM 스키마를 공유 모듈에 작성하고 업로드 한 후 각 프로젝트마다 외부 라이브러리마냥 install해서 import하여 사용하면 된다. 컬럼이 변경되면? 해당 패키지를 업데이트하기만 하면 된다.
1번 문제를 깔끔하게 해결할 수 있게 된다.
결론
깔끔하게 해결이 되는 공유 모듈을 사용하여 시도할 것 같다. 이미 raw쿼리를 사용하여 완성된 상태기 때문에 갈아엎어야한다. CDC를 사용하는 것도 좋은 방법이었지만 아래의 이유로 탈락했다.
- 해당 방법을 사용하기에 프로젝트 규모가 작다.
- 테이블이 생길 때마다 어떤 데이터베이스에 넣을지 고민해야된다.
해당 문제를 해결하기 위해 많은 검색과 고민을 하니 깨달음을 얻고 실력이 조금 늘어난 것 같다.
'프로그래밍 > 개발' 카테고리의 다른 글
[개발] 주니어 개발자의 우당탕탕 MSA 전환기 - Nexus 편 (0) | 2023.12.13 |
---|---|
[개발] RDB에서 Incremental PK와 UUID PK (2) | 2023.12.05 |
[개발] Redis를 사용한 데이터 캐싱으로 조회 API성능 향상시키기 (0) | 2023.07.29 |
[개발] VPC 개념잡기 (0) | 2023.07.27 |
[개발] AWS 아키텍처 구성하기 (4) | 2023.06.09 |