PHpullh
언어 비교/Variable

LANGUAGE COMPARISON

Variable를 언어별로 비교하기

같은 개념을 실제 예제로 나란히 확인하고, 각 언어의 개별 설명으로 이어갈 수 있습니다.

변수 선언이 드러내는 언어의 기본값

변수를 만드는 문법은 어느 언어나 한 줄이지만, 그 한 줄이 정하는 것은 네 가지입니다. 이름이 언제 생기는가, 타입을 누가 정하는가, 값을 바꿀 수 있는가, 그리고 이 이름이 어디까지 보이는가. Python, Go, Java, PHP는 이 네 가지에 대해 서로 다른 기본값을 갖고 있고, 언어를 옮길 때 버그가 나는 지점도 대부분 문법이 아니라 이 기본값의 차이에서 나옵니다.

타입 축으로 보면 Python과 PHP는 값에 타입이 붙고 변수에는 붙지 않습니다. 같은 이름에 문자열을 넣었다가 정수를 넣어도 아무 일이 없습니다. Go와 Java는 변수에 타입이 고정되며, :=와 var는 타입을 없애는 문법이 아니라 컴파일러가 우변에서 타입을 추론해 고정하는 문법입니다. 이 구분을 놓치면 Java의 var를 동적 타입으로 오해하게 됩니다.

가변성 축은 더 미묘합니다. 네 언어 모두 재할당을 기본 허용하고, 상수는 별도 문법으로 따로 표시합니다. Go의 const는 컴파일 타임에 값이 정해지는 것만 담을 수 있고, Java의 final은 참조만 고정할 뿐 가리키는 객체 내부는 계속 바뀔 수 있습니다. "상수로 선언했으니 안전하다"는 판단이 가장 자주 틀리는 곳입니다.

Kotlin

기본 데이터 타입

Kotlin의 숫자, 문자, 불리언 타입과 리터럴 표현법. Java와 달리 암시적 타입 변환이 없습니다.

val age: Int       = 25
val salary: Long   = 50_000_000L   // _ 로 가독성 향상
val height: Float  = 175.5f
val weight: Double = 68.3
val isKotlin: Boolean = true
val grade: Char    = 'A'

// 타입 변환은 반드시 명시적으로
val x: Int = 100
val y: Long = x.toLong()    // ✅
val z: Double = x.toDouble()

// 다양한 진법 리터럴
val hex = 0xFF          // 255
val binary = 0b1010     // 10

fun main() {
    println("최대 Int: ${Int.MAX_VALUE}")
    println("최대 Long: ${Long.MAX_VALUE}")
}

Python

변수 & 타입 힌트

Python의 동적 타이핑과 Python 3.5+의 타입 힌트(Type Hints)로 코드 안정성을 높입니다.

# 동적 타이핑 — 타입 선언 불필요
name = "Python"
version = 3.12
is_awesome = True
count = 100

# 타입 힌트 (Python 3.5+) — 런타임 강제 아님, IDE/mypy용
salary: int = 50_000_000
user_name: str = "Alice"
ratio: float = 0.75
active: bool = True

# 여러 변수 동시 할당
x, y, z = 1, 2, 3
a = b = c = 0

# 타입 확인
print(type(name))     # <class 'str'>
print(type(version))  # <class 'float'>
print(isinstance(name, str))  # True

# 언더스코어로 숫자 가독성
population = 51_000_000
pi = 3.141_592_653

# None — null에 해당
result = None
print(result is None)  # True (== 대신 is 사용)

Go

변수 선언 — var & :=

Go의 두 가지 변수 선언 방식과 제로값(zero value) 초기화.

package main

import "fmt"

// 패키지 레벨 변수 — var만 가능
var (
	appName    = "MyApp"
	maxRetries = 3
	debug      bool // zero value: false
)

// 상수
const (
	Pi      = 3.14159
	Version = "1.0.0"

	// iota — 자동 증가 상수
	Sunday = iota
	Monday
	Tuesday
	Wednesday
)

func main() {
	// var 선언
	var name string = "Go"
	var age int     // zero value: 0
	var ratio float64 // zero value: 0.0

	// 단축 선언 := (함수 내부에서만)
	lang    := "Go"
	version := 1.22
	active  := true

	// 다중 선언
	x, y, z := 1, 2, 3

	// zero value 확인
	var s string  // ""
	var n int     // 0
	var b bool    // false
	var p *int    // nil

	fmt.Printf("name=%s age=%d ratio=%f
", name, age, ratio)
	fmt.Printf("lang=%s v=%.2f active=%t
", lang, version, active)
	fmt.Println(x, y, z)
	fmt.Printf("zeros: %q %d %t %v
", s, n, b, p)
}

Java

기본 타입 & 래퍼 클래스 & 오토박싱

Java의 원시 타입(primitive)과 래퍼 클래스(Wrapper)의 차이, 오토박싱/언박싱의 원리.

public class Types {
    public static void main(String[] args) {
        // 원시 타입 (primitive)
        int    i = 42;
        long   l = 100_000_000L;
        double d = 3.141592653589793;
        float  f = 3.14f;
        boolean b = true;
        char   c = 'A';
        byte   bt = 127;
        short  s = 32767;

        // 래퍼 클래스 (Wrapper)
        Integer  boxed  = 100;   // 오토박싱
        int      unboxed = boxed; // 언박싱

        // Integer 캐시: -128~127은 같은 객체
        Integer a = 127;
        Integer b2 = 127;
        System.out.println(a == b2);    // true (캐시)

        Integer c2 = 128;
        Integer d2 = 128;
        System.out.println(c2 == d2);   // false! (새 객체)
        System.out.println(c2.equals(d2)); // true  (값 비교)

        // 유용한 래퍼 메서드
        System.out.println(Integer.MAX_VALUE);    // 2147483647
        System.out.println(Integer.toBinaryString(42)); // 101010
        System.out.println(Integer.parseInt("FF", 16)); // 255
        System.out.println(Double.isNaN(0.0 / 0.0));   // true

        // 타입 캐스팅
        double pi = 3.99;
        int truncated = (int) pi;  // 3 (소수점 버림)
        long bigNum = 1_000_000_000L;
        int overflow = (int) bigNum; // 오버플로우 주의!
    }
}

JavaScript

숫자 변수 선언

let count = 1은 타입 표기 없이 기본 정수형 i32로 추론되고, 이어지는 if가 값을 만들어 label에 넣습니다. Rust에서 if는 문이 아니라 식이라 두 갈래가 같은 타입을 내놓아야 하며 여기서는 둘 다 &str입니다. 변수는 기본이 불변이라 값을 바꾸려면 mut를 붙여야 합니다.

// 숫자 변수 선언
let count = 1;
const label = count > 5 ? "big" : "small";
console.log(label);

TypeScript

숫자 변수 선언

let count = 1은 타입 표기 없이 기본 정수형 i32로 추론되고, 이어지는 if가 값을 만들어 label에 넣습니다. Rust에서 if는 문이 아니라 식이라 두 갈래가 같은 타입을 내놓아야 하며 여기서는 둘 다 &str입니다. 변수는 기본이 불변이라 값을 바꾸려면 mut를 붙여야 합니다.

// 숫자 변수 선언
let count: number = 1;
const label: string = count > 5 ? "big" : "small";
console.log(label);

C#

숫자 변수 선언

let count = 1은 타입 표기 없이 기본 정수형 i32로 추론되고, 이어지는 if가 값을 만들어 label에 넣습니다. Rust에서 if는 문이 아니라 식이라 두 갈래가 같은 타입을 내놓아야 하며 여기서는 둘 다 &str입니다. 변수는 기본이 불변이라 값을 바꾸려면 mut를 붙여야 합니다.

// 숫자 변수 선언
using System;

var count = 1;
Console.WriteLine(count > 5 ? "big" : "small");

C++

숫자 변수 선언

let count = 1은 타입 표기 없이 기본 정수형 i32로 추론되고, 이어지는 if가 값을 만들어 label에 넣습니다. Rust에서 if는 문이 아니라 식이라 두 갈래가 같은 타입을 내놓아야 하며 여기서는 둘 다 &str입니다. 변수는 기본이 불변이라 값을 바꾸려면 mut를 붙여야 합니다.

// 숫자 변수 선언
#include <iostream>

int main() {
    int count = 1;
    std::cout << (count > 5 ? "big" : "small") << "\n";
}

Rust

숫자 변수 선언

let count = 1은 타입 표기 없이 기본 정수형 i32로 추론되고, 이어지는 if가 값을 만들어 label에 넣습니다. Rust에서 if는 문이 아니라 식이라 두 갈래가 같은 타입을 내놓아야 하며 여기서는 둘 다 &str입니다. 변수는 기본이 불변이라 값을 바꾸려면 mut를 붙여야 합니다.

// 숫자 변수 선언
fn main() {
    let count = 1;
    let label = if count > 5 { "big" } else { "small" };
    println!("{}", label);
}

이름이 보이는 범위와 살아 있는 기간

스코프 규칙이 언어마다 다르다는 사실보다, 어긋나는 방향이 다르다는 점이 중요합니다. Python은 함수 단위로만 스코프를 나누기 때문에 if나 for 블록 안에서 만든 이름이 블록 밖에서도 살아 있습니다. 반복문이 한 번도 돌지 않으면 그 이름은 아예 존재하지 않아 NameError가 납니다. Go와 Java는 중괄호 단위로 스코프를 끊으므로 같은 코드가 컴파일 단계에서 걸립니다. PHP는 함수 안에서 바깥 변수가 자동으로 보이지 않아, 전역을 쓰려면 global이나 인자 전달을 명시해야 합니다. Python에 익숙한 사람이 PHP에서, PHP에 익숙한 사람이 Python에서 각각 반대 방향으로 헤매는 이유입니다.

언어를 옮길 때 실제로 밟는 지뢰

  • Go의 := 섀도잉 — 안쪽 블록에서 :=를 쓰면 바깥의 같은 이름을 덮는 새 변수가 생깁니다. if err := f(); err != nil 패턴을 중첩하다 보면 바깥 err가 끝까지 nil로 남는 일이 생깁니다. go vet과 섀도잉 린터를 켜 두는 편이 안전합니다.
  • Python의 늦은 바인딩 — 반복문 안에서 만든 함수가 반복 변수를 캡처하면 값이 아니라 이름을 붙잡습니다. 반복이 끝난 뒤 호출하면 전부 마지막 값을 봅니다. 기본 인자로 그 시점의 값을 묶어 두는 것이 관용적인 회피법입니다.
  • Java의 var 한계 — 지역 변수에만 쓸 수 있고, 초기화식이 없으면 쓸 수 없으며, 필드나 메서드 파라미터에는 쓰지 못합니다. 그리고 추론된 타입이 사람이 기대한 타입과 다를 수 있어, 다이아몬드 연산자와 함께 쓰면 의도치 않게 넓은 타입이 잡히기도 합니다.
  • PHP의 참조 대입 — 일반 대입은 값 복사처럼 동작하지만 &를 붙이면 같은 저장소를 공유합니다. 특히 foreach에서 참조로 순회한 뒤 같은 변수를 다시 쓰면 마지막 원소가 덮어써지는 유명한 함정이 있습니다. 순회가 끝나면 unset으로 끊는 것이 정석입니다.

규모가 커질수록 유리한 습관은 언어와 무관하게 같습니다. 기본을 불변으로 두고 꼭 필요할 때만 재할당을 허용하는 것입니다. Java는 지역 변수에도 final을 붙이고, Go는 재할당 대신 새 이름을 쓰고, Python은 모듈 수준 상태 대신 인자로 값을 넘기면 됩니다. 언어가 강제하지 않는 규칙을 팀이 강제하면 나중에 동시성 코드를 붙일 때 되돌아오는 비용이 크게 줄어듭니다.

변수 다음에 이어서 보기 좋은 주제는 값을 여러 개 담는 List 비교입니다. 대입이 복사인지 공유인지가 바로 여기서 결과를 바꿉니다. 언어별 기초 문법은 Python 기초, Go 기초, Java 기초, PHP 기초 문서에 정리돼 있습니다.

선언 위치가 만드는 차이

같은 변수라도 지역, 필드, 전역 중 어디에 두느냐에 따라 생명주기와 위험이 달라집니다. 지역 변수는 함수가 끝나면 사라지므로 가장 안전하고, 동시성에서도 스레드마다 별도로 존재합니다. 객체 필드는 그 객체가 살아 있는 동안 유지되며, 여러 스레드가 같은 객체를 공유하는 순간 동기화 대상이 됩니다. 전역이나 모듈 수준 변수는 프로세스 전체가 공유하기 때문에 테스트에서 특히 문제가 됩니다. 앞선 테스트가 남긴 값이 다음 테스트 결과를 바꾸는데, 실행 순서에 따라 성공과 실패가 오가서 원인을 찾기 어렵습니다.

Python의 contextvars, Java의 ThreadLocal, Go의 context는 모두 "전역처럼 편하게 접근하되 실행 흐름마다 분리된 값"이라는 같은 요구에서 나온 장치입니다. 다만 편의를 위해 이런 장치를 남용하면, 함수 시그니처만 봐서는 무엇에 의존하는지 알 수 없는 코드가 됩니다. 인자로 명시적으로 넘길 수 있는 값이라면 그렇게 하는 편이 나중에 테스트를 붙일 때 훨씬 수월합니다.

마지막으로 이름 자체도 언어별 관례가 있습니다. Go는 짧은 스코프에 짧은 이름을 쓰는 문화가 뚜렷하고, Java와 Kotlin은 camelCase, Python과 PHP는 snake_case가 표준입니다. 남의 코드와 섞이는 순간 이 관례를 지키는 편이 리뷰 비용을 줄여 주며, 대부분의 언어에 공식 포매터나 린터가 있어 논쟁 없이 자동으로 맞출 수 있습니다.