변수와 타입 — 자바는 왜 이렇게 타입에 엄격한가요
Python이나 JavaScript에서 Java로 넘어오면 처음 느끼는 게 "왜 이렇게 타입을 일일이 써야 하지?"다. 변수 하나 만들 때마다
int,String,double을 붙여야 하고, 타입이 안 맞으면 컴파일부터 막힌다. 이게 불편하기만 한 건지, 아니면 이유가 있는 건지 — 자바의 타입 시스템을 처음부터 정리해보자.
변수 선언
Java에서 변수를 만들려면 타입을 먼저 써야 한다.
int age = 25; // 정수
double height = 175.5; // 실수
String name = "Java"; // 문자열
boolean active = true; // 참/거짓
Python이라면 age = 25로 끝이지만, Java는 int age = 25로 타입을 명시한다. 이걸 **정적 타이핑(Static Typing)**이라고 한다.
왜 이렇게 엄격한가요?
정적 타이핑의 장점은 명확하다.
- 컴파일 시점에 오류를 잡는다: 런타임에 터지는 것보다 훨씬 안전하다
- IDE 자동 완성이 강력해진다: 타입을 알면 어떤 메서드를 쓸 수 있는지 IDE가 바로 알려줌
- 대규모 코드베이스에서 유리하다: 팀 프로젝트에서 타입이 곧 문서 역할
단점은 코드가 길어진다는 것이지만, Java 10부터 var 키워드로 다소 완화됐다.
var age = 25; // 컴파일러가 int로 추론
var name = "Java"; // String으로 추론
var list = new ArrayList<String>(); // ArrayList<String>으로 추론
var를 써도 내부적으로는 타입이 확정된다. 동적 타이핑이 되는 게 아니다.
기본 타입 8가지 (Primitive Types)
Java에는 8개의 기본 타입이 있다. 이 8개가 전부다.
| 분류 | 타입 | 크기 | 범위 / 설명 |
|---|---|---|---|
| 정수 | byte | 1바이트 | -128 ~ 127 |
short | 2바이트 | -32,768 ~ 32,767 | |
int | 4바이트 | 약 ±21억 | |
long | 8바이트 | 매우 큰 정수 | |
| 실수 | float | 4바이트 | 소수점 약 7자리 |
double | 8바이트 | 소수점 약 15자리 | |
| 문자 | char | 2바이트 | 유니코드 한 글자 |
| 논리 | boolean | - | true / false |
실무에서 자주 쓰는 건?
솔직히 대부분 int, long, double, boolean 이 네 개가 주력이다.
int count = 100; // 일반적인 정수
long id = 123456789L; // 큰 숫자 (L 접미사 필수)
double rate = 3.14; // 소수점
boolean done = false; // 참/거짓
byte, short는 메모리를 아껴야 하는 특수한 상황에서만 쓰고, float는 double이 더 정확해서 잘 안 쓴다. char도 문자열(String)로 대체하는 경우가 많다.
리터럴(Literal) 표기
int decimal = 100; // 10진수
int hex = 0xFF; // 16진수
int binary = 0b1010; // 2진수
int million = 1_000_000; // 언더스코어로 가독성 향상
long big = 100L; // long은 L 접미사
float f = 3.14f; // float은 f 접미사
double d = 3.14; // double은 기본
char c = 'A'; // 작은따옴표
char unicode = '\uAC00'; // 유니코드 (가)
String s = "Hello"; // 큰따옴표 — 기본 타입이 아님!
참조 타입 (Reference Types)
기본 8타입을 제외한 나머지는 전부 참조 타입이다.
String name = "Java"; // String은 참조 타입
int[] numbers = {1, 2, 3}; // 배열도 참조 타입
List<String> list = new ArrayList<>(); // 컬렉션도 참조 타입
기본 타입 vs 참조 타입의 차이
// 기본 타입: 값 자체를 저장
int a = 10;
int b = a; // 값이 복사됨
b = 20;
System.out.println(a); // 10 — a는 변하지 않음
// 참조 타입: 주소(레퍼런스)를 저장
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1; // 주소가 복사됨
arr2[0] = 99;
System.out.println(arr1[0]); // 99 — arr1도 바뀜!
이걸 그림으로 보면 이해가 쉽다.
기본 타입:
a → [10]
b → [20] ← 각각 독립된 값
참조 타입:
arr1 → ┐
├→ [99, 2, 3] ← 같은 배열을 가리킴
arr2 → ┘
null
참조 타입 변수는 아무 객체도 가리키지 않을 수 있다. 그 상태가 null이다.
String name = null;
System.out.println(name.length()); // NullPointerException!
null인 변수에 메서드를 호출하면 **NullPointerException(NPE)**이 터진다. Java에서 가장 흔한 런타임 에러다.
형변환 (Type Casting)
자동 형변환 (묵시적 / Widening)
작은 타입에서 큰 타입으로는 자동 변환된다.
int i = 100;
long l = i; // int → long 자동 변환
double d = l; // long → double 자동 변환
변환 방향:
byte → short → int → long → float → double
char ↗
강제 형변환 (명시적 / Narrowing)
큰 타입에서 작은 타입으로는 명시적으로 캐스팅해야 한다.
double d = 3.99;
int i = (int) d; // 3 — 소수점 버림 (반올림 아님!)
long big = 300;
byte small = (byte) big; // 44 — 오버플로우 발생!
강제 형변환은 데이터 손실이 생길 수 있다. 소수점이 잘리거나, 범위를 넘어서 엉뚱한 값이 나올 수 있다.
문자열 ↔ 숫자 변환
// 문자열 → 숫자
int num = Integer.parseInt("123");
double d = Double.parseDouble("3.14");
// 숫자 → 문자열
String s1 = String.valueOf(123);
String s2 = 123 + ""; // 간편하지만 가독성 논란
String s3 = Integer.toString(123);
래퍼 클래스 (Wrapper Class)
기본 타입은 객체가 아니다. 그래서 컬렉션에 넣거나 null을 표현할 수 없다.
// List<int> list = new ArrayList<>(); // 컴파일 에러!
List<Integer> list = new ArrayList<>(); // Integer(래퍼)로 감싸야 함
| 기본 타입 | 래퍼 클래스 |
|---|---|
int | Integer |
long | Long |
double | Double |
boolean | Boolean |
char | Character |
byte | Byte |
short | Short |
float | Float |
오토박싱 / 언박싱
Java 5부터 기본 타입 ↔ 래퍼 클래스 변환이 자동으로 된다.
Integer a = 10; // 오토박싱: int → Integer
int b = a; // 언박싱: Integer → int
List<Integer> list = new ArrayList<>();
list.add(42); // 오토박싱
int value = list.get(0); // 언박싱
편리하지만 주의할 점이 있다.
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true — 캐시 범위 (-128~127)
Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false — 캐시 밖이라 다른 객체!
System.out.println(a.equals(b)); // true — 값 비교는 equals()
==는 참조(주소) 비교이고, equals()가 값 비교다. 래퍼 클래스 비교는 항상 equals()를 쓰는 게 안전하다.
연산자
산술 연산자
int a = 10, b = 3;
System.out.println(a + b); // 13
System.out.println(a - b); // 7
System.out.println(a * b); // 30
System.out.println(a / b); // 3 — 정수 나눗셈 (소수점 버림!)
System.out.println(a % b); // 1 — 나머지
정수끼리 나누면 소수점이 버려진다는 게 초보자가 가장 많이 실수하는 부분이다.
int a = 10, b = 3;
System.out.println(a / b); // 3
System.out.println((double) a / b); // 3.333... — 하나를 double로 바꿔야 함
비교 / 논리 연산자
// 비교 연산자
System.out.println(10 > 5); // true
System.out.println(10 == 10); // true
System.out.println(10 != 5); // true
// 논리 연산자
System.out.println(true && false); // false (AND)
System.out.println(true || false); // true (OR)
System.out.println(!true); // false (NOT)
단축 평가 (Short-circuit Evaluation)
&&와 ||는 왼쪽 결과만으로 확정되면 오른쪽을 실행하지 않는다.
String name = null;
// name이 null이면 &&의 오른쪽은 실행 안 됨 → NPE 방지
if (name != null && name.length() > 0) {
System.out.println(name);
}
이 패턴은 NPE를 방지할 때 자주 쓰인다.
증감 연산자
int i = 5;
System.out.println(i++); // 5 — 출력 후 증가
System.out.println(i); // 6
int j = 5;
System.out.println(++j); // 6 — 증가 후 출력
i++(후위)와 ++i(전위)의 차이는 알아두되, 복잡한 표현식에서 섞어 쓰는 건 가독성이 떨어지므로 피하는 게 좋다.
상수 (final)
값이 변하면 안 되는 변수는 final을 붙인다.
final int MAX_SIZE = 100;
// MAX_SIZE = 200; // 컴파일 에러!
final String GREETING = "Hello";
// GREETING = "Hi"; // 컴파일 에러!
관례적으로 상수는 대문자 + 언더스코어로 쓴다 (MAX_SIZE, DEFAULT_VALUE).
타입 추론 (var)
Java 10부터 지역 변수에 var를 쓸 수 있다.
var count = 10; // int
var name = "Java"; // String
var list = new ArrayList<String>(); // ArrayList<String>
var map = Map.of("a", 1, "b", 2); // Map<String, Integer>
쓸 수 있는 곳: 지역 변수, for-each 루프 변수
for (var entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
쓸 수 없는 곳: 필드, 메서드 매개변수, 반환 타입
// var field = 10; // 클래스 필드에서는 불가
// public var getCount() {} // 반환 타입에서도 불가
var를 쓸지 말지는 오른쪽에서 타입이 명확한지로 판단하면 된다.
var count = 10; // 명확 → var OK
var result = someService.process(); // 타입 불명확 → 타입 명시가 나음
▸ TIP 이 글의 코드 예제를 직접 실행해보고 싶다면 Java 기본기 핸드북을 확인해보세요.
정리
- Java는 정적 타이핑 — 변수 선언 시 타입을 명시하고, 컴파일 시점에 타입 체크
- 기본 타입 8개:
int,long,double,boolean+byte,short,float,char - 참조 타입: 기본 타입을 제외한 모든 것 (String, 배열, 컬렉션 등)
- 형변환: 작은 → 큰은 자동, 큰 → 작은은 명시적 캐스팅 필요
- 래퍼 클래스: 기본 타입을 객체로 감싸는 클래스 (
int→Integer) ==vsequals(): 참조 타입 비교는equals()를 쓰자
다음 글에서는 제어문과 반복문을 다룬다. if, switch, for, while — 코드의 흐름을 다루는 기본기를 정리할 예정이다.