Odoo에서 순환 재계산 및 반올림 오차
I’m unable to retrieve the contents of external webpages directly. Could you please paste the text you’d like translated (excluding the source link you’ve already provided)? Once I have the article’s content, I’ll translate it into Korean while preserving the original formatting, markdown syntax, and any code blocks or URLs.
실제 문제
ERP 시스템에서는 동일한 값을 서로 다른 단위로 나타내는 두 개의 필드가 흔히 존재합니다—예를 들어, 회사 통화 기준 구매 가격과 동일한 가격을 USD 로 표시하는 경우입니다. 두 필드 모두 편집 가능하며 동기화되어야 합니다.
단순화를 위해 변환 로직을 다음과 같이 가정해 보겠습니다:
purchase_price_usd = round(purchase_price / rate, 2)
purchase_price = round(purchase_price_usd * rate, 2)
여기서 사용된 수식은 의도적으로 단순화된 것입니다. 실제 시스템에서는 적절한 통화 유틸리티, 정밀도 처리 및 프레임워크 헬퍼를 사용합니다. 이 예제의 목표는 재계산 문제를 보여주는 것이지, 통화 구현 세부 사항을 다루는 것이 아닙니다.
기본 사례
rate = 3- 사용자가
purchase_price = 10을 입력
시스템은 다음과 같이 계산합니다:
purchase_price_usd = round(10 / 3, 2) = 3.33
purchase_price = round(3.33 * 3, 2) = 9.99
이제 원래 값이 10에서 9.99로 변합니다. 수학적으로는 올바르지만, 사용자 관점에서는 그렇지 않습니다. 사용자는 10을 입력했으며, 시스템이 이를 조용히 바꾸어서는 안 됩니다.
- 이것은 부동소수점 버그가 아닙니다.
- 이것은 반올림 버그가 아닙니다.
- 이것은 다음을 결합한 결과입니다:
- 양방향 필드 파생
- 반올림
- 자동 재계산
…그리고 여기서 순환 재계산이 사용자의 의도를 깨뜨리기 시작합니다.
Source: …
왜 이런 현상이 발생하는가: Onchange 루프
Odoo에서 편집 가능한 폼은 @api.onchange에 크게 의존합니다. 사용자가 UI에서 필드를 수정하면:
- 프런트엔드가 변경된 값을 서버에 전송합니다.
- Odoo가 임시 레코드 스냅샷을 생성합니다.
- 수정된 필드가 “변경됨”으로 표시됩니다.
- 모든 관련
@api.onchange및@api.depends로직이 실행됩니다. - 그 과정에서 추가 필드가 변경되면 Odoo가 또 다른 패스를 실행합니다.
이 과정은 더 이상 “변경된” 필드가 없을 때까지 계속됩니다. 이 동작은 올바르고 의도된 것으로, 종속 필드 간의 일관성을 보장합니다.
하지만 다음과 같은 양방향 설정에서는:
purchase_price → updates purchase_price_usd
purchase_price_usd → updates purchase_price
메커니즘이 대칭적으로 작동합니다.
- 사용자가
purchase_price를 변경하면:purchase_price_usd가 재계산됩니다.- 그 재계산 과정에서
purchase_price가 다시 수정됩니다. - 시스템이 변화를 감지하고 → 또 다른 패스가 시작됩니다.
차이가 단지 반올림 조정이라 할지라도, 이는 여전히 “변경”으로 간주됩니다. 프레임워크는 어느 필드가 사용자의 원래 의도를 나타내는지 알 수 없으며, 단지 값이 다르다는 사실만 보고 동기화를 시도합니다. 이렇게 해서 순환 재계산이 발생합니다.
라운딩: 숨겨진 증폭기
원형 재계산만으로는 항상 눈에 보이지 않습니다. 실제 문제는 라운딩이 개입될 때 명확해집니다.
양방향 변환에서는 암묵적으로 다음을 가정합니다:
f⁻¹(f(x)) = x
하지만 라운딩이 적용되면 이는 더 이상 성립하지 않습니다. 소수점 둘째 자리까지 라운딩할 경우:
round(round(x / rate, 2) * rate, 2) ≠ x
0.01 차이조차도 실제 변화로 감지됩니다:
- 프레임워크 관점에서는: 값이 다르므로 → 동기화가 필요합니다.
- 사용자 관점에서는: 입력한 값이 다시 쓰여졌습니다.
라운딩은 루프를 생성하는 것이 아니라 관찰 가능하게 만들 뿐입니다. 라운딩이 없으면 아주 작은 부동소수점 차이가 존재할 수 있지만, UI 수준에서는 종종 보이지 않습니다. 라운딩을 하면 그 차이가 명시적으로 드러나고, 사용자의 의도가 무시됩니다. 이것이 수학적 정확성이 실제 ERP 동작과 충돌하는 지점입니다.
일반적인 접근 방식이 해결되지 않는 이유
@api.depends와 inverse를 사용하여 양방향 동기화를 구현하는 것이 완전히 가능합니다. 많은 Odoo 모듈이 역방향 메서드 내부에서 컨텍스트 플래그를 사용해 재귀적인 쓰기를 방지합니다. 데이터베이스 수준에서는 이 접근 방식이 신뢰성 있게 작동합니다.
실제 제한은 UI 수준에서 나타납니다.
compute + inverse는 레코드가 쓰기될 때 일관성을 보장하지만, 사용자가 폼을 편집하는 동안 즉각적인 동기화를 보장하지 않습니다.
폼 뷰에서는 업데이트가 onchange를 통해 이루어집니다:
- Odoo는 레코드의 임시 스냅샷을 생성합니다.
- 변경된 필드를 표시합니다.
- 관련
onchange와 compute 로직을 실행합니다. - 어떤 필드가 수정되었는지 확인합니다.
- 필요시 추가 패스를 실행합니다.
이 모든 과정은 단일 onchange 실행 사이클 내에서 이루어집니다—새로운 요청도, 새로운 평가 컨텍스트도 없습니다. 동일한 레코드 스냅샷이 더 이상 변경 사항이 감지되지 않을 때까지 반복적으로 처리됩니다.
두 필드가 서로를 업데이트하고 반올림으로 인해 아주 작은 차이라도 발생하면, 프레임워크는 이를 실제 수정으로 인식하고 루프가 계속됩니다. 따라서 compute + inverse가 쓰기 시점에 값의 일관성을 유지할 수는 있지만, UI‑수준의 순환 재계산 문제를 해결하지 못합니다.
폼 편집 중 사용자의 의도를 보존하려면, 방향을 onchange 사이클 내부에서 직접 제어해야 합니다.
Source: …
온체인지 사이클 내부에서 방향 제어
핵심 인사이트: 언제든지 사용자 의도를 나타내는 필드는 하나뿐입니다.
- 사용자가
purchase_price를 편집하면, 그 값이 권위 있는 값으로 취급되어야 합니다. - 동기화된 필드(
purchase_price_usd)는 업데이트되어야 하지만, 같은 사이클 내에서는 원본 필드를 다시 건드리지 않아야 합니다.
반대의 경우도 마찬가지입니다.
onchange 루프는 두 필드를 대칭적으로 처리합니다—값 차이만을 보고 두 값을 맞추려 합니다. 이 대칭성을 깨기 위해서는 다음이 필요합니다:
- 어떤 필드가
onchange를 트리거했는지 감지한다. - 동기화 방향을 나타내는 컨텍스트 플래그를 설정한다.
- 같은 사이클 내에서 역방향 업데이트를 방지한다.
전파 방향을 명시적으로 제어함으로써, 반올림으로 인한 무한한 앞뒤 움직임을 막고 사용자가 실제로 입력한 값을 보존할 수 있습니다.
예시 (단순화)
def onchange(self, values, field_names, fields_spec):
ctx = {}
if "purchase_price" in field_names:
ctx["skip_purchase_price_recompute"] = True
if "purchase_price_usd" in field_names:
ctx["skip_purchase_price_usd_recompute"] = True
self = self.with_context(**ctx)
return super().onchange(values, field_names, fields_spec)
대응되는 compute 메서드
@api.depends("purchase_price")
def _compute_purchase_price_usd(self):
if self.env.context.get("skip_purchase_price_usd_recompute"):
return
for record in self:
record.purchase_price_usd = ...
이는 수학적 모델을 바꾸는 것이 아니라 실행 모델을 바꾸는 것입니다.
두 필드가 서로 우위를 다투게 두는 대신, 특정 사용자 행동에 대해 어느 쪽이 진실의 원천인지 명시적으로 정의합니다.
반올림은 여전히 존재하고, 공식은 대칭을 유지하지만 재계산이 더 이상 순환하지 않습니다.
사용자 의도가 보존됩니다.
결론
양방향 필드 동기화와 반올림을 결합하면 수학적 가역성이 필연적으로 깨집니다.
- 프레임워크는 잘못되지 않았습니다.
- 수식은 잘못되지 않았습니다.
문제는 UI 재계산 중 두 필드를 동등하게 권위 있는 것으로 취급하는 데서 발생합니다.
Odoo의 onchange 사이클에서는 감지된 차이가 있으면 또 다른 평가 패스를 트리거합니다. 반올림이 포함되면 아주 작은 편차조차 실제 변경으로 간주됩니다.
사용자의 의도를 보존하는 유일한 신뢰할 수 있는 방법은 방향을 명시적으로 제어하는 것입니다.
어느 순간이든:
- 하나의 필드를 진실의 원천으로 간주해야 합니다.
- 다른 필드는 파생되어야 합니다.
- 같은 평가 사이클 내에서 역방향 재계산을 억제해야 합니다.
이는 우회책이 아니라 UI‑주도 시스템에서 양방향 파생 필드의 구조적 제약입니다.