산란율이 40%로 나왔는데 농장에서는 80%라고 했다
산란계 농장 ERP를 만들다 산란율이 실제의 절반으로 나왔습니다. 문법도 로직도 멀쩡했고, 틀린 건 분모였습니다. 알을 낳지 않는 육성동 마릿수가 거기 들어가 있었습니다.

낮에는 회사에서 일하고, 밤에는 코드를 만지고, 주말에는 부엌에 있습니다. 겪은 것을 정리해 두는 곳입니다.
목차
테스트는 전부 통과했고 화면에도 숫자가 잘 찍혔습니다. 그런데 농장에 보여 드렸더니 첫마디가 "이 숫자는 아닌데요"였습니다. 산란율이 40%로 나와 있었는데 실제로는 80% 언저리라고 하셨습니다. 절반이면 오차가 아니라 완전히 다른 값입니다.
산란계 농장 관리 프로그램을 만들면서 겪은 일입니다. 회사에서 처음 만든 수발주 프로그램 다음에 손댄 것이라, 코드에는 어느 정도 자신이 있던 때였습니다.
결과만 보면 한 줄짜리 수정이었습니다. 다만 그 한 줄을 찾기까지, 제가 아는 산란율과 농장에서 쓰는 산란율이 다른 말이라는 걸 먼저 알아야 했습니다.
코드에는 버그가 없었습니다
산란율은 낳은 알 수를 닭 마릿수로 나눈 값입니다. 계산식은 단순합니다. 알 8,000개를 닭 20,000수로 나누면 40%가 맞습니다. 파이썬도 맞고 데이터베이스 값도 맞았습니다.
틀린 건 20,000이라는 숫자였습니다. 그 농장은 계사가 두 동인데, 한 동은 알을 낳는 산란동이고 다른 한 동은 아직 알을 낳지 않는 육성동이었습니다. 저는 농장에 있는 닭을 전부 더해서 분모에 넣었습니다.
처음에는 입력이 잘못됐나 싶어 그날 일보를 하나씩 열어 봤습니다. 알 수도 맞고 마릿수도 맞았습니다. 그래서 반대로 물어봤습니다. "그 80%는 어떻게 계산하신 거예요?" 돌아온 답이 "산란동 기준이죠"였습니다. 그 한마디에 원인이 나왔습니다.
숫자가 안 맞는다는 말을 들었을 때 가장 먼저 의심한 건 제 코드였습니다. 반올림을 잘못했나, 집계 기간이 하루 밀렸나 같은 것들이었습니다. 전부 아니었습니다.
육성동 닭은 아직 알을 낳지 않습니다
산란계는 태어나자마자 알을 낳지 않습니다. 대략 생후 18주까지는 그냥 자랍니다. 그 기간의 닭이 사는 곳이 육성동이고, 알을 낳기 시작하면 산란동으로 옮깁니다.
그러니 육성동 닭은 분자에 0으로 들어오면서 분모에는 1만 수가 그대로 들어갑니다. 계산이 반토막 나는 이유가 이것입니다.

농장에서 쓰는 산란율은 "지금 알을 낳고 있는 닭 중에 몇 마리가 오늘 알을 낳았나"입니다. 저는 "이 농장에 있는 닭 중에 몇 마리가 오늘 알을 낳았나"를 계산하고 있었습니다. 문장으로 적으면 다른 질문인데, 코드로 적으면 둘 다 그럴듯해 보입니다.
육성동 비중이 클수록 오차도 커집니다. 산란동만 있는 농장은 애초에 멀쩡했고, 육성동을 한 동 이상 돌리는 농장에서만 숫자가 어긋났습니다. 실제 운영 데이터로 맞춰 보니 20%p 이상 벌어졌습니다. 그래서 어떤 농장은 화면을 보고 아무 말이 없었고, 어떤 농장은 첫날부터 이상하다고 하셨던 겁니다.
분모를 산란동의 암컷 수로 좁혔습니다
고친 부분은 집계 한 줄입니다. 계사 종류로 걸러서 산란동만 더하게 했습니다.
계사에는 종류가 있습니다. 산란동과 육성동입니다. 데이터에도 그 구분이 이미 있었는데, 저는 집계할 때 그 칸을 보지 않았습니다.
laying = DailyHouseStatus.objects.filter(
daily_report=report,
house__house_type=House.LAYING, # 육성동은 분모에서 제외
)
hen_count = laying.aggregate(n=Sum("current_female_count"))["n"] or 0
lay_rate = round(total_eggs / hen_count * 100, 1) if hen_count else 0주의할 점이 하나 있습니다. 동별 산란율은 건드리면 안 됩니다. 육성동의 산란율 0%는 그 자체로 맞는 값이고, 화면에서도 그렇게 보여야 합니다. 바뀌어야 하는 건 농장 전체를 하나로 묶은 대시보드 숫자뿐입니다.
육성동 닭이 자라서 산란동으로 옮겨지면 분모가 그날부터 늘어나야 합니다. 이 이동을 일보에서 전입과 전출로 기록하는데, 산란율 분모도 같은 값을 따라가게 맞췄습니다. 분모를 어디서 가져오느냐가 아니라, 분모가 매일 어떻게 변하는지를 따라가는 문제였습니다.
이 구분을 넣고 나니 농장에서 말하던 80%가 화면에 그대로 떴습니다. 고친 코드가 맞아서라기보다, 그제야 같은 것을 세고 있었기 때문입니다.
암수가 섞인 계사에서 한 번 더 틀렸습니다
고치고 나서 또 어긋난 농장이 있었습니다. 종계처럼 수탉이 함께 있는 계사였습니다. 수탉은 알을 낳지 않으니 분모에서 빠져야 하는데, 저는 계사의 전체 마릿수를 쓰고 있었습니다.
그래서 분모를 산란동의 암컷 현재수로 다시 좁혔습니다. 일보를 저장할 때 암수를 나눠 기록하고, 폐사나 이동이 생기면 암수 각각을 따로 갱신합니다. 앞선 날짜의 입력이 나중에 바뀌면 그 뒤 날짜의 마릿수도 연쇄로 다시 계산되는데, 그때 산란율도 함께 다시 계산하게 했습니다.
알아두기 이런 값은 한 번 잘못 저장되면 조용히 남습니다. 지난달 보고서를 다시 열었을 때 숫자가 달라지는 편이, 틀린 숫자가 그대로 남아 있는 것보다 낫습니다.
사람 손으로 고친 부분이 하나 더 있습니다. 이미 저장돼 있던 지난 일보들입니다. 코드를 고쳐도 예전에 계산해 둔 산란율은 그대로 남아 있어서, 과거 데이터를 다시 훑으며 값을 갱신했습니다. 새로 만드는 프로그램이라 양이 많지 않아 다행이었지, 몇 년치였다면 이쪽이 더 큰 일이었을 겁니다.
같은 실수가 다시 나오지 않게 테스트로 묶었습니다
이런 버그는 다시 돌아옵니다. 나중에 집계 코드를 고치다 보면 계사 구분을 빠뜨리기 쉽고, 그때는 아무도 모른 채 배포됩니다. 그래서 산란동 1동과 육성동 1동을 만들어 두고, 각각 암컷 1만 수에 알은 산란동에서만 8,000개가 나오는 상황을 테스트로 고정했습니다. 기대값은 80.0%입니다.

테스트 파일 맨 위에는 왜 이 테스트가 있는지 적어 두었습니다. "육성동이 분모에 들어가면 산란율이 실제보다 20%p 이상 낮게 표시된다"는 한 줄입니다. 반년 뒤의 제가 이 테스트를 지우려 할 때 읽으라고 남긴 문장입니다.
테스트는 세 개로 나눴습니다. 대시보드 산란율이 80.0%로 나오는지, 동별 산란율은 그대로 두는지, 경영 요약 보고서에서도 같은 값이 나오는지입니다. 같은 규칙이 화면 세 곳에서 쓰이고 있어서, 한 곳만 고치면 나머지 두 곳이 조용히 어긋납니다.
테스트를 먼저 쓰는 습관은 없었는데, 이 건은 예외로 두었습니다. 직접 겪어 보니 이런 종류의 버그는 다음에 또 만들 자신이 있었습니다.
도메인을 모르면 코드는 조용히 틀립니다
문법이 틀리면 프로그램이 멈추고, 로직이 틀리면 테스트가 잡아 줍니다. 그런데 도메인을 모르면 아무 일도 일어나지 않습니다. 화면에는 그럴듯한 숫자가 뜨고, 그 숫자를 보고 누군가 판단을 합니다.
이 일 이후로 새 계산식을 만들 때는 코드를 열기 전에 현장에서 쓰는 말을 먼저 묻습니다. "산란율"이라는 한 단어 안에 "산란동만", "암컷만"이라는 조건이 이미 들어 있었는데, 저는 그걸 모른 채 사전적인 의미로 구현했습니다.
다음에 농장 프로그램을 더 손보게 되면, 사료 요구율이나 폐사율처럼 비슷하게 생긴 지표들도 같은 방식으로 한 번씩 되물어 볼 생각입니다. 그 단어들 안에도 제가 모르는 조건이 들어 있을 것 같습니다.
비슷한 일을 하는 분이 계시다면, 지표 하나를 처음 만들 때 현장에서 그 말을 어떻게 쓰는지부터 물어보시길 권합니다. 저는 그 질문 하나를 건너뛰어서 이틀을 썼습니다.