Claude가 Rails 콜백에 대해 알고 있다고 착각한 것

LineItem 레코드와 해당 S3 파일을 삭제하기 위해 rake task를 실행하려고 했습니다. Order와 같은 상위 모델에서 발생하는 비용이 큰 콜백(callback)을 피하고 싶었습니다.

Claude에게 도움을 요청했습니다. Claude는 자신 있게 답변했지만, 틀렸습니다.

Rails, counter cache, 그리고 왜 AI의 조언을 반드시 검증해야 하는지에 대해 제가 배운 내용을 공유합니다.

The Problem LineItem은 OrderItem에 속합니다. OrderItem은 Order에 속합니다. 둘 다 counter_cachetouch를 사용합니다. LineItem을 삭제하면 연쇄 반응(cascade)이 일어납니다. 이 연쇄 반응은 배송 추정 및 총액 재계산과 같은 무거운 작업들을 실행합니다. CPU와 S3 비용을 절약하기 위해 이 연쇄 반응을 중단해야 했습니다.

The AI Mistake Claude는 skip_callback을 사용하라고 제안했습니다. 이것은 좋지 않은 생각입니다. skip_callback은 클래스를 전역적으로 수정합니다. 이는 앱의 모든 스레드에 영향을 미칩니다. 만약 콜백을 다시 활성화하기 전에 코드가 충돌하면, 콜백은 영구적으로 작동하지 않게 됩니다.

그 다음 no_touching을 시도했습니다. 안전을 위해 OrderItem과 Order 모두에서 호출을 감싸(wrap) 보았습니다. 테스트는 통과했지만, 콘솔에서는 다른 결과가 나타났습니다. Order의 타임스탬프가 여전히 변경되고 있었습니다.

The Real Reason 문제는 counter_cachetouch와 함께 작동하는 방식에 있었습니다.

  • counter_cache: truetouch: true를 함께 사용하면, Rails는 이를 하나로 묶습니다(bundle).
  • 단일 raw SQL UPDATE ALL 명령을 실행합니다.
  • Raw SQL은 ActiveRecord 라이프사이클을 우회합니다.
  • 라이프사이클을 우회하기 때문에, after_commit 훅이 실행되지 않습니다.

이로 인해 이상한 역설이 발생했습니다:

  • raw SQL 번들링 때문에 OrderItem 콜백은 실행되지 않았습니다.
  • 하지만 체인의 다음 단계가 일반적인 touch였기 때문에 Order 콜백은 실행되었습니다.

The Fix 저는 증조부(grandparent) 모델만 no_touching으로 감싸면 되었습니다.

Order.no_touching { line_item.destroy! }

이렇게 하면 AR 레벨의 touch가 Order 모델에 도달하는 것을 막을 수 있습니다. OrderItem의 raw SQL을 막지는 못하지만, counter cache 번들이 이미 해당 콜백들을 건너뛰기 때문에 상관없습니다.

Key Takeaways

  • counter_cache: true + touch: true = raw SQL UPDATE ALL.
  • Raw SQL은 모든 after_commit 훅을 건너뜁니다.
  • 일반적인 touch(counter cache가 없는 경우)는 표준 AR 라이프사이클을 따르며 콜백을 실행합니다.
  • AI 코드를 맹목적으로 믿지 마세요. Claude는 어떻게든 답변을 내놓으려 할 뿐, 그 답변이 환각(hallucination)인지 아닌지는 신경 쓰지 않습니다.

항상 Rails 콘솔에서 가설을 테스트하세요. 의도적으로 코드를 망가뜨려 보면서, 당신이 막고자 하는 동작이 실제로 중단되는지 확인해야 합니다.

Source: https://dev.to/husteadrobert/what-claude-thought-he-knew-about-rails-callbacks-and-how-console-testing-proved-him-wrong-j20

Optional learning community: https://t.me/GyaanSetuAi