계산된 필드가 무한 재계산을 일으킴(Odoo)
Source: Dev.to
(번역을 원하는 본문을 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다.)
Problem Statement
잘못 설계된 계산된 필드는 잘못된 의존성 선언(@api.depends) 때문에 불필요한 재계산이나 무한 루프를 유발하여 성능 문제와 불안정한 동작을 초래합니다.
왜 이 문제가 Odoo에서 발생하는가
- 계산된 필드가 자신에게 의존한다
- compute 메서드가 다른 필드에 쓰기를 수행한다
write()가 compute 메서드 내부에서 사용된다- 의존성이 너무 넓거나 잘못되었다
store=True가 잘못 사용된다- 부모 ↔ 자식 필드가 서로 의존한다
결과
- 느린 폼
- 높은 CPU 사용량
- UI 정지
- 운영 중 무작위 충돌
Step 1 – Identify the Problematic Computed Field
Common Symptoms
- Form view keeps loading → 폼 뷰가 계속 로드됩니다
- Server CPU spikes → 서버 CPU 급증
- Logs show repeated “Computing field …” messages → 로그에 반복되는 “Computing field …” 메시지가 표시됩니다
- Issue disappears when the field is removed → 필드를 제거하면 문제가 사라집니다
Enable logs:
--log-level=debug
Look for:
Computing field
repeating continuously.
단계 2 – 일반적인 WRONG 패턴 식별
WRONG 1: 필드가 자신에게 의존
@api.depends('total')
def _compute_total(self):
for rec in self:
rec.total = rec.price * rec.qty
무한 재계산을 유발합니다.
WRONG 2: Compute 안에서 다른 필드 쓰기
@api.depends('price', 'qty')
def _compute_amount(self):
for rec in self:
rec.amount = rec.price * rec.qty
rec.discount = rec.amount * 0.1 # BAD
각 쓰기 작업이 다시 재계산을 트리거합니다.
WRONG 3: Compute 안에서 write() 사용
@api.depends('line_ids.amount')
def _compute_total(self):
for rec in self:
rec.write({'total': sum(rec.line_ids.mapped('amount'))})
재귀와 DB 쓰기를 발생시킵니다.
Step 3 – 올바르고 최소한의 종속성 정의
올바른 종속성 선언
@api.depends('price', 'qty')
def _compute_total(self):
for rec in self:
rec.total = rec.price * rec.qty
규칙
- 원본 필드에만 의존
- 계산된 필드 자체를 포함하지 않음
@api.depends('*')사용 금지
Step 4 – Compute 메서드 순수하게 유지 (부작용 없음)
올바른 패턴
total = fields.Float(compute='_compute_total', store=True)
@api.depends('price', 'qty')
def _compute_total(self):
for rec in self:
rec.total = rec.price * rec.qty if rec.price and rec.qty else 0.0
- 쓰기 없음
- ORM 호출 없음
- 안전하고 예측 가능
Step 5 – 논리를 여러 계산 필드로 분리
좋은 설계
amount = fields.Float(compute='_compute_amount', store=True)
discount = fields.Float(compute='_compute_discount', store=True)
@api.depends('price', 'qty')
def _compute_amount(self):
for rec in self:
rec.amount = rec.price * rec.qty
@api.depends('amount')
def _compute_discount(self):
for rec in self:
rec.discount = rec.amount * 0.1
- 순환 의존성 없음
- 깔끔한 분리
Step 6 – store=True를 반드시 필요할 때만 사용
BAD
total = fields.Float(compute='_compute_total', store=True)
store=True는 다음 경우에만 사용하세요:
- 검색 도메인에서 사용될 때
- 보고서 /
group by에서 사용될 때
GOOD
total = fields.Float(compute='_compute_total')
필드가 UI 전용이며 검색이나 그룹화에 사용되지 않을 때.
단계 7 – UI 로직에 @api.onchange 사용 (계산이 아님)
잘못된 예
@api.depends('qty')
def _compute_price(self):
for rec in self:
rec.price = rec.qty * 10
올바른 예
@api.onchange('qty')
def _onchange_qty(self):
self.price = self.qty * 10
- 재계산 없음
- UI 전용 동작
Step 8 – 부모‑자식 의존성 루프 수정
BAD (숨겨진 루프)
@api.depends('line_ids.total')
def _compute_total(self):
for rec in self:
rec.total = sum(rec.line_ids.mapped('total'))
line.total이 부모에 의존하면 무한 루프가 발생합니다.
SAFE VERSION
@api.depends('line_ids.price', 'line_ids.qty')
def _compute_total(self):
for rec in self:
rec.total = sum(
line.price * line.qty for line in rec.line_ids
)
- 직접 의존성
- 재귀 없음
Step 9 – 계산된 필드 대신 제약 조건을 사용하여 검증하기
BAD
@api.depends('qty')
def _compute_check(self):
if self.qty < 0:
raise ValidationError("Invalid")
GOOD
@api.constrains('qty')
def _check_qty(self):
for rec in self:
if rec.qty < 0:
raise ValidationError("Quantity cannot be negative")
- 재계산 없음
- 적절한 검증 레이어
Step 10 – 최종 안전 템플릿 (모범 사례)
total = fields.Float(
compute='_compute_total',
store=True,
)
@api.depends('price', 'qty')
def _compute_total(self):
for rec in self:
rec.total = (rec.price or 0.0) * (rec.qty or 0.0)
- 순수 계산
- 올바른 의존성
- 프로덕션 안전
결론
Odoo 개발에서 무한 재계산 문제는 거의 항상 데이터를 쓰는 순수하지 않은 계산 필드, 자신에게 의존하는 필드, 혹은 비즈니스 로직과 계산 로직을 혼합한 경우에 발생합니다. 해결책은 엄격한 규율입니다: 순수한 compute 메서드, 최소한의 의존성, ORM 쓰기 금지, UI 로직과 검증 로직의 적절한 분리. 계산 필드를 읽기 전용 계산으로 다루면 Odoo의 ORM은 규모가 커져도 빠르고 안정적이며 예측 가능하게 유지됩니다.