데이터는 결국 비트열이다
컴퓨터 안에서 데이터는 모두 0과 1의 조합으로 저장된다. 이 한 칸을 비트(bit)라고 한다. 비트 하나는 두 가지 상태만 표현한다. 전기가 흐른다/흐르지 않는다, 자성이 있다/없다처럼 하드웨어가 안정적으로 구분할 수 있는 두 상태를 숫자 0과 1로 약속한 것이다.
하지만 비트 하나만으로는 표현할 수 있는 정보가 너무 적다. 그래서 보통 8비트를 묶어 바이트(byte)라고 부른다. 1바이트는 00000000부터 11111111까지, 총 256가지 값을 표현할 수 있다. 파일 크기, 메모리 사용량, 네트워크 전송량을 바이트 단위로 말하는 이유가 여기에 있다.
워드(word)는 CPU가 한 번에 자연스럽게 처리하기 좋은 데이터 크기를 뜻할 때가 많다. 그래서 32비트 환경에서는 보통 32비트, 64비트 환경에서는 보통 64비트를 기계어 수준의 처리 단위로 생각한다. 다만 아키텍처나 문서의 관례에 따라 word라는 말이 16비트처럼 더 좁은 의미로 쓰이는 경우도 있다. 여기서는 넓은 의미의 기계 워드(machine word), 즉 CPU가 다루기 좋은 기본 단위 정도로 이해하면 된다. 워드 크기는 레지스터 크기, 포인터 크기, 주소 공간, 한 번에 처리하기 좋은 연산 단위와 연결된다.
개발자가 기억해야 할 핵심은 이것이다. 컴퓨터는 실제로는 비트열만 저장한다. 그 비트열을 정수로 볼지, 문자로 볼지, 이미지 픽셀로 볼지, 명령어로 볼지는 해석 규칙이 결정한다.
예를 들어 다음 1바이트 비트열을 보자.
01000001이 값을 부호 없는 정수로 해석하면 65다. ASCII 문자로 해석하면 A다. 같은 비트열이지만 규칙이 달라지면 의미도 달라진다. 데이터 표현 방식을 알아야 하는 이유가 바로 여기에 있다.
AI 생성 이미지
정수 표현과 범위의 한계
정수는 가장 단순해 보이지만, 실제 시스템에서는 항상 범위를 가진다. 8비트로 표현할 수 있는 경우의 수는 256개뿐이다. 따라서 8비트 부호 없는 정수는 보통 0부터 255까지 표현한다.
00000000 = 0
00000001 = 1
11111111 = 255그렇다면 음수는 어떻게 표현할까? 가장 널리 쓰이는 방식은 2의 보수(two's complement)다. 2의 보수는 맨 앞 비트를 부호 비트처럼 해석하면서도, 덧셈 회로를 단순하게 유지할 수 있는 표현 방식이다.
8비트 부호 있는 정수는 보통 -128부터 127까지 표현한다.
01111111 = 127
10000000 = -128
11111111 = -1처음 보면 11111111이 왜 -1인지 낯설다. 2의 보수에서는 어떤 양수의 음수를 만들 때 비트를 모두 뒤집고 1을 더한다.
1 = 00000001
비트 반전 = 11111110
1 더하기 = 11111111 => -1이 방식의 장점은 뺄셈을 별도 회로 없이 덧셈처럼 처리할 수 있다는 점이다.
1 = 00000001
-1 = 11111111
합 = 00000000 (넘친 1비트는 버림)결과가 0이 된다. 하드웨어 입장에서는 단순하고 빠르다.
문제는 표현 범위가 고정되어 있다는 점이다. 8비트 부호 있는 정수에서 최댓값은 127이다. 여기에 1을 더하면 수학적으로는 128이 되어야 한다. 하지만 8비트에는 그 값을 담을 수 없다.
01111111 = 127
+00000001 = 1
----------
10000000 = -128값이 최댓값을 넘어 다시 최솟값 쪽으로 돌아간다. 이런 현상을 오버플로(overflow)라고 한다. 반대로 최솟값보다 더 작은 값을 만들려고 할 때도 표현 범위를 벗어난다. 정수에서는 이 경우도 넓게 오버플로 또는 래핑(wrapping)으로 설명하는 일이 많고, 문맥에 따라 언더플로(underflow)라고 부르기도 한다. 다만 부동소수점에서 말하는 언더플로는 "0에 너무 가까운 작은 값이 표현되지 않는 현상"을 가리키는 경우가 많으므로, 어떤 타입의 어떤 상황을 말하는지 구분하는 것이 좋다.
실무에서는 이 문제가 생각보다 자주 나타난다. 조회 수, 포인트, 결제 금액, 파일 크기, 타임스탬프, 배열 인덱스처럼 계속 증가하는 값은 언젠가 범위를 넘을 수 있다. 예전 시스템에서 32비트 정수로 시간을 저장하다가 특정 시점 이후 문제가 생기는 사례도 같은 범주의 문제다.
언어마다 동작도 다르다. Java의 int는 32비트이며 범위를 넘으면 값이 순환한다. C/C++에서는 부호 있는 정수 오버플로가 더 위험할 수 있다. 표준 관점에서 정의되지 않은 동작이 될 수 있고, 컴파일러 최적화와 얽히면 개발자가 예상하지 못한 결과가 나올 수 있다. JavaScript의 일반 숫자는 정수 전용 타입이 아니라 부동소수점 기반의 Number라서, 큰 정수에서는 또 다른 정밀도 문제가 생긴다.
따라서 개발자는 숫자를 저장할 때 먼저 질문해야 한다.
이 값의 가능한 최댓값은 어디까지인가?
음수가 필요한가?
합계나 곱셈 과정에서 중간값이 범위를 넘을 수 있는가?
언어와 데이터베이스의 숫자 타입 범위가 서로 맞는가?
정수 타입은 단순한 문법 선택이 아니라 데이터의 수명과 시스템의 안정성을 결정하는 설계 선택이다.
실수는 근삿값으로 저장된다
정수보다 더 자주 개발자를 당황하게 만드는 것이 실수다. 대표적인 예가 다음 코드다.
console.log(0.1 + 0.2); // 0.30000000000000004이 결과는 JavaScript만의 버그가 아니다. 많은 언어가 실수를 부동소수점(floating point) 방식으로 표현하기 때문에 나타나는 현상이다.
부동소수점은 숫자를 대략 다음과 같은 형태로 저장한다.
부호 × 유효숫자 × 2의 지수큰 수와 작은 수를 넓은 범위에서 표현하기 위해 소수점 위치를 움직일 수 있게 만든 방식이다. 그래서 "부동", 즉 떠다니는 소수점이라고 부른다.
문제는 컴퓨터가 내부적으로 2진수를 사용한다는 점이다. 10진수에서 1/3을 정확히 표현하려면 0.3333...처럼 끝없이 이어진다. 마찬가지로 2진수에서는 0.1 같은 10진 소수가 딱 떨어지지 않는 경우가 많다. 결국 컴퓨터는 제한된 비트 안에서 가장 가까운 값으로 잘라 저장한다.
즉 0.1은 정확한 0.1이 아니라 "0.1에 매우 가까운 값"이다. 0.2도 마찬가지다. 두 근삿값을 더하면 사람이 기대한 0.3과 아주 작은 차이가 생길 수 있다.
이 차이는 화면에 한두 번 출력할 때는 사소해 보인다. 하지만 다음 상황에서는 문제가 된다.
결제 금액, 세금, 수수료 계산
포인트나 마일리지 누적
정산 배치 작업
좌표, 물리 시뮬레이션, 그래픽 계산
실수 값을 직접 비교하는 조건문
특히 금액 계산에 float나 double을 그대로 사용하는 것은 위험하다. 1원, 1센트 단위의 오차가 누적되면 실제 돈이 맞지 않는다. 금액은 보통 다음 방식 중 하나로 처리한다.
첫째, 가장 작은 화폐 단위의 정수로 저장한다. 예를 들어 10,000원을 10000이라는 정수로 저장하고, 달러라면 센트 단위로 1234를 저장해 $12.34로 표시한다.
둘째, 언어와 데이터베이스가 제공하는 고정소수점 또는 decimal 타입을 사용한다. 예를 들어 Java의 BigDecimal, Python의 Decimal, 데이터베이스의 DECIMAL 타입은 금액처럼 정확한 10진 계산이 필요한 곳에 적합하다. 다만 이런 타입을 사용할 때도 이진 부동소수점 값을 먼저 만든 뒤 변환하면 이미 오차가 섞일 수 있으므로, 문자열이나 정수 단위에서 출발하는 습관이 안전하다.
또 하나 중요한 습관은 실수끼리 직접 같은지 비교하지 않는 것이다.
// 위험한 비교
if (a + b === 0.3) {
// ...
}대신 허용 가능한 오차 범위를 정해 비교한다.
const EPSILON = 1e-10;
if (Math.abs((a + b) - 0.3) < EPSILON) {
// 충분히 같다고 판단
}여기서 EPSILON은 항상 같은 값으로 정해지는 마법의 상수가 아니다. 다루는 값의 크기, 허용 가능한 오차, 도메인 요구사항에 따라 달라진다.
부동소수점은 틀린 방식이 아니다. 빠르고 넓은 범위의 수를 다루기에 매우 효율적이다. 다만 "정확한 10진 소수 계산기"가 아니라 "제한된 비트로 근삿값을 표현하는 방식"임을 알고 써야 한다.
문자는 숫자와 약속의 조합이다
컴퓨터는 문자도 직접 이해하지 못한다. 문자를 저장하려면 먼저 각 문자에 숫자를 배정해야 한다. 이 약속을 넓게 문자 집합(character set) 또는 문자 코드라고 부른다.
초기의 대표적인 문자 코드는 ASCII다. ASCII는 영어 알파벳, 숫자, 기호, 제어 문자 등을 7비트 범위 안에 배정했다.
A = 65
B = 66
a = 97
0 = 48영어 중심 환경에서는 ASCII만으로 충분했다. 하지만 한글, 일본어, 중국어, 아랍어, 이모지까지 표현하려면 훨씬 더 큰 체계가 필요하다. 이 문제를 해결하기 위해 등장한 표준이 유니코드(Unicode)다.
유니코드는 전 세계 문자를 하나의 거대한 번호 체계로 정리한다. 여기서 각 문자에 배정된 번호를 코드 포인트(code point)라고 한다. 예를 들어 한글 가는 유니코드 코드 포인트 U+AC00이다.
그런데 코드 포인트는 추상적인 번호다. 이 번호를 메모리나 파일에 실제 바이트로 저장하는 방식은 또 별도다. 이 방식을 인코딩(encoding)이라고 한다. 대표적인 인코딩이 UTF-8이다.
정리하면 다음과 같다.
| 개념 | 의미 |
|---|---|
| 유니코드 | 문자에 번호를 배정한 표준 |
| 코드 포인트 | 유니코드에서 특정 문자가 가진 번호 |
| UTF-8 | 코드 포인트를 바이트열로 저장하는 인코딩 방식 |
| ASCII | 영어권 문자 중심의 오래된 문자 코드 체계 |
UTF-8은 ASCII와 호환된다. 영어 알파벳과 숫자, 기본 기호는 ASCII와 같은 값으로 1바이트에 저장된다. 반면 한글은 보통 3바이트, 많은 이모지는 4바이트로 저장된다.
A -> 1바이트
가 -> 3바이트
😀 -> 4바이트이 차이를 모르면 실무에서 여러 문제가 생긴다.
예를 들어 데이터베이스 컬럼을 VARCHAR(10)으로 만들었을 때, 이것이 문자 10개를 뜻하는지 바이트 10개를 뜻하는지는 DB와 설정에 따라 주의해서 봐야 한다. 한글 10자는 UTF-8에서 30바이트가 될 수 있다. 이모지가 섞이면 더 커질 수 있다.
또 문자열 길이를 셀 때도 주의가 필요하다. 개발자가 원하는 길이는 보통 "사용자가 눈으로 보는 글자 수"다. 하지만 프로그램의 length는 언어에 따라 바이트 수, 코드 유닛 수, 코드 포인트 수 중 하나일 수 있다.
특히 이모지는 단순하지 않다. 어떤 이모지는 하나의 코드 포인트로 보이지만, 어떤 이모지는 여러 코드 포인트가 조합되어 하나의 그림처럼 표시된다. 피부색이 붙은 이모지나 가족 이모지처럼 여러 문자가 결합된 사례가 있다. 사용자는 한 글자로 보지만, 내부적으로는 여러 조각일 수 있다.
그래서 "닉네임은 10자까지" 같은 요구사항은 생각보다 어렵다. 바이트 기준인지, 유니코드 코드 포인트 기준인지, 화면에 보이는 글자 기준인지 명확히 정해야 한다. 그렇지 않으면 프런트엔드에서는 통과했는데 서버에서 저장 실패가 나거나, 데이터베이스에는 저장됐지만 화면에서 깨지는 일이 생긴다.
인코딩 깨짐은 해석 규칙이 어긋난 결과다
문자가 깨지는 현상은 대부분 저장할 때의 인코딩과 읽을 때의 인코딩이 다를 때 발생한다.
예를 들어 어떤 파일을 UTF-8로 저장했는데, 프로그램이 이를 다른 문자 인코딩으로 해석하면 원래 문자가 아닌 엉뚱한 문자들이 보인다. 흔히 말하는 "한글 깨짐"은 컴퓨터가 한글을 모르는 것이 아니라, 같은 바이트열을 다른 규칙으로 해석했기 때문에 생긴다.
웹에서도 비슷하다. 서버가 UTF-8로 응답을 보냈는데 HTTP 헤더나 HTML 메타 태그에서 인코딩 정보가 잘못 전달되면 브라우저가 잘못 해석할 수 있다. API 통신에서도 요청 본문, 응답 본문, 파일 업로드, CSV 다운로드에서 인코딩이 맞지 않으면 운영 환경에서만 데이터가 깨지는 문제가 생긴다.
실무에서 기본 원칙은 단순하다.
새 시스템은 가능하면 UTF-8을 기본으로 사용한다.
파일을 읽고 쓸 때 인코딩을 명시한다.
HTTP 응답의
Content-Type과 문자 인코딩을 확인한다.데이터베이스와 테이블, 컬럼의 문자셋 설정을 확인한다.
이모지를 저장해야 한다면 DB가 4바이트 UTF-8 문자를 지원하는지 확인한다.
문자열은 화면에 보이는 것보다 훨씬 복잡하다. 하지만 핵심은 하나다. 문자는 결국 숫자이고, 숫자는 바이트열이며, 바이트열은 정해진 인코딩 규칙에 따라 해석된다.
데이터 표현을 알면 버그의 원인이 보인다
비트와 바이트는 너무 기초적인 주제처럼 보인다. 하지만 많은 실무 버그는 이 기초에서 시작된다.
정수는 범위가 있어서 오버플로가 생긴다. 부동소수점은 근삿값이라서 소수 계산에 오차가 생긴다. 문자는 인코딩 규칙이 맞지 않으면 깨진다. 한글과 이모지는 영어보다 더 많은 바이트를 사용하고, 사용자가 보는 글자 수와 프로그램이 세는 길이가 다를 수 있다.
이 사실을 알면 문제를 만났을 때 질문이 달라진다.
이 숫자가 타입의 범위를 넘었는가?
이 계산이 정확한 10진 계산을 요구하는가?
이 문자열의 길이는 문자 기준인가, 바이트 기준인가?
저장할 때와 읽을 때 같은 인코딩을 사용하는가?
언어, 데이터베이스, 네트워크 경계에서 표현 방식이 바뀌는가?
컴퓨터가 데이터를 표현하는 방식은 낮은 수준의 지식이지만, 그 영향은 API 응답, 데이터베이스 설계, 결제 시스템, 로그 분석, 사용자 입력 처리까지 이어진다. 개발자는 값을 다룰 때 그 값이 어떤 비트열로 저장되고 어떤 규칙으로 해석되는지 한 번 더 생각해야 한다. 그 습관이 많은 장애를 미리 막는다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.