사전 하위 호환
개념

하위 호환

gabury1

하위 호환은 새 버전이 옛 버전을 쓰던 쪽을 계속 받아 주는 성질입니다. 서버를 고쳐 올려도 옛 앱은 손대지 않은 채 돌아갑니다. 쓰는 쪽이 같은 날 함께 고치지 않아도 된다는 약속입니다.

쉽고 빠른 이해

하위 호환은 새 버전을 내놓아도 옛 쪽이 안 깨지게 막아 주는 약속입니다. 서버를 새 판으로 올려도 옛 앱이 보내던 옛 모양 요청을 받아 줍니다. 옛 앱은 한 줄도 고치지 않습니다.

이게 없으면 만드는 쪽과 쓰는 쪽이 같은 순간에 함께 올라가야 합니다. 남의 손에 깔린 앱은 그렇게 못 합니다. 이미 디스크에 쌓인 옛 데이터는 고쳐 달라고 할 상대도 없습니다.

지키는 방법은 대개 셋입니다:

  1. 더하기만 합니다. 있던 것은 이름도 뜻도 안 바꿉니다
  2. 더한 것은 없어도 되게 둡니다. 안 들어오면 정해 둔 값으로 채웁니다
  3. 없앨 것은 미리 알린 뒤 한참 지나서 없앱니다

대가는 옛 방식을 계속 남겨 두는 것입니다. 한 번 내보낸 이름과 동작은 잘못된 것이어도 오래 삽니다. 부르는 쪽이 전부 내 손에 있어 한 번에 같이 올릴 수 있으면 덜 챙겨도 됩니다.

상세

단골이 늘 시키던 이름 그대로 주문하면 늘 먹던 것이 나오는 가게를 떠올리면 됩니다. 메뉴가 몇 가지 늘어도 그 이름이 그대로면 단골은 메뉴판을 다시 볼 일이 없습니다. 이름은 그대로 두고 안에 든 것만 바꾸면, 단골은 아무 의심 없이 시킵니다. 그러고는 다른 것을 받아 갑니다.

하위 호환은 새 버전이 옛 버전 쪽의 호출과 데이터와 프로그램을 받아도 하던 대로 도는 성질입니다. 서버를 새 판으로 올린 뒤에도 옛 앱이 보내던 옛 모양 요청을 처리하는 것이 그 예입니다. 옛 앱은 손대지 않습니다. 알던 주소와 알던 입력이 그 이름으로 남아 있기 때문입니다.

같아야 하는 것은 이름만이 아닙니다. 이름과 인자가 같아도 돌려주는 값의 뜻이 달라지면 깨집니다. 옛 코드는 오류 없이 돕니다. 그러면서 틀린 값을 쓰니 눈에 띄기까지 오래 걸립니다.

「하위」가 가리키는 방향

「하위」는 옛 버전을 가리킵니다. 견디는 쪽은 새 버전입니다. 새것이 옛것 쪽으로 손을 내미는 그림입니다. 방향이 헷갈리기 쉬우니 누가 새 버전인지부터 정하고 읽으면 편합니다.

반대 방향에도 이름이 있습니다. 옛 프로그램이 나중에 나온 새 데이터를 만나고도 안 깨지면 그것은 상위 호환입니다. 둘은 같은 변경을 양쪽에서 부르는 말이 아니라 서로 다른 약속입니다.

이름 누가 새 버전인가 받는 쪽이 견디는 것
하위 호환 받는 쪽 옛 요청과 옛 데이터가 들어와도 돈다
상위 호환 보내는 쪽 새 요청과 새 데이터가 들어와도 안 깨진다

한 번 주고받는 데에도 방향이 둘입니다. 서버 응답에 항목을 하나 더하는 변경이 그 둘을 한 그림에 담습니다. 요청을 받는 쪽은 새 서버입니다. 응답을 받는 쪽은 옛 앱입니다. 그래서 같은 왕복 안에서 두 이름이 각각 걸립니다.

flowchart TD
    O["옛 앱 · 고치지 않았다"]
    N["새 서버 · 새 판으로 올렸다"]
    O -->|"옛 모양 요청 · 받아 주는 것이 서버의 하위 호환"| N
    N -->|"모르는 항목이 섞인 응답 · 건너뛰는 것이 앱의 상위 호환"| O

새 서버가 옛 앱의 요청을 받아 처리하는 것은 서버의 하위 호환입니다. 옛 앱이 모르는 항목을 만나고도 멈추지 않는 것은 그 앱이 상위 호환으로 만들어졌기 때문입니다. 앱이 모르는 항목에서 오류를 내도록 짜여 있었다면, 서버가 아무리 조심해도 이 변경은 깨집니다.

깨는 변경과 안 깨는 변경

가르는 잣대는 하나입니다. 옛 쪽이 알던 것이 그 이름과 그 뜻으로 남아 있나. 남아 있으면 안전합니다. 없어지거나 뜻이 바뀌면 깨집니다. 이렇게 옛 쪽을 깨는 변경을 실무에서는 브레이킹 체인지(breaking change)라고 부릅니다.

대체로 안전한 변경 깨는 변경
없어도 되는 항목을 더한다 있던 항목을 없애거나 이름을 바꾼다
새 함수나 새 주소를 더한다 반드시 채워야 하는 입력을 더한다
받아 주는 값의 범위를 넓힌다 받아 주는 값의 범위를 좁힌다
쓰지 말라고 알리고 당장은 남겨 둔다 같은 입력에 다른 결과를 돌려준다

더하는 쪽이 안전하고 빼거나 좁히는 쪽이 깬다고 외워 두면 대개 맞습니다. 다만 더하기도 깰 때가 있습니다. 반드시 채워야 하는 입력을 하나 더하면, 그것을 모르는 옛 호출은 전부 거절됩니다.

flowchart TD
    A["옛 쪽이 알던 이름이 남았나"] -->|아니다| X["깬다"]
    A -->|그렇다| B["뜻이 같은가"]
    B -->|아니다| Y["조용히 깬다 · 오래 안 드러난다"]
    B -->|그렇다| C["반드시 채워야 하는 입력을 더했나"]
    C -->|그렇다| Z["깬다"]
    C -->|아니다| W["안전하다"]

새로 더한 것을 없어도 되게 만드는 장치는 기본값입니다. 기본값은 값이 안 들어왔을 때 대신 쓰려고 미리 정해 둔 값입니다. 새 항목에 기본값을 두면, 그 항목이 없는 옛 요청과 옛 데이터도 빈칸 없이 읽힙니다.

배경

프로그램은 혼자 돌지 않습니다. 앱은 서버를 부릅니다. 프로그램은 남이 만든 라이브러리를 가져다 씁니다. 도구는 남이 저장해 둔 파일을 읽습니다. 그런데 만드는 쪽이 새 버전을 내도 쓰는 쪽은 같은 순간에 바뀌지 않습니다.

옛 버전이 남는 기간은 쓰는 쪽마다 다릅니다.

쓰는 쪽 옛 버전으로 남는 기간
여러 대로 나눠 올리는 서버 새 버전이 다 뜰 때까지 몇 분
쓰는 사람 손에 깔린 앱 갱신을 미루면 몇 달
디스크에 저장된 데이터 지우거나 옮기지 않는 한 끝이 없다

이렇게 두 버전이 함께 도는 구간을 버전 혼재라고 합니다. 아래는 그 구간에 새 서버 한 대가 받는 요청입니다.

sequenceDiagram
    participant O as 옛 앱
    participant N as 새 앱
    participant S as 새 서버
    O->>S: 옛 모양 요청
    S-->>O: 항목 하나가 더 붙은 응답
    Note over O: 모르는 항목은 건너뛴다
    N->>S: 새 모양 요청
    S-->>N: 항목 하나가 더 붙은 응답

화살표 네 개가 모두 정상이어야 이 구간을 지납니다. 새 서버가 옛 모양 요청을 거절하기 시작하면 배포를 멈추고 옛 버전으로 되돌리는 롤백부터 하게 됩니다. 그래서 새 쪽이 옛 쪽을 한동안 받아 줘야 했습니다. 그 성질에 붙은 이름이 하위 호환입니다. 새 버전을 일부에게만 먼저 내보내 지켜보는 카나리 배포도 이 성질 위에 서 있습니다.

예시

하위 호환을 규칙으로 못 박은 곳 넷을 봅니다. 버전 번호 규칙 하나, 데이터 형식 둘, 운영체제 커널 하나입니다. 소절마다 그 규칙이 옛 쪽에 무엇을 약속하는지를 봅니다.

시맨틱 버저닝

시맨틱 버저닝(Semantic Versioning, 줄여 SemVer)은 버전 번호에 하위 호환 여부를 싣는 규칙입니다. 번호를 MAJOR.MINOR.PATCH 세 칸으로 적습니다. 어느 칸을 올렸는지가 곧 깨는 변경이 들었는지를 말합니다.

이 규칙은 먼저 공개 API(Application Programming Interface, 응용 프로그램 인터페이스)를 밝히라고 요구합니다. API 는 프로그램끼리 서로를 부르는 접점입니다. 함수 이름과 인자, 요청 주소와 응답 모양이 여기 듭니다. 세 칸은 이 공개 API 를 기준으로 올립니다.

아래 셋은 맨 앞 칸이 1 이상인 판에만 걸리는 규칙입니다. 맨 앞 칸이 0 인 판은 뒤의 「덜 챙겨도 되는 조건」이 따로 받습니다.

올리는 칸 언제 올려야 하나(MUST) 걸리는 판 하위 호환
셋째 칸 PATCH 하위 호환을 지키는 버그 수정만 넣었을 때 x.y.Z 에서 x > 0 지킨다
둘째 칸 MINOR 하위 호환을 지키는 기능을 공개 API 에 더했을 때 · 공개 API 기능을 폐기 예정으로 표시했을 때 x.Y.z 에서 x > 0 지킨다
첫째 칸 MAJOR 하위 호환을 깨는 변경을 공개 API 에 하나라도 넣었을 때 X.y.z 에서 X > 0 깬다

윗칸을 올리면 그 아래 칸은 0 으로 돌아갑니다(MUST).

flowchart TD
    V["1.4.2"]
    V -->|"하위 호환을 지키는 버그 수정"| P["1.4.3"]
    V -->|"하위 호환을 지키는 기능 추가"| M["1.5.0 · 셋째 칸이 0 으로"]
    V -->|"깨는 변경"| J["2.0.0 · 둘째·셋째 칸이 0 으로"]

1.4.2 다음 판이 1.5.0 이면 옛 코드가 그대로 돈다는 뜻입니다. 2.0.0 이면 깨는 변경이 들었다는 뜻입니다. 맨 앞 칸이 1 이상인 판끼리라면, 쓰는 쪽은 맨 앞 칸 하나만 보고 올려도 되는지를 가릴 수 있습니다.

Avro 스키마 해석

Avro 는 데이터를 적을 때 쓴 스키마를 데이터와 함께 두는 직렬화 형식입니다. 직렬화는 메모리 속 값을 저장하거나 보낼 수 있게 바이트로 적는 일입니다. 스키마는 어떤 이름의 값이 어떤 모양으로 들어 있는지를 적은 것입니다. 데이터를 쓴 소프트웨어와 읽는 소프트웨어의 판이 다르면 레코드에 필드가 늘거나 줄어 있을 수 있습니다. 필드는 레코드 안에 든 이름 붙은 값 하나입니다. 앞 절에서 「항목」이라 부른 것과 같습니다.

데이터를 쓸 때 쓴 스키마를 쓴 쪽 스키마(writer's schema), 읽는 앱이 기대하는 스키마를 읽는 쪽 스키마(reader's schema)라고 부릅니다. 두 레코드 스키마가 다를 때는 아래 규칙으로 맞춥니다. 이 규칙을 스키마 해석(schema resolution)이라고 합니다.

두 스키마의 차이 읽는 쪽이 하는 일
필드 순서가 다르다 이름으로 짝을 맞춘다
쓴 쪽에만 있는 필드 그 필드의 값을 무시한다
읽는 쪽에만 있고 기본값이 있다 그 필드의 기본값을 써야 한다
읽는 쪽에만 있고 기본값이 없다 오류를 알린다

새 읽는 쪽 스키마에 기본값 있는 필드를 더하면, 그 필드가 없는 옛 데이터도 셋째 줄대로 읽힙니다. 하위 호환이 지켜진 것입니다. 기본값을 빠뜨리면 넷째 줄에 걸려 옛 데이터를 읽다가 오류가 납니다.

필드를 어느 쪽에서 지우느냐에 따라 판정이 뒤집히기도 합니다. 쓴 쪽이 아니라 새 읽는 쪽 스키마에서 필드를 지우면 옛 데이터는 그대로 읽힙니다. 옛 데이터에 남은 그 필드는 둘째 줄대로 무시되기 때문입니다. 앞 표에서 「있던 항목을 없앤다」를 깨는 변경으로 적었지만, 읽는 쪽 스키마에서 지우는 것은 하위 호환을 지킵니다.

프로토콜 버퍼 메시지 갱신

프로토콜 버퍼(Protocol Buffers)는 메시지 모양을 .proto 파일에 적는 직렬화 형식입니다. 메시지의 필드마다 번호가 붙습니다. 필드 번호를 바꾸는 것은 그 필드를 지우고 같은 타입의 새 필드를 더하는 것과 같습니다.

아래 판정은 이진 와이어 형식(binary wire format)으로 주고받을 때의 것입니다. 와이어는 두 쪽이 주고받는 바이트 그 자체를 가리키는 말입니다. 같은 메시지를 다른 형식으로 주고받으면 고칠 수 있는 범위도 달라집니다.

변경은 둘로 갈립니다. 와이어 안전한 변경은 데이터를 잃거나 새로 파싱이 실패할 위험 없이 스키마를 고치는 변경입니다. 파싱은 받은 바이트를 다시 값으로 읽어 내는 일입니다. 와이어 안전하지 않은 변경은 옛 스키마로 직렬화한 데이터를 새 스키마로 파싱하면(또는 그 반대면) 깨지는 변경입니다.

변경 이진 와이어 형식에서의 판정
새 필드를 더한다 안전하다. 옛 코드가 직렬화한 메시지를 새 코드가 파싱한다. 옛 코드로 빌드한 실행 파일은 새 필드를 무시한다
필드를 없앤다 안전하다. 다만 같은 필드 번호를 다시 쓰면 안 된다
열거형(enum)에 값을 더한다 안전하다
기존 필드의 번호를 바꾼다 안전하지 않다

필드를 없앨 때는 지운 필드 번호를 예약해 둬야 합니다. 예약하지 않으면 나중에 누군가 그 번호를 다시 쓸 수 있습니다. 새 필드를 더할 때는 그 필드의 기본값을 염두에 둬야 새 코드가 옛 코드가 만든 메시지와 맞물립니다. 안전하지 않은 변경은 데이터를 직렬화하고 역직렬화하는 쪽이 전부 새 스키마를 쓴다는 것을 알 때만 합니다.

리눅스 커널의 회귀 금지 규칙

리눅스 커널 개발의 첫째 규칙은 「회귀를 만들지 않는다(no regressions)」입니다. 회귀는 전에 되던 것이 새 판에서 안 되는 것입니다. 규칙은 한 문장으로 줄어듭니다. "If the kernel used to work for you, the rule is that it continues to work for you" — 전에 커널이 잘 돌던 사람에게는 계속 잘 돌아야 합니다.

쓰는 사람이 기대던 것이 갑자기 멈출까 겁낼 일 없이 커널을 올릴 수 있게 하려는 규칙입니다. 「커널을 올리려면 저 프로그램도 같이 올려라」 같은 조건을 붙이지 않습니다.

갈래

축은 무엇이 옛 버전 때와 같아야 하나입니다. 같은 변경이라도 무엇을 재느냐에 따라 호환인지가 갈립니다. 대상은 넷입니다. 어느 변경이 호환인지 늘 분명한 것은 아닙니다.

소스 호환

옛 버전에 맞춰 쓴 코드가 새 버전에 대고 다시 컴파일돼야 합니다. 컴파일만 되는 것으로는 모자랍니다. 새 판 클라이언트 라이브러리와 함께 제대로 돌아야 합니다. 클라이언트 라이브러리는 서비스를 부르는 코드를 미리 만들어 묶어 둔 꾸러미입니다. 함수 이름을 바꾸거나 반드시 넘겨야 하는 인자를 더하면 이 갈래가 깨집니다.

바이너리 호환

다시 빌드하지 않은 옛 실행 파일이 새 라이브러리와 링크돼야 합니다. 링크는 실행 파일이 부르는 이름을 라이브러리 안의 실물에 이어 붙이는 일입니다. 전에 오류 없이 링크되던 바이너리가 계속 오류 없이 링크되면 바이너리 호환입니다. 이 수준의 접점을 ABI(Application Binary Interface, 응용 프로그램 이진 인터페이스)라고 부릅니다.

바이너리 호환과 소스 호환은 같은 것이 아닙니다. 자바에서는 함께 모아 컴파일하면 오류가 나는 소스에서도 서로 호환되는 바이너리 묶음이 나올 수 있습니다. 갈라지는 대목은 한 이름이 무엇을 가리키나입니다. 선언은 클래스나 메서드 같은 이름을 하나 만들어 두는 문장이고, 전체 이름은 그 이름 앞에 꾸러미 이름까지 붙인 온전한 이름입니다.

새 선언이 하나 더해지면, 고치지 않은 소스를 다시 컴파일한 쪽에서는 그 안의 이름이 새 선언을 가리킵니다. 손대지 않은 코드인데 그 이름의 뜻이 달라지는 것입니다. 그 코드는 옛 뜻에 맞춰 쓰여 있어서 달라진 뜻으로는 앞뒤가 맞지 않고, 그래서 소스를 함께 모아 컴파일하면 거기서 오류가 납니다. 전에 만들어 둔 바이너리에서는 같은 이름이 전체 이름이라 옛 선언을 그대로 가리킵니다. 뜻이 달라지지 않으니 전처럼 오류 없이 링크됩니다.

flowchart TD
    N["새 선언이 하나 더해진다"]
    subgraph S1["고치지 않은 소스를 다시 컴파일한 쪽"]
        A1["소스 속 이름"] -->|가리킨다| A2["새로 더해진 선언"]
        A2 --> A3["손대지 않은 코드에서 이름의 뜻이 달라진다"]
        A3 --> A4["함께 모아 컴파일하면 오류가 난다"]
    end
    subgraph S2["전에 만들어 둔 바이너리 쪽"]
        B1["전체 이름"] -->|가리킨다| B2["옛 선언 · 옛 뜻"]
        B2 --> B3["전처럼 오류 없이 링크된다"]
    end
    N --> S1
    S1 -->|"같은 변경을 다른 쪽에서 보면"| S2

와이어 호환

옛 버전에 맞춰 쓴 코드가 새 서버와 제대로 주고받아야 합니다. 입력과 출력의 모양만 맞아서는 안 됩니다. 값을 바이트로 적는 직렬화와 바이트를 값으로 되돌리는 역직렬화에 거는 기대까지 맞아야 합니다.

의미 호환

옛 버전에 맞춰 쓴 코드가 대부분의 합리적인 개발자가 기대할 결과를 계속 받아야 합니다. 코드는 문서에 없는 동작에도 자주 기댑니다. 그래서 보이는 동작이나 의미를 합리적인 쓰는 쪽 코드를 깰 가능성이 큰 방식으로 바꾸면 안 됩니다. 그런 변경은 쓰는 쪽에게 깨는 변경으로 보입니다.

ip_address 필드에 IPv4(Internet Protocol version 4, 인터넷 프로토콜 4판) 형식 값을 담다가 IPv6(같은 프로토콜의 6판) 값을 담게 바꾸는 변경이 그 예입니다. 필드 이름은 그대로입니다. 담기는 값의 형식만 바뀌었습니다. 그래도 깨는 변경입니다.

무엇을 기대할지 가리는 데는 판단이 끼어들 때가 있습니다. 이 기준을 넓게 읽으면 어떤 변경도 못 하게 될 수 있습니다. 그렇게 읽으라는 기준은 아닙니다.

갈래를 가르는 변경 셋

같은 변경이라도 갈래마다 판정이 다를 수 있습니다. 아래 셋은 프로토콜 버퍼로 주고받는 경우입니다. 줄마다 걸리는 범위가 다릅니다. 첫 줄은 프로토콜 버퍼와 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법)을 전송 형식으로 가정한 규칙이라, 다른 전송 형식에서는 규칙이 조금 다를 수 있습니다. 둘째와 셋째 줄은 앞의 예시 소절과 같은 이진 와이어 형식 기준입니다.

변경 지키는 갈래 깨는 갈래
같은 패키지 안에서 정의를 다른 .proto 파일로 옮긴다 와이어 소스 — .proto 파일에서 C++·파이썬 같은 언어로 자동 생성한 코드에서 import 와 #include 가 엉뚱한 곳을 가리킨다
기존 열거형에 값을 하나 더한다 와이어 — 이진 와이어 형식에서 안전하다 소스 — 그 열거형을 빠짐없이 다루는 switch 가 있는 코드는 컴파일이 깨진다
int32 필드를 int64 로 바꾼다 와이어 — 다만 조건부다. 같은 데이터를 바꾸기 전과 뒤 양쪽에서 파싱할 수 있지만 그 파싱이 값을 잃을 수 있다 의미 — INT32_MAX 보다 큰 값을 int32 로 읽는 쪽은 윗자리 비트를 버린다

둘째 줄처럼 이진 와이어 형식에서 안전한 변경도 어느 언어의 애플리케이션 코드에는 깨는 변경일 수 있습니다. 그래서 와이어로는 안전한 변경도 공개 메시지에서는 피할 때가 있습니다.

대가

하위 호환을 지키기로 하면 그때부터 치르는 것이 셋입니다. 옛 방식을 오래 남겨 둡니다. 잘못 낸 것도 고치기 어렵습니다. 깨려면 문턱을 넘어야 합니다. 마지막 소절은 이 대가를 덜 치러도 되는 조건입니다.

옛 방식을 남겨 두는 기간

깨는 변경을 영영 못 하는 것은 아닙니다. 옛 쪽이 옮겨 갈 시간을 주고 깹니다. 응답에서 항목 하나를 없애는 변경이라면 아래 순서로 갑니다.

flowchart TD
    A["새 방식을 더한다 · 옛 방식은 남겨 둔다"] --> B["없앨 것을 미리 알린다"]
    B --> C["둘 다 도는 동안 쓰는 쪽이 옮겨 온다"]
    C --> D["옛 방식을 없앤다 · 하위 호환이 끊긴다"]

미리 알리는 둘째 단계를 폐기 예고(deprecation)라고 합니다. 끊는 때는 맨 끝입니다. 그 앞의 세 단계 내내 옛 방식과 새 방식을 둘 다 남겨 둬야 합니다.

이 기간이 길게 정해진 곳도 있습니다. 파이썬은 폐기 절차를 거치지 않는 한 연속한 두 릴리스 사이에 API 동작을 호환되지 않게 바꾸면 안 됩니다(must not). 알리지 않고 기능을 없애는 것도 안 됩니다. 폐기 절차를 밟을 때도 폐기 기간은 최소 2년을 채워야 합니다. 릴리스가 해마다 한 번이라 두 해면 판이 두 번 넘어갑니다. 파이썬 운영 위원회(steering council)는 이 정책에 예외를 줄 수 있습니다. 폐기 기간을 줄여 주기도 합니다. 예외는 위험하게 망가졌거나 보안에 구멍이 있는 기능처럼 극단적인 경우에만 나옵니다.

남겨 둔 방식이 늘면 확인할 경우는 더하기가 아니라 곱하기로 늘어납니다. 갈리는 데가 두 군데면 한 번의 처리가 두 군데를 다 지나므로 조합을 다 봐야 합니다. 요청을 받는 접점에는 옛 방식과 새 방식이 둘 다 서 있습니다. 디스크에 저장된 데이터는 형식을 두 번 바꿨다면 옛 형식과 그 사이 형식과 새 형식 셋이 함께 남습니다.

flowchart TD
    subgraph L1["요청을 받는 접점 · 방식이 둘"]
        A1["옛 방식"]
        A2["새 방식"]
    end
    subgraph L2["저장된 데이터 · 형식이 셋"]
        B1["옛 형식"]
        B2["그 사이 형식"]
        B3["새 형식"]
    end
    L1 --> L2 --> C["확인할 경우 2 × 3 = 6 · 갈리는 데가 하나 더 늘면 다시 곱해진다"]

고쳐도 깨는 변경

한 번 내보낸 동작은 잘못된 것이어도 지켜야 할 때가 있습니다. 리눅스 커널에서는 버그를 고친 변경이라도 누군가 기대던 동작을 깨면 회귀로 칩니다. 버그였는지 아닌지는 상관이 없습니다.

예외는 아주 적습니다. 하나는 근본적이고 커다란 보안 문제라 바꿀 수밖에 없었던 경우입니다. 좁은 쓰임 하나조차 살릴 수 없었을 때입니다. 다른 하나는 문제가 들어온 지 몇 해 뒤에야 보고돼 이제 대부분이 새 동작에 기대는 경우입니다.

잘못 지은 이름과 잘못 고른 기본값도 같은 처지입니다. 밖으로 내보낸 다음에는 고치는 것 자체가 깨는 변경이 됩니다. 그래서 공개 API 의 이름은 처음 내보내기 전까지만 값싸게 손볼 수 있습니다.

깨는 변경에 붙는 문턱

깨려면 그만한 까닭이 있어야 합니다. 파이썬에서 호환을 깨는 변경은 대체로 깨지는 것에 비해 이득이 커야 합니다(should). 영향받는 코드에서 고치기도 쉬워야 합니다. 그래서 더하기만 하는 변경도 이 문턱에 걸립니다. 옛 쪽이 쓰던 이름과 부딪히는 더하기가 그렇습니다.

더하는 변경 받아들여지나
서드파티 패키지와 같은 이름의 표준 라이브러리 모듈을 더한다 대개 받아들여지지 않는다
상속을 거쳐 서드파티 코드와 부딪히는 메서드나 속성을 더한다 받아들여질 가능성이 크다

서드파티 패키지는 표준 라이브러리에 안 든, 남이 만들어 올린 꾸러미입니다. 두 줄 모두 그 꾸러미가 쓰던 이름과 부딪히는 더하기입니다. 판정을 가르는 것은 위의 두 잣대입니다. 깨지는 것에 비해 이득이 큰가, 영향받는 코드에서 고치기 쉬운가입니다.

시맨틱 버저닝에서는 아주 작은 깨는 변경도 맨 앞 칸을 올립니다. 그때마다 쓰는 쪽이 올라가는 비용을 치릅니다. 그 비용은 클 수 있습니다. 그래서 기대는 코드가 많은 소프트웨어에는 깨는 변경을 가볍게 넣지 말아야 합니다(should not). 맨 앞 칸을 올려야 한다는 부담이 변경의 영향과 비용 대 이득을 따져 보게 만듭니다.

덜 챙겨도 되는 조건

언제나 끝까지 챙겨야 하는 것은 아닙니다. 아직 아무도 기대면 안 된다고 밝힌 것과 부르는 쪽이 좁은 것은 덜 챙깁니다. 다만 저장된 데이터에는 이 예외가 거의 없습니다.

경우 덜 챙겨도 되는 까닭과 단서
시맨틱 버저닝에서 맨 앞 칸이 0 인 판(0.y.z) 초기 개발 단계라 무엇이든 언제든 바뀔 수 있습니다(MAY). 공개 API 를 안정된 것으로 보지 말아야 합니다(SHOULD NOT)
임시(provisional)라고 밝힌 모듈 파이썬은 임시로 문서에 밝힌 모듈과 API 에 하위 호환 규칙을 적용하지 않습니다
비공개로 밝힌 이름 파이썬에서 밑줄(_)로 시작하는 이름(특수 이름은 빼고)과 비공개라고 문서에 적은 것은 언제든 어떻게든 바뀌거나 없어질 수 있습니다
부르는 쪽이 좁다 같은 팀이 짠 코드만 부르거나 갱신을 강제할 수 있는 API 는 호환 요건을 스스로 따져 보면 됩니다. 아무 때나 올라가는 바깥 쪽을 가정하지 않아도 되기 때문입니다. 서버가 여러 대면 겹치는 구간은 그래도 남습니다
옛 형식으로 저장된 데이터 덜 챙길 수 없습니다. 한 번에 새 형식으로 옮기지 않는 한 읽는 쪽은 계속 챙깁니다

마지막 줄의 「한 번에 옮기는 일」이 데이터베이스 마이그레이션입니다. 옮기기 전까지는 옛 데이터를 읽는 하위 호환이 계속 필요합니다.

관련 항목

하위 호환과 방향이 맞세워지는 개념

상위 호환성 · 양방향 호환성 · 상호운용성

하위 호환이 지키는 대상

소스 호환 · 바이너리 호환 · 와이어 호환 · 의미 호환 · 조건부 호환

하위 호환이 깨졌을 때 붙는 이름

하위 비호환 변경 · 브레이킹 체인지 · 회귀 · 성능 회귀 · 회귀 테스트 · 의존성 지옥

하위 호환이 지켜지는지 알리는 버전 규칙

시맨틱 버저닝 · 메이저 버전 · 마이너 릴리스 · 패치 릴리스 · 버전관리 · 안정성 수준

하위 호환을 챙겨야 하는 접점

API · API 설계 · 공개 API · 프로토콜 · ABI · 공유 라이브러리 · 동적 링크 · 생성 코드 · 사용자 공간

데이터 쪽에서 호환을 다루는 장치

스키마 · 스키마 진화 · 스키마 해석 · 스키마 레지스트리 · 직렬화 · 역직렬화 · 필드 · 필드 번호 · 예약 필드 · 알 수 없는 필드 · 기본값 · 데이터베이스 마이그레이션

두 버전이 겹치는 구간을 만드는 배포 방식

배포 · 카나리 배포 · 블루-그린 배포 · 단계적 출시 · 롤링 업데이트 · 버전 혼재 · 롤백

옛 방식을 걷어내는 단계

폐기 예고 · 사용 중단 경고 · 임시 API · 유예 기간 · 기능 플래그 · 이중 쓰기 · 확장-수축 패턴

하위 호환 규칙을 문서로 둔 프로젝트

Python · Linux · 커널 · Java · 프로토콜 버퍼 · Apache Avro · JSON

호환 약속을 적어 두는 문서

서비스 수준 계약 · 릴리스 노트 · 변경 로그 · 지원 기간 · 인터페이스 정의 언어

다른 이름: 하위 호환성 · backward compatibility · 하위호환 · 역방향 호환성 · backwards compatibility