소개
Odoo에서 판매 주문서를 열어 고객 정보 아래에 줄 단위로 나열된 품목 목록을 본 적이 있다면, 그 화면에서 One2many 필드가 동작하는 모습을 이미 목격한 셈입니다. 이 기능은 Odoo 데이터 모델의 핵심 구성 요소 중 하나로, 시스템을 맞춤화하거나 개발할 때 반드시 이해해야 하는 개념입니다.
이 문서는 One2many 필드의 개념과 Odoo ORM에서의 동작 방식, 언제 이를 사용해야 하는지, 그리고 Odoo Studio 또는 파이썬 코드로 직접 생성·설정하는 방법까지 실무 중심으로 안내합니다.
데이터 구조가 궁금한 비즈니스 사용자든, 현장 적용을 위한 필드 구현 방법을 찾는 개발자든 이 글은 One2many 필드 사용에 필요한 실무 정보를 제공합니다.
Odoo의 One2many 필드란 무엇인가
One2many는 Odoo 프레임워크의 관계형 필드 유형입니다. 하나의 부모 레코드가 다른 모델의 여러 자식 레코드와 연결되도록 허용합니다.
쉽게 말하면 한 고객이 여러 청구서를 가질 수 있고, 한 판매 주문이 여러 주문 항목을 포함할 수 있는 구조입니다. 부모 레코드는 One2many 필드를 통해 관련된 모든 자식 레코드를 참조합니다.
사용자 화면에서는 보통 폼 내부에 테이블 형태로 내장된 목록으로 표시됩니다. 이 인라인 리스트에서 사용자는 부모 폼을 벗어나지 않고 자식을 추가·수정·삭제할 수 있어 사용자 경험 면에서 매우 자주 쓰이는 구성입니다.
다른 관계형 필드들과의 차이점
Odoo ORM에는 주로 세 가지 관계형 필드 유형이 있습니다:
- Many2one: 한 레코드가 다른 모델의 단일 레코드를 가리킵니다(예: 판매주문에서 고객을 가리키는 경우).
- One2many: 한 레코드가 다른 모델의 여러 레코드와 연결됩니다(예: 판매주문과 여러 주문 라인).
- Many2many: 양쪽 모두 여러 레코드가 서로 연결되는 경우입니다(예: 제품과 다수의 태그가 서로 연결되는 관계).
One2many는 자식 레코드가 오직 하나의 부모에 속해야 하는 상황에 적합합니다. 만약 자식이 여러 부모에 공유되어야 한다면 Many2many를 선택해야 합니다.
One2many의 특징 중 하나는 자체적으로 데이터 칼럼을 저장하지 않는다는 점입니다. 이 필드는 가상(virtual) 필드로, 실제 데이터는 자식 모델의 Many2one 필드를 통해 읽어옵니다.
필드의 동작 원리
실제 데이터베이스 관점에서는 부모 테이블에 별도 컬럼을 추가하지 않습니다. 자식 모델 테이블 쪽에 외래키 컬럼이 존재하며 그 컬럼이 부모 레코드의 ID를 가리킵니다.
Odoo ORM의 핵심 원칙은 모든 One2many 필드가 관련 자식 모델에 대응하는 Many2one 필드를 가진다는 것입니다. One2many는 사실상 역방향 조회(reverse lookup)이며, ORM은 자식의 Many2one 값이 현재 부모 ID와 일치하는 모든 레코드를 찾아 표시합니다.
One2many와 Many2one의 관계 정의
파이썬에서 One2many 필드를 정의할 때는 두 가지 주요 인수를 지정합니다:
- comodel_name: 자식 레코드를 담고 있는 모델 이름.
- inverse_name: 자식 모델에서 부모를 가리키는 Many2one 필드 이름.
예를 들어, 서비스 계약(Service Contract)이 여러 납품 항목(Deliverable)을 가지는 관계를 정의하면 다음과 같은 구조가 됩니다.
deliverable_ids = fields.One2many(
comodel_name='service.deliverable',
inverse_name='contract_id',
string='Deliverables'
)
위 예에서 contract_id는 service.deliverable 모델에 정의된 Many2one 필드입니다. 이 Many2one이 먼저 존재하지 않으면 One2many는 정상 작동하지 않습니다.
데이터베이스에서 실제로 일어나는 일
One2many 필드의 데이터는 부모 테이블이 아닌 자식 모델의 테이블에 저장됩니다. 각 자식 행(row)이 부모 레코드의 ID를 가리키는 외래키 컬럼을 포함합니다.
Odoo가 부모 레코드를 로드해 One2many를 표시할 때는 자식 테이블에서 외래키가 현재 부모 ID와 일치하는 모든 행을 조회하는 SQL 쿼리를 실행합니다. 이는 관계형 DB의 표준 동작이며 Odoo ORM이 이를 자동으로 처리합니다.
따라서 One2many는 데이터베이스 칼럼을 추가하지 않는 계산된 뷰(computed view)로 간주됩니다. 실제 데이터는 자식 모델에 보관됩니다.
비즈니스 활용 사례
One2many는 현실의 부모-자식 관계를 자연스럽게 모델링하기 때문에 Odoo 전반에 널리 쓰입니다. 다음은 흔히 마주치는 실제 업무 사례 다섯 가지입니다.
1. 판매주문과 주문 라인
판매(Sales) 모듈에서는 sale.order 모델에 order_line_ids라는 One2many 필드로 sale.order.line을 연결합니다. 각 주문 라인에는 제품, 수량, 단가, 할인 등이 저장되며 사용자는 판매 주문서 내에서 라인을 바로 추가하거나 제거할 수 있습니다.
2. 송장과 송장 항목
회계(Accounting)에서는 account.move가 One2many로 account.move.line을 관리합니다. 각 항목은 청구 대상 제품 또는 서비스를 나타내며 부모 송장은 자식 항목들의 합계를 집계합니다.
3. 프로젝트와 작업
프로젝트(Project) 모듈은 project.project에 One2many로 해당 프로젝트의 모든 작업(Task)을 표시합니다. 폼 뷰에서는 리스트, 칸반, 간트 차트 등 다양한 방식으로 같은 연결 관계를 시각화할 수 있습니다.
4. 거래처와 하위 연락처
res.partner 모델의 child_ids One2many는 회사에 연결된 개별 연락처들을 나열합니다. 회사 레코드를 열면 직원이나 담당자들이 탭으로 표시되며, 각 하위 연락처는 부모를 가리키는 parent_id Many2one을 가집니다.
5. 맞춤형 모델(서비스·생산 등)
커스텀 모듈을 설계할 때도 One2many는 자연스러운 선택입니다. 예: 유지보수 요청에 사용된 부품 목록, 교육 과정의 여러 세션 날짜, 서비스 계약의 납품 항목 목록 등 다양한 시나리오에서 활용됩니다.
이러한 유연성 때문에 One2many는 산업별 맞춤 데이터 모델을 설계할 때 핵심 역할을 합니다.
필드 생성 및 커스터마이징 방법
One2many 필드를 추가하는 방법은 크게 두 가지입니다. 코드 없이 구성할 수 있는 Odoo Studio 방식과 개발자가 직접 파이썬으로 정의하는 방식입니다.
Odoo Studio로 만들기
Studio에서는 부모 모델의 필드 선택기에서 바로 One2many를 생성하지 못하는 경우가 많습니다. 그 이유는 One2many가 항상 자식 모델 쪽의 Many2one 존재를 전제로 하기 때문입니다.
Studio에서 권장되는 절차는 다음과 같습니다:
- 먼저 자식 모델을 열어 부모를 가리키는 Many2one 필드를 추가합니다.
- Many2one을 저장한 뒤 부모 모델로 돌아갑니다.
- 새 필드를 추가할 때 타입으로 "One2many"를 선택하고, 관련 모델과 inverse(역참조) 필드를 지정합니다.
- 그러면 폼 뷰 안에 인라인 목록 형태로 One2many 필드가 나타납니다.
많은 비즈니스 사용자는 이 방법으로 코드 작업 없이 관계를 구성할 수 있으며, Studio로 생성한 필드는 파이썬 정의와 동일하게 동작합니다.
파이썬 코드로 커스터마이즈하기
개발자는 파이썬으로 One2many를 정의하면 동작 제어, 필드명, 도메인 등 세부 설정을 자유롭게 다룰 수 있습니다. 예시는 다음과 같습니다.
from odoo import fields, models
class ServiceContract(models.Model):
_name = 'service.contract'
_description = 'Service Contract'
name = fields.Char(string='Contract Name', required=True)
deliverable_ids = fields.One2many(
comodel_name='service.deliverable',
inverse_name='contract_id',
string='Deliverables'
)
class ServiceDeliverable(models.Model):
_name = 'service.deliverable'
_description = 'Service Deliverable'
contract_id = fields.Many2one(
comodel_name='service.contract',
string='Contract',
ondelete='cascade'
)
name = fields.Char(string='Description', required=True)
포인트는 자식 모델에 먼저 Many2one(contract_id)이 정의되어야 하고, One2many의 inverse_name은 그 필드명과 정확히 일치해야 한다는 것입니다.
XML-RPC API로 필드 생성하기
여러 환경에 걸쳐 프로그래매틱하게 Odoo 필드를 관리해야 한다면 ir.model.fields를 통해 XML-RPC로 One2many를 생성할 수도 있습니다. 규칙은 동일합니다: 먼저 Many2one을 만들고, 그 필드명을 relation_field에 지정해 One2many를 정의합니다.
이 방식은 여러 인스턴스에 걸친 필드 배포나 자동화된 커스터마이징 시나리오에서 유용합니다.
권장 실무(베스트 프랙티스)
다양한 산업의 Odoo 도입 프로젝트를 수행한 경험을 바탕으로, One2many를 다룰 때 실무에서 효과를 주는 핵심 권장사항을 정리합니다.
- 항상 먼저 Many2one을 생성하세요. One2many는 역참조 구조이므로 자식 모델의 Many2one이 먼저 있어야 합니다. Studio, 코드, API 어느 방식이든 동일하게 적용됩니다.
- 필드명은 관례에 따라
_ids접미사를 사용하세요. 예:line_ids,task_ids,deliverable_ids. 이렇게 하면 필드가 레코드셋을 반환한다는 점이 즉시 명확해집니다. - 자식 레코드를 부모 삭제 시 함께 삭제하려면 Many2one에
ondelete='cascade'를 설정하세요. 고아 레코드(orphan)를 방지할 수 있습니다. - 인라인 리스트는 핵심 컬럼만 노출하세요. 너무 많은 컬럼을 한 화면에 보여주면 성능 저하와 가독성 문제가 생깁니다. 보조 정보는
optional속성으로 토글 가능하게 하세요. - 도메인(domain) 필터로 표시되는 자식 레코드를 제한하세요. 자식 모델이 여러 부모에서 공유되는 경우 특정 조건으로 보이는 항목을 좁히는 것이 유용합니다.
- 대량 데이터는 신중히 다루세요. 부모 레코드에 수천 개의 자식이 연결될 수 있다면 한 번에 모두 로드하지 마시고, 별도 리스트 뷰로 전환하거나 페이지네이션을 도입하세요.
자주 발생하는 실수
One2many 필드 작업 시 자주 접하는 실수들
역참조인 Many2one을 잊음
가장 흔한 오류는 자식 모델에 대응하는 Many2one을 먼저 만들지 않고 One2many를 정의하려다 발생합니다. Odoo는 inverse 필드가 없으면 에러를 냅니다. inverse_name이 자식 모델에 실제로 존재하는지 항상 확인하세요.
잘못된 쓰기(write) 문법 사용
One2many를 파이썬 코드나 API로 갱신할 때는 Odoo의 명령 튜플(command tuples) 문법을 사용해야 합니다. 일반 파이썬 리스트를 할당하면 기대한 대로 동작하지 않습니다.
(0, 0, values_dict)— 새 자식 레코드 생성(1, child_id, values_dict)— 기존 자식 레코드 업데이트(2, child_id, 0)— 기존 자식 레코드 삭제(4, child_id, 0)— 이미 존재하는 자식 레코드를 관계에 추가(5, 0, 0)— 부모와의 연결만 해제(자식은 삭제하지 않음)
예시처럼 record.line_ids = [1, 2, 3]처럼 단순 리스트를 쓰면 ORM과 충돌하므로 피해야 합니다.
One2many와 Many2many 혼동
One2many는 자식 레코드가 정확히 하나의 부모에 속할 때 쓰는 모델입니다. 자식이 여러 부모에 동시 연결되어야 한다면 Many2many가 올바른 선택입니다. One2many로 처리하면 레코드 중복 및 데이터 무결성 문제를 초래할 수 있습니다.
대량 자식 목록에서의 성능 문제
One2many가 수백~수천 줄을 폼 뷰에 표시하면 로딩 속도가 크게 떨어집니다. 회계의 송장 항목이나 재고의 이동 라인에서 자주 발생합니다. 리스트 뷰에 limit를 두거나 대량 데이터는 별도 뷰로 안내하세요.
부모 삭제 후 고아 레코드 발생
부모를 삭제할 때 자식의 Many2one에 ondelete='cascade'를 설정하지 않으면 자식 레코드가 부모 참조 없이 남아 데이터베이스를 어지럽힐 수 있습니다. 명확한 삭제 정책을 설계하세요.
요약 및 정리
One2many 필드는 Odoo ORM과 데이터 모델의 기초적인 구성 요소입니다. 판매주문의 라인, 송장 항목, 프로젝트 작업 등 핵심 기능 대부분이 이 관계를 통해 동작합니다. 자식 모델의 Many2one과의 관계를 이해하면 Odoo 내부 구조를 읽고 확장하는 일이 훨씬 쉬워집니다.
Odoo를 설정하든 Studio로 커스터마이즈하든, 혹은 모듈을 직접 개발하든 One2many는 자주 사용하게 될 도구입니다. 언제 사용해야 하는지, 올바르게 정의하는 방법, 흔한 실수를 피하는 방법을 알면 시간과 품질 면에서 큰 이득을 얻을 수 있습니다.
더 많은 Odoo 필드 튜토리얼과 기술 가이드를 원하신다면 Odoo Data & API 컬렉션의 다른 글들을 참고하세요.
Odoo 도입 지원이 필요하신가요?
Dasolo는 기업이 Odoo를 도입·커스터마이즈·최적화하도록 지원합니다. 데이터 모델 설계, 맞춤 모듈 개발, 필드 생성, 기존 설정의 효율화 등 비즈니스 요구에 맞는 솔루션을 제공합니다.
Odoo 프로젝트에 대한 질문이 있거나 데이터 구조를 어떻게 설계할지 논의하고 싶으시다면, 문의해 주세요— 기쁜 마음으로 도와드리겠습니다.