값을 옮기면 무슨 일이 생기나
Esc

    값을 옮기면 무슨 일이 생기나

    2026년 10월 26일 수정–C#기초

    byte 변수에 300을 넣었더니 44가 나오고, double 에 10 / 3 을 담았는데 3이 나옵니다. 둘 다 에러도 경고도 없이 일어나서, 모르면 버그를 한참 찾게 됩니다.

    변수는 타입이 정한 크기의 메모리 칸입니다(byte 는 1바이트, int 는 4바이트). 타입이 다른 변수로 값을 옮기면 칸의 크기가 바뀌고, 이것을 형 변환이라고 부릅니다. int 로 센 점수를 float 를 받는 유니티 API 에 넘기거나, 실수로 계산한 값을 정수 칸에 담을 때마다 일어납니다.

    방향은 둘뿐입니다.

    • 넓은 칸으로 갈 때는 남는 자리를 채웁니다. 잃는 게 없습니다
    • 좁은 칸으로 갈 때는 안 들어가는 만큼 잘라냅니다. 잃는 게 있습니다

    C# 이 둘을 다르게 다루는 이유가 여기 있습니다. 앞은 알아서 해주고, 뒤는 직접 쓰라고 요구합니다.

    넓은 칸으로 갈 때는 채운다

    byte 에 든 5를 int 에 넣어봅니다. 타입을 바꿔가며 보세요.

    05 하나였던 것이 05 00 00 00 이 됩니다. 값은 그대로고 자리만 늘었습니다. 메모리에는 작은 자리부터 낮은 주소에 놓여서 05 가 먼저 보일 뿐, 수로 읽으면 00 00 00 05 입니다. 앞자리가 0으로 채워진 것입니다. 잃는 게 없으니 C# 이 알아서 해줍니다.

    byte small = 5;
    int big = small;      // 그냥 됩니다
    double d = big;       // 이것도 됩니다

    이걸 암시적 변환이라고 합니다. 규칙은 하나입니다.

    받는 타입이 보내는 타입의 모든 값을 표현할 수 있을 때만 자동입니다.

    byte(0~255)의 어떤 값도 int(±21억)에 들어갑니다. 그래서 자동입니다. 반대는 아닙니다. int 의 21억은 byte 에 안 들어갑니다.

    자동인데 정확하지는 않은 경우

    여기에 함정이 하나 있습니다. int 를 float 에 넣는 것은 자동입니다.

    int n = 16_777_217;      // 2의 24제곱 + 1
    float f = n;             // 경고 없이 됩니다
    Console.WriteLine(f);    // 16777216

    float 의 범위는 int 보다 훨씬 넓습니다. 그래서 규칙상 자동이 됩니다. 그런데 float 는 32비트를 부호, 소수점 위치, 숫자의 자릿수로 나눠 쓰고, 자릿수는 2진수 24자리만큼만 담을 수 있습니다. 10진수 세 자리로 999까지 세듯이, 2진수 24자리로 정확히 셀 수 있는 정수는 224 = 16,777,216 까지입니다. 그다음 16,777,217 은 담을 자리가 없어서 가장 가까운 값인 16,777,216 으로 저장됩니다. 범위는 넉넉해도 자릿수가 모자라서 1이 사라졌습니다.

    암시적 변환이 보장하는 것은 범위지 정밀도가 아닙니다. 큰 정수를 실수에 넣을 때는 double 을 쓰세요.

    좁은 칸으로 갈 때는 잘라낸다

    이번엔 300을 넣어봅니다. byte 를 눌러보세요.

    300 을 넣었는데 읽으면 44 가 나옵니다. 비트를 보면 이유가 보입니다. 컴퓨터는 수를 0과 1만으로 적습니다(2진수). 그 한 자리가 비트이고, 1바이트는 비트 8개입니다. 300 = 256 + 32 + 8 + 4 라서 2진수로 1 0010 1100, 아홉 자리가 됩니다.

    300  =  1 0010 1100    (9비트가 필요합니다)
    byte =    0010 1100    (8칸에 안 들어가는 앞자리가 버려집니다)
          =   44

    맨 앞의 1(256 자리)이 잘려 나가서 300 - 256 = 44 가 남았습니다.

    이건 저절로 되지 않습니다. 직접 써야 합니다. 값 앞에 괄호로 타입을 적는 (byte)n 같은 것을 명시적 변환, 흔히 캐스트라고 부릅니다.

    int n = 300;
    byte b = n;          // 컴파일 에러
    byte b = (byte)n;    // 44

    (byte) 는 부탁이 아닙니다.

    (byte) 는 “잘려도 좋으니 넣어라” 는 지시입니다. 컴파일러는 그 말을 듣고 검사를 그만둡니다. 범위를 보장하는 건 그때부터 개발자 몫입니다.

    값이 칸을 넘쳐 잘려도(오버플로) 아무 신호도 없습니다. 에러도 경고도 나지 않습니다. 넘칠 것 같으면 checked 로 감싸거나 넣기 전에 범위를 검사하세요. checked 안에서는 넘치는 순간 예외가 나서 바로 알 수 있습니다. 예외는 프로그램이 그 줄에서 멈추고 무엇이 잘못됐는지 알리는 오류입니다.

    int n = 300;
    byte b = checked((byte)n);   // OverflowException: 잘리기 전에 멈춥니다

    (int) 는 반올림이 아니라 버림이다

    실수를 정수로 옮길 때가 가장 많이 틀리는 자리입니다.

    Console.WriteLine((int)9.78);    //  9
    Console.WriteLine((int)9.99);    //  9
    Console.WriteLine((int)-9.78);   // -9

    9.99 도 9 입니다. 반올림이 아니라 소수점 아래를 그냥 버립니다. 음수도 -10 이 아니라 -9 입니다. 0 쪽으로 자릅니다.

    반올림이 필요하면 Math.Round 를 씁니다. 그런데 여기에도 함정이 있습니다.

    Console.WriteLine(Math.Round(0.5));   // 0  ← 1이 아닙니다
    Console.WriteLine(Math.Round(1.5));   // 2
    Console.WriteLine(Math.Round(2.5));   // 2  ← 3이 아닙니다

    Math.Round 의 기본은 정확히 0.5일 때 짝수 쪽으로 보냅니다. 통계에서 반올림이 한쪽으로만 쏠리는 걸 막는 방식이고, 흔히 은행가 반올림이라고 부릅니다.

    학교에서 배운 반올림을 원하면 방식을 지정하세요.

    (int)Math.Round(x) 처럼 반올림한 다음에 캐스팅하는 게 보통의 답입니다.

    실수를 정수로 옮길 때 값이 int 범위 밖이면 결과가 정해져 있지 않습니다. 0이 나올 수도, 엉뚱한 값이 나올 수도 있습니다. checked 를 씌우면 예외가 납니다.

    10 / 3 은 나눗셈 시점에 이미 3이다

    가장 자주 나오는 질문입니다.

    double result = 10 / 3;
    Console.WriteLine(result);   // 3

    double 에 담았는데 왜 3.333 이 아닐까요. 변환이 나눗셈보다 늦게 일어나기 때문입니다.

    사탕 10개를 3명에게 나누면 3개씩입니다. 남은 1개는 쪼개지 않습니다. 정수 나눗셈은 처음부터 그런 계산입니다.

    그래서 소수가 필요하면 나누기 전에 한쪽을 실수로 만들어야 합니다.

    마지막 것에서 괄호 위치를 조심하세요.

    double r1 = (double)a / b;     // 3.3333...  (a 를 먼저 실수로)
    double r2 = (double)(a / b);   // 3          (정수로 나눈 뒤에 변환)

    r2 는 처음 코드와 똑같습니다. 캐스팅이 나눗셈보다 늦으면 아무 의미가 없습니다.

    문자열과 숫자 사이는 캐스트로 못 옮긴다

    입력창에서 받은 "123" 을 숫자로 쓰고 싶을 때 (int)"123" 은 컴파일 에러입니다. 캐스트는 숫자 칸 사이에서 크기를 바꾸는 일인데, 문자열은 글자들의 묶음이라 칸의 모양부터 다릅니다. 글자 '1' '2' '3' 을 읽어 123 이라는 수를 새로 계산해야 합니다. 그 일을 하는 메서드가 따로 있습니다.

    int n = int.Parse("123");             // 123
    string s = n.ToString();              // "123"
    
    int.Parse("abc");                     // FormatException: 숫자가 아닙니다
    bool ok = int.TryParse("abc", out int m);   // ok = false, m = 0. 예외 없이 실패를 알려 줍니다

    out int m 은 “변환에 성공하면 결과를 m 에 넣어 달라” 는 뜻이고, 돌려받는 ok 가 성공했는지를 알려 줍니다. 사용자가 입력한 값처럼 숫자가 아닐 수 있는 것은 TryParse 로 받습니다.

    정리

    • 넓은 칸으로 갈 때는 앞을 채운다. 잃는 게 없어서 자동이다.
    • 좁은 칸으로 갈 때는 잘라낸다. 잃는 게 있어서 직접 써야 한다.
    • 암시적 변환이 보장하는 것은 범위지 정밀도가 아니다. int 를 float 에 넣으면 자릿수가 깎일 수 있다.
    • (byte) 는 부탁이 아니라 “잘려도 좋다” 는 지시다. 범위 보장은 그때부터 개발자 몫이다.
    • (int) 는 반올림이 아니라 버림이다. 음수도 0 쪽으로 자른다.
    • Math.Round 의 기본은 0.5를 짝수 쪽으로 보낸다.
    • 10 / 3 은 나눗셈 시점에 이미 3이다. 소수가 필요하면 나누기 전에 한쪽을 실수로 만든다.
    • 문자열과 숫자 사이는 캐스트가 아니라 int.Parse · TryParse · ToString 으로 옮긴다.