공공부하자개발 · 영어 학습 노트
자바
초급자바의 뼈대0/8 완료
  • 00개발 환경과 첫 프로그램
  • 01변수, 데이터 타입, 형변환
  • 02연산자와 제어문
  • 031차원 · 2차원 배열
  • 04클래스, 객체, 생성자, 오버로딩
  • 05접근 제어자와 캡슐화
  • 06메서드 활용 패턴 (초급)
  • 07enum, 패키지와 import, static 과 final
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 초급 › 01 / 8

변수, 데이터 타입, 형변환

섹션 7진행 0 / 8
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 변수 = 이름이 붙은 메모리 공간

java
int quantity = 3;

이 한 줄은 세 가지 일을 합니다.

  1. 선언(declaration) — int quantity: "정수 4바이트짜리 공간을 확보하고 quantity라는 이름을 붙여라"
  2. 초기화(initialization) — = 3: 그 공간에 3을 넣어라
  3. 이후 quantity라는 이름으로 그 공간을 읽고 쓸 수 있다

int가 타입(type, 자료형)입니다. 타입은 두 가지를 결정합니다. 크기(몇 바이트를 쓰는가)와 해석 방법(그 바이트들을 정수로 볼 것인가, 실수로 볼 것인가, 문자로 볼 것인가). 같은 4바이트 01000001 00000000 00000000 00000000이라도 int로 해석하면 65, float로 해석하면 전혀 다른 실수가 됩니다. 타입은 곧 "해석 규칙"입니다.

변수 이름 규칙:

규칙 가능 불가능
문자, _, $로 시작 count, _temp, $price 1st, -x
숫자는 두 번째 글자부터 item2 2item
예약어 사용 불가 className class, int, for
대소문자 구분 total과 Total은 다른 변수
관례: camelCase orderCount, userName order_count(자바에선 비관례)

2.2 원시 타입 8종 — 크기, 범위, 기본값

자바의 타입은 크게 원시 타입(primitive type, 값 자체를 저장) 8개와 참조 타입(reference type, 값이 있는 주소를 저장) 무한개로 나뉩니다. 원시 타입은 딱 8개이고 모두 소문자로 시작합니다.

분류 타입 크기 범위 기본값 리터럴 예시
정수 byte 1바이트 -128 ~ 127 0 (byte) 10
정수 short 2바이트 -32,768 ~ 32,767 0 (short) 100
정수 int 4바이트 약 -21억 ~ 21억 (-2³¹ ~ 2³¹-1) 0 42, 1_000_000, 0xFF
정수 long 8바이트 약 -922경 ~ 922경 (-2⁶³ ~ 2⁶³-1) 0L 42L, 10_000_000_000L
실수 float 4바이트 약 ±3.4×10³⁸, 유효숫자 7자리 0.0f 3.14f
실수 double 8바이트 약 ±1.8×10³⁰⁸, 유효숫자 15자리 0.0 3.14, 1e-9
문자 char 2바이트 0 ~ 65,535 (유니코드) '' 'A', '가', '\n'
논리 boolean 1비트(JVM 구현마다 다름) true, false false true

왜 int의 최대가 21억인가? 4바이트 = 32비트. 첫 비트는 부호(0이면 양수, 1이면 음수)로 쓰고 나머지 31비트로 크기를 표현합니다. 2³¹ = 2,147,483,648이므로 최대는 2,147,483,647입니다. 음수는 2의 보수(two's complement) 방식으로 표현해서 -2,147,483,648까지 갑니다(0을 양수 쪽에서 세므로 음수가 하나 더 많습니다).

어떤 타입을 쓸 것인가:

상황 권장 타입 이유
정수 일반 (개수, 인덱스, 나이) int JVM이 가장 빠르게 처리. 기본값
21억 넘을 수 있는 정수 (ID, 타임스탬프, 총 매출) long 오버플로우 방지
실수 일반 (좌표, 비율) double float보다 정밀. 자바 실수 기본형
돈 BigDecimal (참조 타입) 부동소수점 오차 없음. double 절대 금지
파일/네트워크 바이트 byte 1바이트 단위 데이터
참/거짓 boolean
한 글자 char 문자열은 String

byte, short, float는 실무에서 거의 안 씁니다. 메모리 몇 바이트 아끼는 것보다 int, double로 통일하는 편이 안전하고 빠릅니다.

2.3 스택과 힙 — 원시 타입과 참조 타입이 다른 이유

JVM은 메모리를 여러 영역으로 나누는데, 변수 관점에서 중요한 것은 두 곳입니다.

영역 무엇이 저장되는가 특징
스택(stack) 지역 변수(메서드 안에서 선언한 변수), 원시 타입의 값, 참조 타입의 주소 메서드가 호출되면 쌓이고 끝나면 통째로 사라짐. 매우 빠름. 스레드마다 하나
힙(heap) new로 만든 모든 객체(배열, String, 사용자 정의 클래스 인스턴스) 크기가 크고 오래 살 수 있음. 가비지 컬렉터(GC, 안 쓰는 객체를 자동 청소하는 장치)가 관리
java
int age = 30;
String name = "Kim";
int[] scores = {90, 85};

메모리 그림:

text
스택                          힙
┌───────────────┐            ┌─────────────────┐
│ age    │ 30   │            │ "Kim"           │ ← 0x1A2B
│ name   │ 0x1A2B ──────────►│                 │
│ scores │ 0x3C4D ──────────►│ [90, 85]        │ ← 0x3C4D
└───────────────┘            └─────────────────┘
  • age는 스택에 값 30 그 자체가 들어 있습니다.
  • name은 스택에 주소(0x1A2B)만 들어 있고, 실제 문자열은 힙에 있습니다.
  • scores도 마찬가지로 주소만 스택에 있습니다.

이 차이가 만드는 결과:

java
int a = 10;
int b = a;      // 값 10이 복사됨
b = 20;         // a는 여전히 10

int[] x = {1, 2, 3};
int[] y = x;    // 주소가 복사됨 → 둘이 같은 배열을 가리킴
y[0] = 99;      // x[0]도 99가 됨!

원시 타입은 값을 복사하고 참조 타입은 주소를 복사합니다. 이걸 모르면 "분명히 복사했는데 원본이 바뀌었다"는 버그를 겪게 됩니다. 04 레슨의 call by value에서 다시 깊게 다룹니다.

왜 이렇게 나누었을까? 원시 타입은 크기가 작고 고정되어 있어 스택에 직접 두는 것이 빠릅니다. 객체는 크기가 가변적이고(문자열 길이가 얼마일지 모름) 여러 곳에서 공유해야 하므로 힙에 하나만 두고 주소로 가리키는 편이 효율적입니다.

2.4 참조 타입과 null

참조 타입 변수는 "아직 아무것도 가리키지 않음"을 나타내는 특별한 값 null을 가질 수 있습니다.

java
String title = null;      // 주소 없음
title.length();           // NullPointerException! 주소가 없는데 따라가려 함

NullPointerException(NPE)은 자바 런타임 오류 1위입니다. 원리는 단순합니다. 주소가 null인데 그 주소를 따라가서 뭔가를 하려 하면 JVM이 멈춥니다. 원시 타입은 항상 값이 있으므로 null이 될 수 없습니다. int x = null;은 컴파일 오류입니다.

2.5 String은 특별한 참조 타입

String은 클래스(참조 타입)이지만 자바에서 너무 자주 쓰여서 원시 타입처럼 "..." 리터럴로 만들 수 있습니다. 두 가지를 기억하세요.

1) 불변(immutable)이다. 한번 만든 문자열은 절대 바뀌지 않습니다.

java
String s = "hello";
s.toUpperCase();          // 새 문자열 "HELLO"를 만들어 반환하지만 버려짐
System.out.println(s);    // 출력: hello  (s는 그대로)
s = s.toUpperCase();      // 반환값을 다시 담아야 바뀜

2) ==가 아니라 equals로 비교한다. ==는 주소 비교이고 equals는 내용 비교입니다.

java
String a = "java";
String b = new String("java");
a == b          // false (다른 객체)
a.equals(b)     // true  (내용 같음)

리터럴 "java"끼리는 JVM이 문자열 풀(string pool)에서 같은 객체를 재사용하므로 ==도 true가 나오지만, 이것에 의존하면 안 됩니다. 문자열 비교는 무조건 equals입니다.

2.6 리터럴과 상수 final

리터럴(literal)은 코드에 직접 쓴 값입니다. 42, 3.14, 'A', "text", true. 타입별 표기 규칙:

리터럴 타입 비고
42 int 정수 리터럴의 기본은 int
42L long 접미사 L (소문자 l은 1과 헷갈리니 대문자)
3.14 double 실수 리터럴의 기본은 double
3.14f float 접미사 f 필수. float x = 3.14;는 오류
0b1010 int 2진수
0777 int 8진수 (앞에 0). 실수로 쓰기 쉬움
0xFF int 16진수
1_000_000 int 밑줄로 자릿수 구분 (JDK 7+)
'A' char 작은따옴표, 한 글자
"A" String 큰따옴표

final을 붙이면 한 번 초기화한 뒤 값을 바꿀 수 없는 상수가 됩니다.

java
final int MAX_RETRY = 3;
MAX_RETRY = 5;     // 컴파일 오류: cannot assign a value to final variable

상수 이름은 관례상 대문자_밑줄입니다. 실무에서는 "매직 넘버(magic number, 의미를 알 수 없는 숫자)"를 없애는 데 씁니다. 코드 곳곳에 3이 흩어져 있으면 그게 재시도 횟수인지 배송비 할인율인지 알 수 없습니다. MAX_RETRY로 이름을 붙이면 의미가 드러나고, 바꿀 때도 한 곳만 고치면 됩니다.

2.7 형변환 — 자동과 강제

타입이 다른 값을 대입하려면 형변환(type casting)이 필요합니다. 두 종류가 있습니다.

자동 형변환(묵시적, widening) — 작은 타입 → 큰 타입. 값이 손실될 위험이 없으므로 컴파일러가 알아서 해 줍니다.

text
byte → short → int → long → float → double
              char ↗
java
int i = 100;
long l = i;        // OK, 자동
double d = l;      // OK, 자동

강제 형변환(명시적, narrowing) — 큰 타입 → 작은 타입. 값이 손실될 수 있으므로 개발자가 (타입)을 붙여 "손실을 감수하겠다"고 명시해야 합니다.

java
double price = 9.99;
int won = price;          // 컴파일 오류: possible lossy conversion
int won = (int) price;    // OK, 결과 9 (소수점 버림, 반올림 아님)

주의: long → float은 자동 변환이지만 정밀도가 손실됩니다. float는 유효숫자 7자리라 long 값 123456789L을 넣으면 123456792.0이 됩니다. 크기(바이트)는 float가 작지만 표현 범위가 넓어서 자바는 이를 widening으로 취급합니다. 실무에서 마주칠 일은 드물지만 "자동 = 무손실"이 항상 참은 아니라는 점은 기억하세요.

2.8 오버플로우 — 범위를 넘으면 반대편에서 나온다

java
int max = Integer.MAX_VALUE;   // 2,147,483,647
max = max + 1;                 // -2,147,483,648

왜 음수가 될까요? 32비트로 표현 가능한 가장 큰 양수는 0111...1111입니다. 여기에 1을 더하면 1000...0000이 되는데, 첫 비트가 1이면 음수로 해석되므로 가장 작은 음수가 됩니다. 자동차 주행거리계가 999,999에서 000,000으로 넘어가는 것과 같은 원리입니다.

자바는 오버플로우를 알려 주지 않습니다. 예외도 경고도 없이 조용히 틀린 값이 됩니다. 이것이 총 매출, 누적 조회수, 밀리초 타임스탬프 등에 long을 써야 하는 이유입니다. 정말 확실히 잡고 싶으면 Math.addExact(a, b)를 쓰면 오버플로우 시 예외를 던집니다.

정수 연산 중간에도 오버플로우가 납니다:

java
long total = 1_000_000 * 3_000;   // 결과 -1294967296 !

1_000_000과 3_000 모두 int 리터럴이므로 곱셈이 int로 수행되고, 그 결과(30억)가 int 범위를 넘어 이미 망가진 뒤에 long에 담깁니다. 해결은 피연산자 중 하나를 long으로 만드는 것입니다: 1_000_000L * 3_000.

2.9 실수 → 정수 손실과 부동소수점 오차

실수 → 정수 캐스팅은 소수점을 무조건 버립니다(truncation).

java
(int) 3.99    // 3
(int) -3.99   // -3  (0 방향으로 잘림, 내림이 아님)

반올림이 필요하면 Math.round(3.99) → 4, 올림은 Math.ceil, 내림은 Math.floor.

부동소수점(floating point)은 근사값입니다. double은 값을 2진수 소수로 저장하는데, 0.1은 2진수로 무한소수(0.0001100110011...)라서 정확히 표현할 수 없습니다. 10진수에서 1/3을 0.333...으로밖에 못 쓰는 것과 같습니다.

java
System.out.println(0.1 + 0.2);         // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);  // false

따라서 돈 계산에 double을 쓰면 안 됩니다. 1원 단위 오차가 수백만 건 쌓이면 회계가 맞지 않습니다. 대안은 두 가지입니다.

  1. BigDecimal — 10진수를 정확히 저장하는 클래스. 느리지만 정확. new BigDecimal("0.1") 처럼 문자열로 생성해야 합니다(new BigDecimal(0.1)은 이미 오차 있는 double을 받음).
  2. 정수로 계산 — 원 단위나 센트 단위의 long으로 저장하고 표시할 때만 나눔.

2.10 정수 나눗셈과 승격

java
int a = 7, b = 2;
a / b          // 3   (정수 / 정수 = 정수, 나머지 버림)
a % b          // 1   (나머지)
a / 2.0        // 3.5 (하나라도 double이면 double 연산)
(double) a / b // 3.5 (a를 먼저 double로 캐스팅)
(double) (a / b) // 3.0 (정수 나눗셈이 먼저 끝난 뒤 캐스팅 → 이미 늦음)

두 피연산자의 타입이 다르면 자바는 큰 쪽으로 자동 승격(promotion)해서 계산합니다. 또한 byte, short, char 끼리의 산술 연산은 무조건 int로 승격됩니다.

java
byte x = 10, y = 20;
byte z = x + y;        // 컴파일 오류! x + y는 int
byte z = (byte) (x + y);

이것이 실무에서 byte, short를 잘 안 쓰는 이유 중 하나입니다. 계산할 때마다 캐스팅해야 합니다.

2.11 var — 타입 추론 (JDK 10+)

java
var count = 10;              // int로 추론
var name = "Kim";            // String으로 추론
var list = new ArrayList<String>();   // ArrayList<String>

var는 타입을 없애는 것이 아니라 컴파일러가 우변을 보고 타입을 대신 적어 주는 것입니다. var count = 10; 이후 count = "text";는 여전히 컴파일 오류입니다. 동적 타입 언어(파이썬, 자바스크립트)의 변수와는 다릅니다.

제약:

가능 불가능
지역 변수 필드(클래스 멤버 변수), 메서드 매개변수, 반환 타입
초기화와 동시에 var x; (추론할 값이 없음)
var x = null; (타입을 알 수 없음)
for문 변수 for (var item : items) 람다 var f = () -> {};

언제 쓰는가: 우변만 봐도 타입이 명백할 때(var user = new User();, var map = new HashMap<String, List<Order>>();). 우변이 메서드 호출이라 타입이 불명확하면(var result = service.process();) 명시적 타입이 읽기 좋습니다. 팀 컨벤션을 따르세요.

핵심 원리
  • 2.1 변수 = 이름이 붙은 메모리 공간
  • 2.2 원시 타입 8종 — 크기, 범위, 기본값
  • 2.3 스택과 힙 — 원시 타입과 참조 타입이 다른 이유
  • 2.4 참조 타입과 null
  • 2.5 String은 특별한 참조 타입
  • 2.6 리터럴과 상수 final
  • 2.7 형변환 — 자동과 강제
  • 2.8 오버플로우 — 범위를 넘으면 반대편에서 나온다
  • 2.9 실수 → 정수 손실과 부동소수점 오차
  • 2.10 정수 나눗셈과 승격
  • 2.11 var — 타입 추론 (JDK 10+)
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제