Odoo를 ‘오픈소스 ERP’ 한 줄로 받아들이고 레포를 열면 인상이 어긋난다. 루트의 addons/ 디렉터리 아래에는 sale, purchase, account, stock, hr, mrp, point_of_sale, website 같은 폴더가 수백 개 쌓여 있고, 각 폴더는 __manifest__.py 한 파일로 자기 자신을 선언한다. depends에 부모 모듈을 적고, data에 뷰·메뉴·보안 규칙을 묶고, application 플래그로 UI ‘앱’으로 노출할지 정한다. 즉 Odoo는 거대한 ERP 하나가 아니라, 패키지 매니저로 묶인 수백 개의 작은 앱이다.
모델 레이어는 이 구조를 더 흥미롭게 만든다. base 모듈이 res.partner를 정의하면 account가 _inherit로 회계 필드를 붙이고, sale이 또 영업 필드를 얹는다. 같은 PostgreSQL 테이블 위에 모듈별 칼럼이 수평으로 누적되는 horizontal extension 패턴이라, 어떤 앱을 켜뒀느냐에 따라 같은 res.partner의 실제 스키마가 인스턴스마다 달라진다. 뷰도 마찬가지로 xpath inherit으로 다른 모듈의 화면에 필드 한 칸을 ‘끼워 넣는’ 식이라, 한 화면이 보통 여러 모듈의 합성 결과다.
이게 장점이자 비용이다. 장점은 회계만 도입한 회사도, 제조·재고·HR까지 묶어 쓰는 회사도 같은 코드베이스 위에 올라탄다는 것. 비용은 업그레이드에서 드러난다. odoo-bin -u <module>은 단일 모듈을 건드리는 것처럼 보이지만, 실제로는 의존 트리 위의 migrations/<version>/*.py 스크립트를 버전 순서대로 실행한다. 게다가 이 repo는 Community 에디션이고 Enterprise는 별도 repo다. 같은 이름의 enterprise 모듈이 community 버전을 덮어쓰는 일이 잦아, 커스텀을 잘못 얹어두면 업그레이드 한 번에 회계 잔액까지 흔들릴 수 있다.
그러니 별 48개가 하루에 박힌 이 저장소에서 봐야 할 것은 기능 카탈로그가 아니라 세 군데다. addons/<module>/__manifest__.py로 ‘무엇이 무엇을 끌어오는가’를 읽고, 같은 모듈의 migrations/ 폴더로 ‘업그레이드 비용’을 읽고, _inherit으로 확장된 모델 체인을 따라가며 ‘우리 인스턴스의 스키마가 어떻게 합성될지’를 본다. ERP는 도입보다 운영의 시간이 훨씬 길고, Odoo의 설계는 그 시간을 정직하게 비용으로 청구하는 쪽이다.