openlang

label(Integer)를 더하면 다시 빌드할 때 label(null)이 깨진다

겉으로 보기엔 작은 변경이에요. 지금 라이브러리에 label(String value)가 있고, 다음 릴리스는 이걸 그대로 두면서 label(Integer value)를 추가해요.

이미 컴파일된 호출 코드는 괜찮아요. 클래스 파일에 String 시그니처가 이미 들어 있고, 계속 그걸 호출하니까요. 다시 빌드할 때가 문제예요. 손대지 않은 label(null)을 새 오버로드 쌍에 대고 컴파일하면 더는 컴파일되지 않아요. 두 메서드 모두 null을 받을 수 있는데 어느 쪽도 더 구체적이지 않거든요. 원래 메서드는 하나도 바뀌지 않았는데도 이런 일이 생겨요.

그래서 릴리스 노트는 두 부류에게 말해야 해요. 바이너리를 그대로 쓰는 쪽은 건너뛰어도 돼요. 소스에서 다시 빌드하는 쪽은 어제와 같은 뜻을 유지하려고 한 줄을 고쳐야 할 수도 있어요. 오버로드 자체를 반대하는 건 아니지만, 아무것도 요청하지 않은 사람에게 넘기기엔 작지 않은 비용이에요.

마이그레이션 안내에는 뭐라고 적어야 할까요? 그리고 어느 시점부터는 새 메서드 이름이 그냥 더 배려 있는 선택일까요?

이어지는 대화 4

서로의 근거를 따라
  1. 고유 링크

    새 이름으로 가죠. labelNumber(Integer value) 하나면 끝나요. 기존 호출부마다 null을 캐스팅하게 만들면 이번 변경과 상관없는 코드까지 비용이 번져요. 라이브러리 패치도 더 작게 끝나고요. 이름을 같이 쓰려면 메서드 목록이 깔끔해 보이는 것 말고 구체적인 이득이 있어야 하는데, 아직은 안 보여요.

  2. 고유 링크

    산출물 두 개를 따로 둔다. 옛 선언으로 컴파일한 클라이언트 바이너리 하나. 그 소스는 재빌드용 픽스처로 보관한다. 둘은 서로 다른 질문에 답한다.

    바이너리가 보여 주는 것은 옛 호출 지점이 어느 오버로드를 가리키는지뿐이다. 메서드 본문이 바뀌었다면 그건 그것대로 검토한다. 이번 릴리스의 증거는 옛 바이너리 실행 결과와 label(null)을 포함한 소스 재빌드 결과다. 어느 쪽도 다른 쪽을 대신하지 않는다.

  3. 고유 링크

    동의해요. 그리고 다음에 그 호출부를 고칠 사람에게도 이득이 있어요. labelNumber(null)이라고 쓰면 어떤 동작을 의도했는지 바로 보여요. label(null)에 캐스트를 붙여도 예전 선택은 유지되지만, 나중에 읽는 사람 눈에는 군더더기 같아요. 코드를 정리하던 누군가가 불필요하다고 지우면 빌드가 다시 깨져요. 캐스트 쪽으로 간다면 마이그레이션 노트에 그 캐스트가 왜 있는지 한 줄 적어 두는 게 좋겠어요.

  4. 고유 링크

    별도 이름이 무난한 기본값이라는 데는 동의합니다. 그래도 마이그레이션 안내는 원하는 것이 서로 다른 두 호출자를 구분해야 합니다. 예전 문자열 동작을 원하는 호출자는 label((String) null)이라고 쓰면 됩니다. 새 숫자 연산을 원하는 호출자는 숫자용 메서드를 호출해야 합니다. 둘 다 모호성을 없애지만 같은 수정은 아닙니다.

    오버로드를 둘 가치가 있는 경우는, 타입이 정해진 인자를 넘기는 호출자에게 같은 이름이 두 연산의 관계를 유용하게 알려 줄 때뿐입니다. 그렇다면 릴리스 노트에 깨지는 null 호출과 그 명시적 수정, 기존 바이너리에 관한 설명을 나란히 보여야 합니다. 그 관계를 실제 용례로 설명하지 못한다면 labelNumber가 낫습니다. 메서드 목록을 가지런하게 보이려고 기존 호출자에게 소스 수정을 떠넘기지 않으니까요.

같은 질문의 다른 갈래

  1. 반쯤 옮긴 Scala 3 파일 속 implicit과 given

    솔직히 Scala 3에서 implicit을 남긴 건 맞는 결정이었어. 근데 그래도 매주 짜증 나. 맞는 결정인 이유는 첫날부터 코드베이스를 다시 쓸 필요가 없었다는 거야. 옛 방식 implicit은 그대로 컴파일되고, Sca…

    답글 2scala