운영 중인 PostgreSQL 배포 환경이 중단되었습니다. 개발자가 CREATE OR REPLACE FUNCTION을 사용하여 기존 함수에 선택적 인자를 추가한 후 발생한 사고였습니다. 이 변경으로 인해 이름이 같은 함수가 두 개 생성되었고, 데이터베이스는 “function is not unique” 오류를 반환했으며 API는 400 에러를 발생시켰습니다. 이 사고는 단 한 번의 마이그레이션 실수가 어떻게 운영 중인 스키마를 조용히 손상시킬 수 있는지, 그리고 왜 코드 수준의 검사만으로는 충분하지 않은지를 보여줍니다.

무엇이 잘못되었나

팀은 저장 프로시저에 추가적인 선택적 매개변수를 확장해야 했습니다. 그들은 CREATE OR REPLACE FUNCTION …이 기존 정의를 덮어쓸 것이라고 가정하고 실행했습니다. 하지만 PostgreSQL은 전체 인자 목록이 정확히 일치할 때만 함수를 교체합니다. 시그니처를 변경하면 기존 함수는 그대로 둔 채 완전히 새로운 함수 항목이 생성됩니다.

새 인자에 기본값이 설정되어 있었기 때문에, 기존 개수의 인자를 전달하는 호출자는 두 정의 중 어느 것과도 일치할 수 있었습니다. PostgreSQL은 어떤 함수를 호출해야 할지 결정할 수 없었고 “function is not unique” 오류를 던졌으며, 이는 API에서 400 응답으로 나타났습니다.

코드 저장소에는 단일 정의만 표시되었고, 소스 트리를 스캔하는 커스텀 스크립트에서도 중복이 없다고 보고되었습니다. 중복은 데이터베이스에만 존재했으며, 서버에서 오래된 마이그레이션 파일이 재실행될 때 발생했습니다.

마이그레이션이 왜 통과되었나

선택적 매개변수를 추가하는 마이그레이션은 단순히 CREATE OR REPLACE FUNCTION을 실행했습니다. 롤백 후나 반복 배포 중에 마이그레이션이 두 번째로 실행되었을 때, 데이터베이스는 이 명령을 "기존 것 교체"가 아닌 "새로운 오버로드 추가"로 처리했습니다. 마이그레이션이 결과 상태를 검증하지 않았기 때문에 중복이 인지되지 않은 채 지속되었습니다.

저장소를 확인하는 스크립트는 소스 파일을 조사했을 뿐, 운영 중인 스키마를 조사하지 않았습니다. 버그가 뒷문으로 들어오고 있는데 앞문만 보고 있었던 셈입니다.

위험 요소

모호한 함수 하나가 해당 함수에 의존하는 모든 서비스를 중단시킬 수 있습니다. 해결 방법은 함수 개수가 잘못된 경우 트랜잭션을 롤백하고 스키마 캐시에 재로드를 요청하는 것이었습니다.

마이그레이션을 보호하는 방법

팀은 명시적인 검사 기능을 포함하여 마이그레이션을 재구축함으로써, 이를 자기 검증형(self-asserting) 작업으로 전환했습니다.

  • 트랜잭션을 시작하여 실패 시 모든 변경 사항이 롤백되도록 합니다.
  • 새 버전을 만들기 전에 **기존 함수를 명시적으로 삭제(Drop)**하여 단 하나의 정의만 존재하도록 보장합니다.
  • 원하는 시그니처로 새 함수를 생성합니다.
  • pg_catalog에서 해당 이름을 가진 함수의 개수를 세고, 개수가 정확히 하나인지 확인합니다.
  • 개수가 일치하지 않으면 트랜잭션을 롤백하여 중복이 지속되는 것을 방지합니다.
  • 스키마 캐시에 재로드를 알림으로써 이후의 쿼리가 업데이트된 정의를 볼 수 있도록 합니다.

코드가 올바르다고 가정하는 대신 데이터베이스에 "어떤 함수들이 존재하는가?"라고 물음으로써, 마이그레이션은 반복 실행, 부분 배포 또는 수동 편집에 대해 신뢰성을 갖게 됩니다.

반론: 편의성 vs 안전성

CREATE OR REPLACE FUNCTION은 별도의 삭제(drop) 문을 작성하지 않고도 개발자가 빠르게 반복 작업(iterate)을 할 수 있게 해주므로 매력적입니다. 마이그레이션이 한 번만 실행되고 다시 실행되지 않는 환경에서는 이 지름길이 잘 작동합니다. 위험은 마이그레이션이 재실행될 때 나타납니다. 테스트 데이터베이스를 초기화하는 CI 파이프라인, 자동 롤백, 또는 운영 환경에서의 수동 재적용 등이 그 원인이 될 수 있습니다.

요약

CREATE OR REPLACE를 사용하여 함수의 시그니처를 변경하는 것은 교체를 보장하지 않습니다. 인자 목록이 다르면 PostgreSQL은 조용히 오버로드를 생성합니다. 마이그레이션에 의존하는 운영 환경은 소스 코드뿐만 아니라 결과 스키마를 반드시 검증해야 합니다. 명시적인 삭제, 트랜잭션 검사, 마이그레이션 후 검증을 포함하면 편리한 지름길을 신뢰할 수 있고 반복 가능한 프로세스로 바꿀 수 있습니다.