디컴파일된 코드는 하나의 가설일 뿐이다 — 실제로 신뢰할 수 있으려면 시계열 생산 파일 diff와 벤더 UI 대조 같은 독립적인 근거로 교차검증해야 한다.

배경
한 생산라인(생산라인 A)의 품질검사·기계보정 루프가 서드파티 클로즈드소스 프로그램(이하 VendorTool)에 의존하고 있었다. 흐름은 단순했다 — CMM(3차원측정기)이 가공된 홀을 측정하면, VendorTool이 그 리포트를 받아 보정값을 계산하고, 그 값을 CNC 프로그램에 반영한다. 문제는 이 프로그램이 소스코드도, 알고리즘 문서도 없는 순수 블랙박스였다는 것이다. 어떤 날 제안된 보정값이 유난히 크거나 작아 보여도, 사내 누구도 그 계산 근거를 설명할 수 없었다. 이 의존성을 없애고 자체 시스템으로 대체하려면, 먼저 기존 프로그램이 정확히 무엇을 계산하는지부터 알아내야 했다.
첫 시도, 그리고 실패
처음 계획은 이 조사를 자율 코딩 서브에이전트에 위임하는 것이었다. "이 바이너리를 디컴파일해서 알고리즘을 문서화하라"는 지시는 도구 중심의 완결된 과제처럼 보였다. 두 번의 시도 모두 0초 만에 interrupted 상태로 즉시 끊겼다 — 원인을 진단할 틈도 없이 두 번째 시도가 첫 번째와 똑같이 실패했다. 위임을 계속 재시도하는 건 시간 낭비라고 판단하고, 곧바로 방향을 바꿔 직접 조사하기로 했다. 이 선택은 나중에 중요해진다 — 가장 결정적인 발견은 diff 결과를 직접 눈으로 들여다보다 나온 것이었다.
조사: 도구부터 직접 만들어야 했다
환경에는 sudo 권한이 전혀 없었다. PE/.NET 메타데이터 검사를 위한 Python venv(pefile, dnfile), 사용자 권한으로 설치한 .NET SDK 8.0, 그리고 ILSpy 디컴파일러까지 전부 사용자 쓰기권한 영역 안에서 직접 구축해야 했다. 첫 걸림돌은 최신 ilspycmd(10.x)가 아예 실행되지 않는다는 것이었다 — 배포 패키지에 매니페스트 파일이 빠진 패키징 버그였다. 업스트림 수정을 기다리는 대신 알려진 정상 버전(8.2.0.7535)으로 고정했다. "일단 구버전으로 내려본다"를 1순위 디버깅 조치로 삼지 않았다면, 이 지점에서 몇 시간을 그냥 날렸을 것이다.
디컴파일러가 살아나자 예상 밖의 행운이 있었다 — 바이너리에 디버그 심볼이 그대로 남아 있어서, 원본 클래스명·메서드명이 100% 복원됐다. Class1, method_0 같은 일반화된 잔해가 아니라, 실행파일과 부속 라이브러리를 합쳐 약 67개 파일, 약 3,200줄 분량의 진짜 로직이 그대로 드러났다. 메서드명 자체가 각 코드 경로의 목적을 알려줬기 때문에, 순전히 제어흐름만으로 의도를 추측해야 하는 상황을 피할 수 있었다.
발견: 숫자 몇 개가 알고리즘 전체를 좌우하고 있었다
회수한 소스코드를 읽어나가며 세 가지 핵심 로직을 찾아냈다.
첫째, 80% 댐핑(감쇠) 계수. 계산된 원시 보정량 중 실제로는 80%만 매 사이클 기계에 반영되고 있었다. 코드베이스 세 곳에서 독립적으로 같은 헬퍼 함수를 기반으로 이 감쇠를 적용하고 있었다 — 전형적인 지수감쇠 제어 전략으로, 한 번에 완전히 보정했다가 오버슈트하는 것을 막기 위한 설계였다. 이 숫자는 어디에도 문서화돼 있지 않았고, 메서드 깊숙한 곳에 하드코딩된 승수로만 존재했다.
둘째, 전체 좌표계 보정 vs 개별 지점 보정을 가르는 두 개의 임계값. 편차의 70% 이상이 한 방향(+/-)으로 쏠리면 구조적 오프셋으로 판단해 좌표계 전체를 일괄 보정하고, 그렇지 않으면 지점별 개별 보정으로 대체됐다. 그 이전에, 공차의 30% 미만인 편차는 노이즈로 간주돼 아예 필터링됐다. 운영자 입장에서는 "어떤 날은 전체가 보정되고 어떤 날은 한 지점만 보정된다"는 겉모습만 보일 뿐, 이 분기 조건은 코드를 직접 읽지 않고는 알 수 없는 로직이었다.
셋째, 축 매핑과 독립적인 부호 반전. CMM에서 측정된 축이 실제 기계의 어느 축에 대응하는지, 그리고 부호를 반전시킬지는 config로 제어됐다. 부호가 매핑 단계와 별도의 boolean 플래그 두 곳에서 각각 반전될 수 있어서, 재구현 시 이 부분을 잘못 이해하면 특정 축의 보정 방향 전체가 조용히 뒤집히는 버그가 생길 수 있었다 — 계산 결과 자체는 어느 방향이든 그럴듯해 보이기 때문에 단일 사례만으로는 잡아내기 어려운 종류였다.
조치: clean-room 재구현
디컴파일된 코드는 여전히 벤더의 저작물이라 그대로 재사용하면 라이선스 리스크가 생긴다. 그래서 회수한 로직은 철저히 "알고리즘 명세서"로만 취급했다. 실제 대체 시스템은 완전히 새로 작성한 config-driven 파이프라인(측정값 파싱 → 보정 엔진 → NC 프로그램 작성기 → 전송)으로 구성했고, 원본 프로그램에는 없던 명시적 승인 단계도 추가했다 — 원본은 사람의 개입 없이 완전 자동으로 보정을 반영하고 있었다.
@dataclass
class CorrectionConfig:
damping_rate: float = 0.80
coord_system_threshold: float = 0.70
min_tolerance_ratio: float = 0.30
axis_map: dict = field(default_factory=lambda: {
"measured_x": ("machine_x", False),
"measured_y": ("machine_y", True), # True = 부호 반전
"measured_z": ("machine_z", False),
})
디컴파일된 로직 속에 파묻혀 있던 하드코딩된 숫자들이, 이제는 이름이 붙고 테스트 가능하고 독립적으로 조정 가능한 설정값이 됐다.
검증
코드를 읽는 것은 알고리즘이 무엇을 하도록 설계됐는지 알려줄 뿐, 실제로 그렇게 동작하는지는 증명하지 못한다. 그래서 두 가지 방식으로 실제 근거와 대조했다.
- 시계열 NC 파일 diff. 동일 설비의 NC 프로그램을 여러 날에 걸쳐 수집해 보정 오프셋을 diff했다. 약 15개 홀 위치에서 관측된 변화가, 매 사이클 보정값이 기존 오프셋을 덮어쓰지 않고 그 위에 누적된다는 회수 모델과 정확히 일치했다.
- 벤더 UI 대비 축 매핑 교차검증. 회수한 6개 파라미터 축 매핑을 벤더 프로그램의 실제 설정 화면(축 드롭다운, 반전 체크박스)과 직접 대조해 1:1로 일치함을 확인했다.
실제 데이터로 백테스트하자 코드만으로는 알 수 없던 사실도 드러났다 — 선형 모델은 보정의 방향은 잘 재현했지만 크기까지 늘 정확히 재현하지는 못했다. 이는 단일 측정 파일만으로는 완전히 복원할 수 없는 추가적인 형상 보정 요인이 존재함을 시사했고, 이 근거가 대체 시스템 초기 버전에 사람의 승인 단계를 그대로 남겨야 한다는 판단을 뒷받침했다.
소스코드도, 벤더 협조도, 문서도 없는 프로덕션 시스템 앞에서 "이해할 수 없으니 대체할 수 없다"는 답은 통하지 않는다. 여기서 진짜 핵심 역량은 디컴파일러를 다루는 기술이 아니라, 회수한 모든 주장을 하나의 가설로 취급하고 실제 시스템 동작(생산 파일 diff, 실제 UI 상태)과 대조해 검증하는 태도였다. 그 근거가 디컴파일된 코드든 벤더 자체 도구든, 단일 출처를 그대로 신뢰하지 않는 것 — 그리고 자신의 판단이 어디까지 확실하고 어디부터 불확실한지 정확히 아는 것이, 결과물을 후속 팀에게 정직하게 전달할 수 있게 만든 힘이었다.
첫 댓글을 남겨보세요