[IT-정보] Go 변수 패턴과 상수 활용 가이드 – 실무에서 바로 쓰는 클린 코드 원칙

변수와 상수 개념을 시각화한 Go 프로그래밍 일러스트

변수와 상수, 왜 단순히 값을 담는 그릇 이상일까요

프로젝트 규모가 커지면서 어느 날 갑자기 코드의 흐름이 눈에 들어오지 않는 경험을 해보셨나요? 분명 어제까지 잘 작동하던 코드가 오늘 수정하고 나니 엉뚱한 곳에서 에러를 내뱉고 있다면, 그 원인은 로직 자체가 아니라 변수 관리의 부재에 있을 확률이 매우 높아요. 변수의 범위가 너무 넓거나, 무엇이 변하고 무엇이 고정되는지 명확하지 않으면 코드는 금세 스파게티처럼 꼬여버리거든요.

특히 Go 변수 패턴을 제대로 이해하지 못한 채 단순히 값을 할당하는 데만 급급하다 보면, 나중에 유지보수 단계에서 엄청난 비용을 치르게 돼요. 수만 줄의 코드 사이에서 특정 값이 어디서 바뀌었는지 찾아 헤매는 상황은 모든 개발자가 피하고 싶은 악몽과도 같죠. 그래서 우리는 단순히 값을 저장하는 법을 넘어, 어떻게 하면 안전하고 예측 가능한 코드를 설계할 것인지를 고민해야 해요.

이 글은 단순히 문법을 나열하는 교과서가 아니에요. 실무에서 마주치는 복잡한 상황들을 어떻게 변수와 상수를 활용해 깔끔하게 정리할 수 있는지, 그리고 Go 언어 특유의 관용적 표현은 무엇인지 깊이 있게 다룰 예정이에요. 이 가이드를 끝까지 읽고 나면, 여러분의 코드는 훨씬 더 견고하고 읽기 쉬워질 거예요.

💡 알아두기
이 글을 통해 얻을 수 있는 것들:

  • Go 언어의 변수 선언 방식과 올바른 선택 기준
  • 매직 넘버를 제거하고 코드를 안전하게 만드는 상수 활용법
  • 실무에서 바로 적용 가능한 Go 변수 패턴과 명명 규칙
  • 흔히 발생하는 변수 관련 실수와 이를 방지하는 체크리스트

Go 프로그래밍을 시작하기 전 알아야 할 기본 원칙

본격적으로 패턴을 배우기 전에, 우리가 다루는 재료들이 각각 어떤 성질을 가졌는지 명확히 정리할 필요가 있어요. Go에서는 변수와 상수를 다루는 방식이 매우 엄격하면서도 효율적이에요. 이 규칙들을 무시하면 컴파일 단계에서부터 빨간 줄을 마주하게 될 거예요.

변수와 상수의 근본적인 차이점

가장 먼저 이해해야 할 점은 변경 가능성이에요. 변수는 프로그램 실행 중에 값이 언제든 바뀔 수 있는 유동적인 공간이고, 상수는 한 번 결정되면 프로그램이 끝날 때까지 절대로 변하지 않는 약속이에요. 이 차이를 명확히 인지하는 것만으로도 설계의 절반은 성공한 셈이에요.

구분 변수 (Variable) 상수 (Constant)
값의 변경 실행 중 자유롭게 변경 가능 변경 불가능 (Read-only)
선언 방식 var 또는 := 사용 const 사용
주요 용도 상태 값, 계산 결과, 사용자 입력 설정값, 수학적 상수, 상태 코드
메모리 할당 런타임에 결정됨 컴파일 타임에 결정됨

선택을 위한 판단 기준

그렇다면 실무에서는 어떤 기준으로 이 둘을 선택할까요? 단순히 “바뀔 것 같으면 변수”라고 생각하면 위험해요. 데이터의 성격을 먼저 파악해야 해요. 만약 특정 값이 프로그램의 정책이나 비즈니스 로직의 기준이 된다면, 설령 나중에 그 값을 수정해야 하더라도 상수로 선언하는 것이 훨씬 안전해요. 반대로 사용자의 클릭 횟수나 API 응답 데이터처럼 매 순간 변하는 값은 당연히 변수로 처리해야 하죠.

⚠️ 주의
상수는 컴파일 타임에 그 값이 결정되어야 해요. 즉, 함수의 리턴 값이나 실행 중에 계산되는 복잡한 수식을 상수에 할당하려고 하면 컴파일 에러가 발생하니 주의하세요.

또한 Go 언어는 사용하지 않는 변수가 있으면 컴파일을 거부하는 아주 엄격한 특징을 가지고 있어요. 이는 불필요한 메모리 낭비를 막고 코드를 깨끗하게 유지하려는 Go의 철학이 반영된 결과예요. 따라서 변수를 선언할 때는 항상 그 용도를 명확히 정의하고, 실제로 사용할 준비가 되었을 때 선언하는 습관을 들이는 것이 좋아요.

실전 Go 변수 패턴과 상수 활용 전략

이제 기초를 넘어 실무에서 고수들이 사용하는 Go 변수 패턴에 대해 깊이 있게 파헤쳐 볼게요. 단순히 코드를 짜는 것을 넘어, 어떻게 하면 나중에 읽을 사람이 편안함을 느낄 수 있는 코드를 만들 수 있을지 고민하며 따라와 주세요.

STEP 1. 변수 선언의 두 얼굴: var와 :=

Go에서는 변수를 선언하는 두 가지 주요 방식이 있어요. 바로 var 키워드와 단축 선언 연산자인 :=예요. 이 둘을 상황에 맞게 구분해서 쓰는 것이 클린 코드의 시작이에요.

먼저 :=는 함수 내부의 지역 변수를 선언할 때 아주 유용해요. 코드가 간결해지고 타입 추론을 통해 가독성을 높여주죠. 하지만 모든 곳에 :=를 남발하면 안 돼요. 변수의 의도가 불분명해질 수 있기 때문이에요. 반면 var는 변수의 타입을 명시적으로 드러내고 싶을 때, 혹은 제로 값(Zero value)으로 초기화가 필요할 때 사용해요. 예를 들어, 함수의 리턴 값으로 사용될 변수나 구조체의 필드를 초기화할 때는 var를 사용하는 것이 훨씬 명확해요.

💡 알아두기
가장 권장되는 패턴은 다음과 같아요:

  • 함수 내부의 짧은 생명주기를 가진 변수 $\rightarrow$ :=
  • 패키지 레벨의 변수 또는 명시적 타입이 중요한 변수 $\rightarrow$ var
  • 구조체 필드나 초기화가 필요한 변수 $\rightarrow$ var

STEP 2. 매직 넘버를 제거하는 상수 패턴

코드 중간에 갑자기 등장하는 숫자나 문자열을 우리는 매직 넘버(Magic Number)라고 불러요. 예를 들어 if age >= 19라는 코드가 있다면, 여기서 19가 무엇을 의미하는지 한눈에 알기 어렵죠. 법적 성인 기준인지, 특정 서비스의 이용 기준인지 명확하지 않아요.

이를 해결하기 위해 const를 활용하세요. const AdultAge = 19라고 정의해 두면, 나중에 코드를 읽는 사람은 의도를 즉시 파악할 수 있고, 기준이 바뀌더라도 상수값 하나만 수정하면 전체 코드에 반영되죠. 또한 Go의 강력한 기능인 iota를 사용하면 연속된 숫자나 상태 값을 아주 우아하게 관리할 수 있어요. 상태 코드나 요일, 권한 레벨 같은 값들을 정의할 때 이보다 더 좋은 방법은 없답니다.

STEP 3. 관용적인 명명 규칙: CamelCase와 가시성

Go 개발자들 사이에는 암묵적인 약속이 있어요. 바로 카멜 케이스(CamelCase)를 사용한다는 점이에요. Snake\_case(밑줄 사용)는 Go의 표준 스타일이 아니니 주의해야 해요. 더 중요한 것은 변수 이름의 첫 글자를 어떻게 시작하느냐에 따라 변수의 가시성(Visibility)이 결정된다는 사실이에요.

첫 글자가 대문자이면 외부 패키지에서 접근할 수 있는 Exported 변수가 되고, 소문자이면 해당 패키지 내부에서만 사용하는 Unexported 변수가 돼요. 무분별하게 모든 변수를 대문자로 시작하면 캡슐화가 깨지고 내부 구현이 외부로 노출되어 버려요. 반대로 꼭 필요한 변수까지 소문자로 만들면 외부에서 사용할 수 없죠. 따라서 변수의 범위를 설계할 때 이 가시성 규칙을 항상 염두에 두어야 해요.

STEP 4. 패키지 레벨 변수의 위험성과 관리 전략

패키지 전체에서 공유되는 전역 변수는 편리해 보이지만, 사실 테스트하기 까다로운 코드의 주범이에요. 전역 변수는 어디서든 값을 바꿀 수 있기 때문에, 어떤 함수가 변수의 상태를 오염시켰는지 추적하기가 매우 어렵거든요. 특히 멀티스레딩 환경인 고루틴(Goroutine)을 사용할 때는 여러 곳에서 동시에 변수에 접근하면서 발생하는 데이터 경합(Race Condition) 문제가 치명적인 버그로 이어질 수 있어요.

이를 방지하기 위해 실무에서는 전역 변수를 최소화하고, 대신 의존성 주입(Dependency Injection) 패턴을 권장해요. 필요한 값을 변수로 직접 들고 있는 것이 아니라, 구조체의 필드로 포함시켜 함수나 메서드에 전달하는 방식이죠. 이렇게 하면 각 함수가 독립적인 상태를 가지게 되어 테스트가 쉬워지고, 코드의 예측 가능성도 비약적으로 높아져요.

STEP 5. 프로덕션 환경의 실전 시나리오

이해를 돕기 위해 간단한 결제 시스템의 시나리오를 구성해 볼게요. 결제 상태를 관리할 때 변수와 상수를 어떻게 조합하는지 살펴보세요.

먼저, 결제 상태는 변하지 않는 값들이므로 iota를 이용한 상수로 정의해요.
const ( Pending = iota; Success; Failed )
이렇게 하면 결제 상태의 종류가 명확해지죠. 반면, 실제 결제 요청이 들어온 금액이나 결제 시간은 매번 달라지므로 당연히 변수로 처리해야 해요. 이때 결제 금액은 var amount int64와 같이 선언하여 런타임에 값이 할당되도록 설계합니다.

이처럼 고정된 규칙은 상수로, 흐르는 데이터는 변수로 분리하는 것이 핵심이에요. 이 원칙만 지켜도 여러분의 코드는 훨씬 더 전문적이고 견고한 프로덕션 수준의 코드로 탈바꿈할 거예요.

자주 하는 실수와 해결법 및 자주 묻는 질문

실력이 늘기 위해서는 자신이 어디서 넘어지는지 아는 것이 중요해요. 많은 입문자가 공통적으로 겪는 실수들을 정리해 두었으니, 코드를 작성할 때마다 스스로 체크해 보세요.

자주 하는 실수와 해결법

변수 섀도잉(Shadowing) 발생 $\rightarrow$ 왜 발생하는가: if문이나 for문 내부에서 :=를 다시 사용하여 외부 변수를 가리는 현상이에요. 이 경우 겉보기엔 값이 바뀐 것 같지만 실제로는 새로운 지역 변수가 만들어진 것이라 버그를 유발해요. $\rightarrow$ ✅ 해결법: 중첩 범위 내에서는 새로운 변수를 만드는 대신 기존 변수에 값을 할당하는 = 연산자를 사용하세요.

사용하지 않는 변수 방치 $\rightarrow$ 왜 발생하는가: 디버깅을 위해 잠시 선언해 두었거나, 로직을 짜다가 불필요해진 변수를 지우지 않았을 때 발생해요. $\rightarrow$ ✅ 해결법: Go 컴파일러가 에러를 내뱉기 전에 미리 코드 리뷰나 린터(Linter)를 통해 정리하는 습관을 들이세요.

제로 값(Zero Value)에 대한 오해 $\rightarrow$ 왜 발생하는가: Go는 변수를 선언하면 자동으로 기본값(0, false, nil 등)을 할당해요. 하지만 포인터 변수의 경우 nil로 초기화되는데, 이를 확인하지 않고 메서드를 호출하면 패닉(Panic)이 발생해요. $\rightarrow$ ✅ 해결법: 포인터나 인터페이스를 사용할 때는 반드시 if ptr != nil과 같이 유효성 검사를 먼저 하세요.

상수에 복잡한 계산식 사용 $\rightarrow$ 왜 발생하는가: 상수는 컴파일 타임에 결정되어야 하는데, 함수 호출 결과 등을 상수에 넣으려고 시도하기 때문이에요. $\rightarrow$ ✅ 해결법: 런타임에 결정되는 값은 과감하게 var로 전환하세요.

전역 변수의 남용 $\rightarrow$ 왜 발생하는가: 어디서든 접근할 수 있어 편리하다는 생각 때문이지만, 이는 코드의 테스트 가능성을 완전히 망가뜨려요. $\rightarrow$ ✅ 해결법: 가능한 한 변수의 범위를 좁게 유지하고, 구조체 필드를 통해 데이터를 전달하세요.

자주 묻는 질문

Q. var와 :=를 구분하는 명확한 기준이 있을까요?

가장 쉬운 기준은 변수의 수명이에요. 함수 안에서 잠깐 쓰고 사라질 변수라면 :=를 쓰고, 함수 밖(패키지 레벨)에 있거나 선언과 동시에 값을 넣지 않고 나중에 할당할 변수라면 var를 쓰는 것이 관례예요.

Q. 상수는 왜 이렇게 엄격하게 다뤄야 하나요?

상수는 프로그램의 ‘불변의 진리’를 담고 있기 때문이에요. 상수가 중간에 바뀐다면 프로그램의 논리 자체가 붕괴될 수 있어요. 그래서 컴파일러도 상수의 안전성을 매우 엄격하게 검증하는 것이랍니다.

Q. iota를 사용할 때 주의할 점은 무엇인가요?

iota는 상수가 선언되는 줄(line)을 기준으로 증가해요. 만약 중간에 다른 상수가 끼어들거나 형식이 바뀌면 숫자가 꼬일 수 있으니, 한 덩어리의 const ( ... ) 블록 안에서 관리하는 것이 가장 안전해요.

Q. 가시성(Visibility)을 조절할 때 이름만 바꾸면 되나요?
네, 맞아요. 첫 글자를 대문자로 쓰면 외부에서 보이는 ‘공개(Public)’ 상태가 되고, 소문자로 쓰면 ‘비공개(Private)’ 상태가 됩니다. 아주 단순하지만 가장 강력한 규칙이에요.

Q. 매직 넘버를 상수로 바꿨는데도 코드가 지저분해 보여요.

그렇다면 상수의 이름이 너무 추상적이지 않은지 확인해 보세요. const Val = 10보다는 const MaxRetryCount = 10처럼 그 의미를 문장처럼 담아내는 것이 훨씬 좋은 패턴이에요.

깨끗한 코드를 위한 첫걸음, 요약과 실천

오늘 우리는 Go 언어에서 변수와 상수를 단순히 사용하는 법을 넘어, 어떻게 하면 이를 활용해 유지보수하기 좋은 코드를 설계할 수 있는지 알아보았어요. 변수는 흐르는 데이터의 통로로, 상수는 변하지 않는 기준점으로 명확히 구분하는 것이 핵심이었죠.

✅ 핵심 요약

  • 지역 변수는 :=를, 패키지/명시적 변수는 var를 사용하세요.
  • 매직 넘버는 반드시 const로 정의하여 의미를 부여하세요.
  • 변수 이름의 첫 글자로 가시성(Public/Private)을 제어하세요.
  • 전역 변수보다는 의존성 주입을 통해 데이터의 흐름을 제어하세요.
  • 상수는 컴파일 타임에 결정되는 값만 담을 수 있음을 기억하세요.
  • 변수 섀도잉(Shadowing)을 주의하여 의도치 않은 동작을 방지하세요.

이제 이론을 넘어 실제 코드로 옮길 차례예요. 오늘 바로 여러분의 프로젝트를 열어 다음 단계들을 실천해 보세요.

실행을 위한 액션 플랜

  • 오늘 할 일: 기존 코드에서 의미를 알 수 없는 숫자(매직 넘버)를 찾아 상수로 선언해 보기
  • 이번 주 할 일: 전역 변수를 사용 중인 함수를 찾아, 구조체 필드로 옮기는 리팩토링 진행하기
  • 실행 직전 할 일: 새로운 변수를 선언할 때, 이 변수가 함수 밖으로 나갈 필요가 있는지 가시성 검토하기

더 깊이 있는 Go 프로그래밍의 세계가 궁금하다면, Go 변수와 상수 완벽 가이드 글과 함께 읽어보시는 것을 추천드려요. 여러분의 클린 코드 여정을 응원할게요! 실무 체크리스트를 저장해 배포 전에 꼭 한 번 활용해 보세요.

댓글 남기기