Kubernetes에서 코드 가독성 배우기
Wenhao Wang
Dev Intern · Leapcell

Kubernetes 코드베이스에서는 다양한 코드 캡슐화 이론과 기술이 실제로 적용됩니다. 결과적으로 코드를 읽는 동안 이해하고 싶은 내용을 직관적으로 추론할 수 있으며 코드 뒤에 숨겨진 의도를 빠르게 파악할 수도 있습니다.
Kubernetes 소스 코드 내에서 훌륭한 주석과 변수 명명법은 개발자가 설계 의도를 이해하는 데 더욱 도움이 됩니다. 그렇다면 Kubernetes 소스 코드의 주석과 변수 명명법에서 무엇을 배울 수 있을까요?
변수에 대하여
변수 이름: 길수록 좋은 것은 아닙니다.
변수 이름이 의미를 정확하게 표현해야 하는 경우 불가피한 문제는 변수 이름이 너무 길어질 수 있다는 것입니다. 그러나 매우 긴 변수 이름이 코드에 반복적으로 나타나면 마치 Belyozhsky와 Tversky라는 이름의 "운전자"가 많은 것처럼 느껴져서 머리가 아픕니다.
이러한 과도하게 정확하고 반복적인 명명으로 인한 혼란을 피하기 위해 의미적 컨텍스트를 활용하여 간결한 변수 이름으로 더 많은 의미를 표현할 수 있습니다.
func (q *graceTerminateRSList) remove(rs *listItem) bool{ //... }
Kubernetes의 graceTerminateRSList
구조체 정의에서 graceTerminateRealServerList
라고 쓸 필요가 없습니다. 왜냐하면 해당 listItem
을 참조할 때 이미 전체 이름이 내부에 정의되어 있기 때문입니다.
type listItem struct { VirtualServer *utilipvs.VirtualServer RealServer *utilipvs.RealServer }
따라서 이 컨텍스트에서 rs
는 replicaSet
이나 다른 것이 아닌 realServer
만 참조할 수 있습니다. 이러한 모호성이 발생할 가능성이 있다면 이와 같은 약어를 사용해서는 안 됩니다.
또한 graceTerminateRSList
의 remove
메서드에서 removeRS
또는 removeRealServer
라고 이름을 지정할 필요가 없습니다. 왜냐하면 매개변수 시그니처에 이미 rs *listItem
이 있기 때문입니다. 따라서 이 메서드는 rs
만 제거할 수 있습니다. 메서드 이름에 rs
를 추가하는 것은 중복될 것입니다.
이름을 지정할 때 더 짧은 이름이 더 많은 의미를 담도록 노력하십시오.
func CountNumber(nums []int, n int) (count int) { for i := 0; i < len(nums); i++ { // 할당하려면 v := nums[i] if nums[i] == n { count++ } } return } func CountNumberBad(nums []int, n int) (count int) { for index := 0; index < len(nums); index++ { value := nums[index] if value == n { count++ } } return }
index
는 i
보다 더 많은 정보를 전달하지 않으며 value
는 v
보다 낫지 않습니다. 따라서 이 예에서는 약어를 대체로 사용할 수 있습니다. 그러나 약어가 항상 유익한 것은 아닙니다. 약어 사용 여부는 특정 시나리오에서 모호성이 발생하는지 여부에 따라 달라집니다.
변수 이름은 모호성을 피해야 합니다.
이벤트에 참여하는 사용자 수를 표현하고 싶다고 가정합니다(int
유형). user
또는 users
를 사용하는 것보다 userCount
를 사용하는 것이 좋습니다. 왜냐하면 user
는 사용자 객체를 참조할 수 있고 users
는 사용자 객체의 슬라이스를 참조할 수 있기 때문입니다. 둘 중 하나를 사용하면 모호성이 발생할 수 있습니다.
다른 예를 살펴보겠습니다. 일부 컨텍스트에서 min
은 "최소값" 또는 "분"을 의미할 수 있습니다. 특정 시나리오에서 두 가지가 혼동하기 쉬운 경우 약어 대신 전체 단어를 사용하는 것이 좋습니다.
// 최저 가격과 남은 프로모션 시간 계산 func main() { // 제품 가격 목록 prices := []float64{12.99, 9.99, 15.99, 8.49} // 각 제품의 남은 프로모션 시간(분) remainingMinutes := []int{30, 45, 10, 20} // min := findMinPrice(prices) // 변수 "min": 최저 가격을 나타냅니다. minPrice := findMinPrice(prices) fmt.Printf("가장 낮은 제품 가격: $%.2f\n", min) // min = findMinTime(remainingMinutes) // 변수 "min": 가장 짧은 남은 시간을 나타냅니다. remainingMinute := findMinTime(remainingMinutes) fmt.Printf("가장 짧은 남은 프로모션 시간: %d분\n", min) }
이 예에서 min
은 가장 낮은 제품 가격을 참조할 수 있지만 프로모션의 가장 짧은 남은 시간을 참조할 수도 있습니다. 이와 같은 경우 최소 가격을 찾고 있는지 최소 시간을 찾고 있는지 명확하게 구분할 수 있도록 약어를 피하십시오.
동일한 의미를 가진 변수 이름은 일관성을 유지해야 합니다.
프로젝트 전체에서 동일한 의미를 나타내는 변수 이름은 가능한 한 일관성을 유지해야 합니다. 예를 들어 프로젝트에서 사용자 ID를 UserId
로 작성한 경우 변수를 복사하거나 재사용할 때 다른 곳에서 Uid
로 변경해서는 안 됩니다. 이는 UserId
와 Uid
가 동일한 것을 참조하는지 여부에 대한 혼란을 야기할 수 있기 때문입니다.
이 문제를 과소평가하지 마십시오. 때로는 여러 시스템에 모두 사용자 ID가 있기 때문에 모두 저장해야 할 수도 있습니다. 차별화를 위해 접두사를 추가하지 않으면 필요할 때 어떤 것을 사용해야 하는지 알기가 어려울 것입니다.
예를 들어 사용자 A가 구매자이고 판매자로부터 제품을 구매했으며 운전자가 제품을 배송한다고 가정합니다.
여기서 구매자, 판매자 및 운전자라는 세 개의 사용자 ID가 있습니다.
이때 모듈 접두사 BuyerId
, SellerId
및 DriverId
를 추가하여 구분할 수 있습니다.
그리고 가능한 한 이러한 접두사를 약어로 줄여서는 안 됩니다. 이미 충분히 간결하기 때문입니다. 함수 매개변수 SellerId
를 Sid
로 줄이면 나중에 상점 ID(ShopId
)를 도입할 때 Sid
가 SellerId
를 참조하는지 ShopId
를 참조하는지 궁금할 수 있습니다. 판매자가 ShopId
에 SellerId
를 채우는 경우 생산 환경에서 버그가 발생할 수 있습니다.
주석에 대하여
주석은 코드가 표현할 수 없는 내용을 설명해야 합니다.
함수의 내부 로직이 너무 복잡하면 주석을 사용하여 코드 판독자가 세부 사항을 파고드는 데 소요되는 시간을 절약할 수 있으므로 시간을 절약하고 코드를 안내하는 역할을 합니다.
Kubernetes의 동기화 Pod 루프는 상당히 복잡하므로 주석을 사용하여 메서드를 설명합니다.
// syncLoopIteration은 다양한 채널에서 읽고 주어진 처리기에 포드를 디스패치합니다. // // ...... // // 염두에 두고 특정 순서 없이 다양한 채널이 다음과 같이 처리됩니다. // // - configCh: 구성 변경에 대한 포드를 이벤트 유형에 적합한 처리기 콜백으로 디스패치합니다. // - plegCh: 런타임 캐시를 업데이트합니다. 포드를 동기화합니다. // - syncCh: 동기화를 기다리는 모든 포드를 동기화합니다. // - housekeepingCh: 포드 정리를 트리거합니다. // - 상태 관리자: 실패했거나 하나 이상의 컨테이너가 상태 확인에 실패한 포드를 동기화합니다. func (kl *Kubelet) syncLoopIteration(ctx context.Context, configCh <-chan kubetypes.PodUpdate, handler SyncHandler, syncCh <-chan time.Time, housekeepingCh <-chan time.Time, plegCh <-chan *pleg.PodLifecycleEvent) bool { }
맨 처음에는 이 주석은 이 함수가 다양한 채널에서 받은 Pod 정보를 처리하고 적절한 처리 로직으로 디스패치한다고 설명합니다. 또한 주석은 각 채널을 처리하기 위한 일반적인 로직을 요약합니다.
코드가 특히 복잡하지 않고 코드 자체를 읽는 것만으로도 의도를 이해할 수 있다면 주석을 추가할 필요가 없습니다.
다음 예에서는 사용자가 로그인하면 일반 VIP는 기본 10점을 받고 VIP 사용자는 추가로 100점을 받습니다.
const ( basePoints = 10 vipBonus = 100 ) type User struct { IsVIP bool Points int } // SignIn은 사용자 로그인을 처리하고 VIP 상태에 따라 점수를 높입니다. func (u *User) SignIn() { pointsToAdd := basePoints if u.IsVIP { pointsToAdd += vipBonus } u.Points += pointsToAdd }
코드 자체에서 이미 함수의 의도를 보여주고 로직이 간단하므로 추가 주석이 필요하지 않습니다.
동시에 비즈니스 요구 사항이 여전히 불안정한 경우 모호성을 유발할 수 있는 주요 작업에 대해서만 주석을 추가하는 것이 좋습니다. 비즈니스 로직이 자주 변경되고 내부 로직이 업데이트되었지만 주석이 업데이트되지 않으면 판독자를 오도할 수 있습니다.
그러나 코드가 지나치게 복잡한 경우 단순히 주석에 의존하기보다는 코드를 리팩터링하거나 메서드로 추상화하는 것이 더 좋습니다. Kubernetes에서 Kubelet이 구성 신호(configCh
)를 처리하는 예를 살펴보겠습니다.
func (kl *Kubelet) syncLoopIteration(...) bool { select { case u, open := <-configCh: switch u.Op { case kubetypes.ADD: klog.V(2).InfoS("SyncLoop ADD", "source", u.Source, "pods", klog.KObjSlice(u.Pods)) handler.HandlePodAdditions(u.Pods) case kubetypes.UPDATE: klog.V(2).InfoS("SyncLoop UPDATE", "source", u.Source, "pods", klog.KObjSlice(u.Pods)) handler.HandlePodUpdates(u.Pods) case kubetypes.REMOVE: klog.V(2).InfoS("SyncLoop REMOVE", "source", u.Source, "pods", klog.KObjSlice(u.Pods)) handler.HandlePodRemoves(u.Pods) case kubetypes.RECONCILE: klog.V(4).InfoS("SyncLoop RECONCILE", "source", u.Source, "pods", klog.KObjSlice(u.Pods)) handler.HandlePodReconcile(u.Pods) case kubetypes.DELETE: klog.V(2).InfoS("SyncLoop DELETE", "source", u.Source, "pods", klog.KObjSlice(u.Pods)) handler.HandlePodUpdates(u.Pods) case kubetypes.SET: // TODO: 이것을 지원하고 싶습니까? klog.ErrorS(nil, "Kubelet은 스냅샷 업데이트를 지원하지 않습니다.") default: klog.ErrorS(nil, "잘못된 작업 유형을 받았습니다.", "operation", u.Op) } } }
각 이벤트 작업을 자체 메서드로 추상화하면 모든 로직이 switch 분기 내에 평면적으로 배치되는 것을 방지할 수 있습니다.
예를 들어 kubetypes.ADD
에 대한 작업은 HandlePodAdditions
메서드에 캡슐화되어 전체 코드가 디렉터리와 유사하게 됩니다.
Pod 추가 프로세스를 이해하려면 HandlePodAdditions
메서드를 직접 살펴보면 됩니다.
다른 예를 살펴보겠습니다. Kubernetes의 BoundedFrequencyRunner
에서 Run
메서드에 대한 주석입니다.
// 가능한 한 빨리 함수를 실행합니다. 루프가 실행되고 있지 않은 동안에 이 함수가 호출되면 호출이 무기한 연기될 수 있습니다. // 기본 함수를 호출하기 위한 대기열에 있는 요청이 이미 있는 경우 삭제될 수 있습니다. 지금부터 가능한 한 빨리 기본 함수를 호출하려고 시도하는 것만 보장됩니다. func (bfr *BoundedFrequencyRunner) Run() { select { case bfr.run <- struct{}{}: default: } }
여기서 주석은 메서드 본문에서 직접 볼 수 없는 두 가지 사항을 알려줍니다.
Loop
가 실행되고 있지 않으면 이를 처리할 소비자가 없기 때문에 실행 신호가 무기한 연기됩니다.- 대기열에 있는 요청이 이미 있는 동안
Run
이 호출되면 새 신호가 삭제될 수 있습니다. 메서드는 지금부터 가능한 한 빨리 함수를 _시도_만 할 뿐입니다.
이러한 두 가지 정보는 코드 자체를 읽는 것만으로는 알 수 없습니다. 작성자는 주석을 통해 이러한 숨겨진 세부 정보를 알려주어 이 메서드에 대한 중요한 사용 고려 사항을 빠르게 이해하도록 도와줍니다.
이제 정규식 컴파일과 관련된 예를 살펴보겠습니다.
func ExecRegex(value string, regex string) bool { regex, err := decodeUnicode(regex) if err != nil { return false } if regex == "" { return true } rx := regexp.MustCompile(regex) return rx.MatchString(value) }
여기에서 전달된 정규식이 decodeUnicode
에 의해 처리되는 것을 볼 수 있습니다. 이 메서드를 살펴보겠습니다.
func decodeUnicode(inputString string) (string, error) { re := regexp.MustCompile(`\\u[0-9a-fA-F]{4}`) matches := re.FindAllString(inputString, -1) for _, match := range matches { unquoted, err := strconv.Unquote(`"` + match + `"`) if err != nil { return "", err } inputString = strings.Replace(inputString, match, unquoted, -1) } return inputString, nil }
이 메서드만 보면 전달된 문자열을 이스케이프하는 것을 알 수 있지만 왜 필요한지 또는 그렇게 하지 않으면 어떤 일이 발생하는지 알 수 없습니다. 이로 인해 향후 유지 관리 담당자는 혼란스러워합니다. 이제 해당 주석을 추가하고 메서드를 다시 검토해 보겠습니다.
// decodeUnicode는 CJK 문자와 일치시키기 위해 [\[u4e00-\[u9fa5]와 같은 정규식 패턴을 전달할 때 패닉을 피하기 위해 정규식 문자열을 이스케이프합니다. func decodeUnicode(inputString string) (string, error) { //... }
이제 모든 것이 명확해졌습니다. Go가 CJK 문자에 대한 정규식을 구문 분석할 때 정규식 문자열이 이스케이프되지 않으면 [\u4e00-\u9fa5]
와 같은 패턴을 전달하면 패닉이 발생할 수 있습니다.
이 한 줄의 주석은 디코딩 뒤에 숨겨진 의도를 즉시 명확히 할 뿐만 아니라 나중에 개발자가 이스케이프된 문자열을 부주의하게 조작하지 않도록 경고하여 새로운 버그를 방지합니다.
코드에서 변수 이름과 주석은 자연어에 가장 가까운 부분이므로 이해하기 가장 쉽기도 합니다. 이러한 측면을 신중하게 고려하면 가독성이 크게 향상됩니다.
빈 줄도 일종의 주석입니다. 코드를 논리적으로 분할하여 판독기에게 로직 섹션이 완료되었음을 나타냅니다.
예를 들어 위의 decodeUnicode
메서드에서 빈 줄은 전처리해야 하는 일치하는 정규 표현식, 주 처리 루프 및 최종 반환 구문을 구분합니다. 시각적으로 코드를 세 섹션으로 나누어 더욱 직관적이고 명확하게 만듭니다.
Kubernetes에서 graceTerminateRSList
가 RS가 존재하는지 확인하는 예를 살펴보겠습니다.
func (q *graceTerminateRSList) exist(uniqueRS string) (*listItem, bool) { q.lock.Lock() defer q.lock.Unlock() if rs, ok := q.list[uniqueRS]; ok { return rs, true } return nil, false }
여기서 잠금 로직 뒤에 빈 줄이 삽입되어 잠금/해제 작업이 완료되었음을 나타내고 다음 섹션은 존재 여부를 확인하기 위한 것입니다. 이렇게 하면 로직이 시각적으로 분리되므로 판독기는 후반 로직에 더 집중하고 요점을 빠르게 파악할 수 있습니다.
func (q *graceTerminateRSList) exist(uniqueRS string) (*listItem, bool) { q.lock.Lock() defer q.lock.Unlock() if rs, ok := q.list[uniqueRS]; ok { return rs, true } return nil, false }
빈 줄을 제거하면 코드로 점프할 때 메서드의 초점을 빠르게 보기가 훨씬 더 어려워집니다.
사용하지 않는 코드는 주석 처리만 할 것이 아니라 삭제해야 합니다.
대부분의 경우 사람들은 나중에 편리하게 재사용하기를 바라기 때문에 코드를 주석 처리합니다.
그러나 또 다른 상황이 있습니다. 나중에 사용할 것이라고 생각하고 주석 처리했지만 시간이 되면 코드가 현재 버전과 더 이상 호환되지 않고 버그가 발생할 수 있습니다. 어쨌든 다시 작성해야 합니다.
따라서 처음부터 사용하지 않는 코드를 삭제하는 것이 좋습니다. 나중에 다시 필요한 경우 git commit
기록을 사용하여 코드를 찾고 다시 작성할 수 있습니다. 이 시점에서 주석 처리된 코드가 많아서 산만해지는 대신 테스트 중에 코드를 최적화할 수 있습니다.
이 원칙은 Kubernetes YAML 파일을 작성할 때도 적용됩니다.
spec: spec: # ... # server-conf라는 configMap 볼륨을 마운트합니다. # - name: server-conf-map # configMap: # name: server-conf-map # items: # - key: k8s-conf.yml # path: k8s-conf.yml # defaultMode: 511
이와 같은 YAML 정의를 읽을 때 쓸모없는 주석 블록이 많으면 방해가 됩니다. server-conf-map
이 이미 삭제된 경우 주석은 더 혼란스럽습니다. 따라서 자체 프로젝트에서 코드가 외부 당사자에 의해 의존되지 않는 경우 코드를 삭제하고 나중에 필요한 경우 git를 사용하여 복구하십시오.
프로젝트에 타사에서 의존하는 코드가 포함된 경우 코드를 삭제하는 것보다 "deprecated" 주석을 추가하는 것이 좋습니다. 때로는 다른 사람에게 패키지를 제공하는 경우 코드를 완전히 삭제하면 업그레이드할 때 많은 오류가 발생할 수 있습니다. 이 경우 사용자가 최신 코드로 전환하도록 안내해야 합니다.
대신 사용할 항목과 전달할 매개변수를 지정하는 Deprecated
주석을 추가할 수 있습니다. gRPC에서 WithInsecure
메서드에 대한 폐기 주석을 살펴보겠습니다.
// Deprecated: WithTransportCredentials 및 insecure.NewCredentials()를 대신 사용하십시오. // 대신 사용하십시오. 1.x 버전에서 계속 지원됩니다. func WithInsecure() DialOption { return newFuncDialOption(func(o *dialOptions) { o.copts.TransportCredentials = insecure.NewCredentials() }) }
이 주석은 대신 사용할 메서드를 명확하게 알려줍니다. 또한 WithTransportCredentials
에는 매개변수가 필요하므로 작성자는 매개변수를 전달하는 방법을 정확하게 알려줍니다.
이렇게 하면 사용자가 이전 메서드를 교체하고 새로운 기능을 채택하는 것이 훨씬 쉬워집니다.
결론적으로
이 문서에서 배운 내용을 검토해 보겠습니다.
- 컨텍스트에 따라 변수 및 함수 이름에 적절한 양의 약어를 사용하는 방법.
- 주석은 코드 자체가 표현할 수 없는 내용을 표현해야 합니다.
- 적절한 안내 주석을 사용하여 향후 기여자가 코드를 읽는 데 도움을 주지만 더 나은 방법은 메서드 추출을 사용하여 코드를 자체적으로 표현하도록 만드는 것입니다.
- 주석 처리하는 대신 제거할 수 있는 코드를 삭제합니다. 타사 종속성으로 인해 주석 처리할 수 없는 코드의 경우 명확한 폐기 주석을 사용하여 사용자가 새 메서드로 빠르게 전환하도록 돕습니다.
Leapcell은 Go 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하십시오.
무료로 무제한 프로젝트 배포
- 사용량에 대해서만 지불하십시오. 요청도 없고 요금도 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하십시오.
- 예: $25는 평균 응답 시간이 60ms인 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
간편한 확장성 및 고성능
- 쉬운 동시성 처리를 위한 자동 확장.
- 운영 오버헤드가 없습니다. 빌드에만 집중하십시오.
설명서에서 자세히 알아보십시오!
X에서 팔로우하세요: @LeapcellHQ