문제 발생 원인
문제는 ActiveRecord 객체 직렬화 코드에서 attributes.merge(id: public_id)와 같은 패턴을 사용하며 시작되었습니다. attributes는 문자열 키를 반환하고, id: public_id는 심볼 키를 사용했기에 {"id" => 1, :id => "one"}과 같이 동일한 키가 문자열과 심볼 형태로 공존하는 해시가 생성되었습니다.
Rails 버전별 동작 변화
-
Rails 7.2까지:
render json:호출 시as_json메서드가 내부적으로 실행되어 키를 정규화하고 중복을 제거했습니다. 이로 인해 최종 JSON 응답에는 마지막 값이 사용되어 중복 키 문제가 발생하지 않았습니다. -
Rails 8의 변화:
render json:에 옵션이 없는 경우 “고속 경로(fast path)”가 도입되었습니다. 이 경로는as_json호출을 생략하여 JSON 렌더링 성능을 향상시켰지만, 키 정규화 및 중복 제거 로직 또한 건너뛰게 되었습니다.
중복 키 노출 및 근본 원인
Rails 8에서는 고속 경로가 활성화되면서 {"id" => 1, :id => "one"}과 같은 혼합 키 해시가 그대로 JSON 인코더로 전달되어 {"id":1,"id":"one"}과 같이 중복 키가 포함된 JSON 응답이 생성되었습니다. 이는 개발 코드의 잠재적 버그가 Rails 7의 암묵적 동작에 의해 가려져 있다가 Rails 8의 성능 최적화로 인해 드러난 것입니다. ActiveSupport 7.1.3 변경 로그의 중복 키 방지 관련 내용은 릴리스 전 롤백되어 혼란을 야기했습니다.
문제 방지 및 감지 전략
-
요청 스펙(Request Specs): JSON 응답 본문의 정확한 형태를 검증하는 스펙을 작성하여 중복 키를 즉시 감지합니다.
-
json젬 활용:json젬 2.14.0부터는 동일 키가 문자열/심볼 형태로 공존할 경우 경고를, 3.0 버전부터는 기본적으로 오류를 발생시킵니다. -
Rails Deprecation 설정:
config.active_support.disallowed_deprecation_warnings = [/detected duplicate key/]및config.active_support.disallowed_deprecation = :raise설정을 통해json젬의 경고를 Rails의 Deprecation 프레임워크에서 오류로 처리하여 개발 및 테스트 환경에서 조기에 문제를 발견할 수 있습니다.