[Go 공식문서 한국어 정리] ①4⑨. 모듈 호환성 유지하기
모듈 호환성 유지하기
https://go.dev/blog/module-compatibility
새로운 메이저 버전을 릴리스하는 것은 사용자에게 부담을 줍니다. 이 글에서는 기존 API를 깨뜨리지 않고 새 기능을 추가하는 기법들을 소개합니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
① 서론
모듈은 시간이 지남에 따라 진화합니다. 하위 호환성을 깨는 변경은 메이저 버전 증가가 필요합니다. 새 메이저 버전은 사용자가 코드를 수정해야 하므로, 가능한 한 하위 호환 방식으로 변경하는 것이 좋습니다.
② 핵심 개념
1. 추가만 하고, 변경하거나 제거하지 마라: API 확장의 황금률입니다.
2. 함수 시그니처는 변경 불가: 새 인자를 추가하면 기존 코드가 깨집니다.
3. 구조체는 확장 가능: 제로값이 의미 있는 새 필드를 추가할 수 있습니다.
4. 인터페이스 확장 주의: 직접 추가하면 모든 구현체가 깨집니다.
③ 주요 내용 상세
새 함수 추가
기존 함수를 변경하는 대신 새 함수를 추가합니다.
예: database/sql의 Query → QueryContext
QueryContext를 추가하고 기존 Query는 QueryContext를 호출하도록 구현합니다.
옵션 구조체
func Dial(network, addr string, config *Config) (*Conn, error)
- nil을 전달하면 기본값을 사용합니다.
- 새 설정은 Config 구조체에 필드를 추가하면 됩니다.
함수형 옵션(Functional Options)
grpc.Dial("target", grpc.WithAuthority("auth"), grpc.WithBlock())
- 가변 인자로 옵션 함수를 전달합니다.
- 확장성이 높지만 패키지 이름을 반복해야 하는 단점이 있습니다.
인터페이스 확장 전략
archive/tar.Reader는 io.Reader를 받지만, 효율을 위해 Seek가 필요했습니다.
- io.Seeker 인터페이스를 별도로 정의하고 타입 단언으로 지원 여부를 확인합니다.
- rs, ok := r.r.(io.Seeker); ok { ... }
- 이 방식은 구현체를 깨뜨리지 않습니다.
구체 타입 반환
생성자는 인터페이스 대신 구체 타입을 반환하는 것이 좋습니다.
- 구체 타입에는 나중에 메서드를 추가핟도 사용자 코드가 깨지지 않습니다.
- 인터페이스는 사용자가 구현할 수 있어 메서드 추가가 어렵습니다.
구조체 호환성
- 새 필드를 추가할 때 제로값이 기존 동작을 보존해야 합니다.
- 모든 필드가 비교 가능(comparable)하면 구조첵도 비교 가능합니다. 비교 불가능한 필드를 추가하면 비교 코드가 깨집니다.
- 비교를 원치 않으면 _ [0]func() 같은 필드를 추가합니다.
동작 변경은 옵트인으로
json.Decoder의 DisallowUnknownFields처럼, 새 동작은 메서드 호출로 활성화하고 기본값은 기존 동작을 유지합니다.
④ 실전 활용
- API 설계 시 미래 확장을 고려하여 Config 구조첵이나 옵션 패턴을 사용하세요.
- 인터페이스를 공개할 때는 사용자가 구현할 가능성을 고려하세요.
- 구조체에 필드를 추가하기 전에 제로값이 기존 동작을 보존하는지 확인하세요.
- 테스트에서 구조체 비교 가능성을 검증하세요.
⑤ 정리
대부분의 경우 "추가만 하고 변경하지 마라"는 원칙만 지켜도 하위 호환성을 유지할 수 있습니다. 함수 시그니처는 절대 변경하지 말고, 인터페이스 확장은 타입 단언으로 우회하며, 구조체는 제로값이 의미 있는 필드를 추가하세요. 정말 필요할 때만 메이저 버전을 올리세요.
#Go #Golang #Modules #호환성 #Compatibility #API설계 #하위호환 #공식문서

오뉴노노 님의 최근 댓글
ㅋㅋㅋㅋㅋ 2019 01.14 잘 읽었습니다 2018 12.30 포인트가 없어서 아직 시작을 못하고있는데요! 글은 잘 읽었습니다! 포인트 쌓고 도전할거에요 2018 12.30