자바 참조 타입 — 스택과 힙, 배열, 열거 타입 정리
자바의 데이터 타입은 크게 기본 타입(원시 타입, primitive type)과 참조 타입(reference type)으로 분류됩니다. 참조 타입이란 객체(Object)의 번지를 참조하는 타입으로, 배열·열거·클래스·인터페이스 타입이 여기에 속합니다.
이 글은 2021년 8월 기준 기록입니다.
두 타입의 차이는 변수 안에 무엇이 들어 있느냐다
기본 타입으로 선언된 변수와 참조 타입으로 선언된 변수의 차이는 하나뿐입니다. 저장되는 값이 무엇인가.
- 기본 타입 — 실제 값을 변수 안에 저장합니다.
- 참조 타입 — 메모리의 번지를 값으로 저장하고, 그 번지를 통해 객체를 참조합니다.
이 차이는 곧 메모리 위치의 차이로 이어집니다. 변수는 스택(stack) 영역에 생성되고, 객체는 힙(heap) 영역에 생성됩니다.
int age = 25;
double price = 100.5;
String name = "신용권";
String hobby = "독서";
위 선언에서 기본 타입인 age와 price는 스택 영역에 값 자체를 가집니다. 반면 참조 타입인 name과 hobby는 실제 값을 힙 영역에 저장한 후, 힙 영역의 번지를 스택 영역에 저장합니다.
JVM은 메모리를 세 영역으로 나눠 쓴다
JVM이 사용하는 메모리 영역은 메소드 영역(Method Area), 힙 영역(Heap Area), JVM 스택(Stack)입니다.
메소드 영역은 JVM이 시작할 때 생성되고 모든 스레드가 공유하는 영역입니다. 런타임 상수풀, 필드 데이터, 메소드 데이터, 메소드 코드, 생성자 코드 등을 분류해서 저장합니다.
힙 영역은 객체와 배열이 생성되는 영역입니다. 힙 영역에 생성된 객체와 배열은 JVM 스택 영역의 변수나 다른 객체의 필드에서 참조합니다. 참조하는 변수나 필드가 없다면 의미 없는 객체가 되므로, JVM은 Garbage Collector를 실행시켜 이를 제거합니다. 따라서 개발자가 객체를 직접 제거할 필요는 없습니다.
JVM 스택 영역은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 할당됩니다. 추가로 스레드를 생성하지 않았다면 main 스레드만 존재하므로 JVM 스택도 하나입니다.
JVM 스택은 메소드를 호출할 때마다 프레임(Frame)을 추가(push)하고, 메소드가 종료되면 해당 프레임을 제거(pop)합니다. 이 구조는 눈으로 확인할 수 있습니다. 예외 발생 시 printStackTrace() 메소드로 보여 주는 Stack Trace의 각 라인이 하나의 프레임을 표현합니다.
프레임 내부에는 로컬 변수 스택이 있어서 기본 타입 변수와 참조 타입 변수가 추가(push)되거나 제거(pop)됩니다. 변수는 최초로 값이 저장될 때 이 영역에 생성되고, 선언된 블록 안에서만 스택에 존재하다가 블록을 벗어나면 제거됩니다.
참조 타입의 ==는 값이 아니라 주소를 묻는다
기본 타입 변수의 ==, != 연산은 변수의 값이 같은지 아닌지를 조사합니다. 반면 참조 타입 변수들 간의 ==, != 연산은 동일한 객체를 참조하는지, 다른 객체를 참조하는지를 알아볼 때 사용됩니다.
참조 타입 변수의 값은 힙 영역의 객체 주소이므로 결국 주소 값을 비교하는 것이고, 동일한 주소 값은 동일한 객체를 참조한다는 뜻이므로 == 연산의 결과가 true가 됩니다.
null은 "참조할 객체가 없다"는 표시다
참조 타입 변수는 힙 영역의 객체를 참조하지 않는다는 뜻으로 null 값을 가질 수 있습니다. 변수가 null인지 확인하려면 ==, != 연산을 수행합니다.
refVar1 == null;
refVar1 != null;
참조 변수를 사용하다 가장 많이 만나게 되는 예외가 NullPointerException입니다. 참조 타입 변수가 null을 가지고 있을 경우, 참조할 객체 자체가 없으므로 그 변수를 사용할 수 없습니다.
int[] intArray = null;
intArray[0] = 10; // NullPointerException 발생(intArray는 null로 참조하는 객체 자체가 없기 때문)
String str = null;
System.out.println(str.length()); // NullPointerException 발생(str 변수가 참조하는 String 객체가 없기 때문)
String 변수는 문자열을 담지 않는다
흔히 "문자열을 String 변수에 저장한다"고 말하지만, 정확히 말하면 틀린 표현입니다. String 객체로 문자열이 생성되고, 변수는 그 String 객체를 참조합니다.
String 변수;
변수 = "문자열"; // 문자열은 쌍따옴표로 표시
String 변수 = "문자열"; // 선언과 초기화 동시
여기서 한 가지 규칙이 붙습니다. 문자열 리터럴이 동일하다면 String 객체를 공유하도록 되어 있습니다.
String name1 = "신용권";
String name2 = "신용권";
// name1과 name2는 동일한 객체를 참조
String name1 = new String("신용권");
String name2 = new String("신용권");
// name1과 name2는 다른 객체를 참조
그래서 같은 글자를 담고 있어도 비교 결과가 갈립니다.
String name1 = "신용권";
String name2 = "신용권";
String name3 = new String("신용권");
name1 == name2 // true
name1 == name3 // false
name1.equals(name3) // true
객체와 상관없이 문자열만을 비교할 때에는 String 객체의 equals() 메소드를 사용해야 한다는 결론이 여기서 나옵니다.
String 변수도 참조 타입이므로 초기값으로 null을 대입할 수 있습니다.
String hobby = "여행";
hobby = null; // String 객체를 참조하지 않게 되고, 그 객체는 메모리에서 자동 제거
배열은 길이를 바꿀 수 없는 객체다
변수는 한 개의 데이터만 저장할 수 있습니다. 같은 타입의 많은 양의 데이터를 다루기 위한 방법이 배열입니다. 배열은 같은 타입의 데이터를 연속된 공간에 나열시키고, 각 데이터에 인덱스(index)를 부여해 놓은 자료구조입니다.
제약이 두 가지 있습니다. 다른 타입의 값을 저장하려고 하면 타입 불일치(Type Mismatch) 컴파일 오류가 발생하고, 한 번 생성된 배열은 길이를 늘리거나 줄일 수 없습니다.
배열 선언
배열 변수는 두 가지 형태로 선언할 수 있습니다.
int[] intArray;
double[] doubleArray;
String[] strArray;
int intArray[];
double doubleArray[];
String strArray[];
배열 변수는 참조 변수에 속합니다. 자바는 배열을 객체로 다루기 때문에 배열은 힙 영역에 생성되고, 배열 변수는 힙 영역의 배열 객체를 참조합니다. 참조할 배열 객체가 없다면 배열 변수는 null 값으로 초기화될 수 있습니다.
타입[] 변수 = null;
값 목록으로 생성하기
배열 항목에 저장될 값의 목록이 이미 있다면 중괄호로 배열 객체를 만들 수 있습니다.
타입[] 변수 = {값0, 값1, 값2, ... };
단, 이 형태는 선언과 동시에 쓸 때만 허용됩니다. 배열 변수를 이미 선언한 후 다른 실행문에서 중괄호를 사용한 배열 생성은 허용되지 않습니다.
타입[] 변수;
변수 = {값0, 값1, 값2, ... }; // 컴파일 에러
배열 변수를 미리 선언해 두고 값 목록이 나중에 결정되는 상황이라면 new 연산자를 함께 씁니다.
변수 = new 타입[] {값0, 값1, 값2, ... };
메소드의 매개값이 배열일 때도 같은 제약이 적용됩니다.
int add(int[] scores) { ... }
int result = add( {95, 85, 90} ); // 컴파일 에러
int result = add( new int[] {95, 85, 90} );
new 연산자로 생성하기
값의 목록을 아직 가지고 있지 않지만 향후 값들을 저장할 배열을 미리 만들고 싶다면 new 연산자로 생성합니다.
타입[] 변수 = new 타입[길이];
타입[] 변수 = null;
변수 = new 타입[길이]; // 가능
이렇게 만든 배열의 각 항목은 타입별 기본 초기값으로 채워집니다.
| 분류 | 데이터 타입 | 초기값 |
|---|---|---|
| 기본 타입(정수) | byte[] |
0 |
| 기본 타입(정수) | char[] |
'\u0000' |
| 기본 타입(정수) | short[] |
0 |
| 기본 타입(정수) | int[] |
0 |
| 기본 타입(정수) | long[] |
0L |
| 기본 타입(실수) | float[] |
0.0F |
| 기본 타입(실수) | double[] |
0.0 |
| 기본 타입(논리) | boolean[] |
false |
| 참조 타입 | 클래스[] | null |
| 참조 타입 | 인터페이스[] | null |
배열 길이
배열의 길이란 배열에 저장할 수 있는 전체 항목 수를 말하며, 배열 객체의 length 필드를 읽으면 됩니다.
int[] intArray = { 10, 20, 30 };
int num = intArray.length;
length 필드는 읽기 전용 필드이기 때문에 값을 바꿀 수 없습니다.
main()의 String[] args도 배열이다
프로그램을 실행하려면 main() 메소드가 필요합니다. 그렇다면 그 매개값인 String[] args는 무엇일까요.
명령 프롬프트에서 java 클래스로 프로그램을 실행하면 JVM은 길이가 0인 String 배열을 먼저 생성하고, main() 메소드를 호출할 때 매개값으로 전달합니다. 그리고 java 클래스 문자열0 문자열1 문자열2 ... 문자열n-1로 실행하면 다음과 같이 문자열 배열을 만든 후 main() 호출 시 매개값으로 전달합니다.
String[] args = { 문자열0, 문자열1, 문자열2, ... , 문자열n-1 };
이클립스에서 매개값을 전달해서 실행하려면 [Run] - [Run Configurations…]를 선택한 뒤, Main 탭에서 Project와 Main Class를 확인하고 Arguments 탭의 Program arguments에 매개값을 입력합니다.
다차원 배열은 배열을 참조하는 배열이다
행과 열로 구성된 배열을 2차원 배열이라 하고, 가로 인덱스와 세로 인덱스를 사용합니다.
int[][] scores = new int[2][3];
이 코드가 만드는 것은 배열 하나가 아닙니다. 메모리에 세 개의 배열 객체를 생성합니다. 배열 변수 scores는 길이 2인 배열을 참조하고, 그 길이 2인 배열의 각 값은 길이 3인 배열 두 개를 각각 참조합니다.
일차원 배열을 연결한 구조로 다차원 배열을 구현하기 때문에, 계단식 구조도 가능합니다.
int[][] scores = new int[2][];
scores[0] = new int[2];
scores[1] = new int[3];
그룹화된 값 목록을 가지고 있다면 중괄호 안에 중괄호를 사용해서 생성할 수도 있습니다.
타입[][] 변수 = { {값1, 값2, ...}, {값1, 값2, ... }, ... };
참조 타입 배열은 항목에 값이 아니라 번지를 담는다
기본 타입 배열은 각 항목에 직접 값을 가지고 있지만, 참조 타입 배열은 각 항목에 객체의 번지를 가지고 있습니다. String[] 배열은 각 항목에 문자열이 아니라 String 객체의 주소를 가지고 있습니다. 따라서 String[] 배열 항목 간에 문자열을 비교하려면 == 대신 equals() 메소드를 사용해야 합니다.
String[] strArray = new String[3];
strArray[0] = "Java";
strArray[1] = "Java";
strArray[2] = new String("Java");
strArray[0] == strArray[1] // true — 문자열 리터럴이 동일하므로 동일 객체를 참조
strArray[0] == strArray[2] // false — new로 새 객체를 생성했으므로 다른 객체를 참조
strArray[0].equals(strArray[2]) // true — 문자열 리터럴의 값만을 비교
배열을 늘리려면 새로 만들고 복사해야 한다
배열은 한 번 생성하면 크기를 변경할 수 없으므로, 더 많은 저장 공간이 필요하다면 보다 큰 배열을 새로 만들고 이전 배열로부터 값을 복사해야 합니다. for문을 쓰거나 System.arraycopy() 메소드를 사용하면 됩니다.
System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
src— 원본 배열srcPos— 원본 배열의 복사할 항목의 시작 인덱스dest— 새 배열destPos— 새 배열에서 붙여 넣을 시작 인덱스length— 복사할 개수
원본 배열이 arr1이고 새 배열이 arr2인 경우 모든 항목을 복사하려면 다음과 같이 씁니다.
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
향상된 for문은 인덱스를 지운다
배열 및 컬렉션 객체를 좀 더 쉽게 처리할 목적으로 향상된 for문이 제공됩니다. 배열 및 컬렉션 항목의 수만큼 반복하고, 끝나면 자동으로 for문을 빠져나갑니다.
for( 타입 변수 : 배열 ) {
실행문;
}
동작 순서는 이렇습니다. 배열에서 가져올 값이 있는지 평가한 후, 값이 존재하면 해당 값을 변수에 저장합니다. 그리고 실행문을 실행한 뒤 다시 배열에서 가져올 값이 있는지 평가합니다.
열거 타입은 한정된 값만 갖는 참조 타입이다
열거 타입(enumeration type)은 몇 가지로 한정된 값만을 갖는 데이터 타입입니다.
열거 타입을 만들려면 이름을 정하고, 그 이름으로 소스 파일(.java)을 생성해야 합니다. 관례적으로 첫 문자를 대문자로 하고 나머지를 소문자로 합니다 — Week.java, MemberGrade.java, ProductKind.java.
소스 파일의 내용으로는 열거 타입 선언이 옵니다. 열거 상수 이름은 관례적으로 모두 대문자로 작성하고, 여러 단어로 구성될 경우 밑줄(_)로 연결합니다.
public enum Week {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
열거 타입 변수는 다음과 같이 선언하고 값을 넣습니다. 상수를 저장할 때는 반드시 열거타입.열거상수 형식으로 써야 합니다.
열거타입 변수;
열거타입 변수 = 열거타입.열거상수;
여기서 중요한 것은 열거 타입도 참조 타입이라는 점입니다. 그래서 null 값을 저장할 수 있습니다. 열거 타입도 객체이고, 열거 상수 각각이 객체로 생성되며, 열거 타입 변수는 그 객체의 주소를 참조합니다. 메소드 영역에 생성된 열거 상수가 해당 열거 객체를 각각 참조하게 됩니다.
Week week1 = Week.SATURDAY;
Week week2 = Week.SATURDAY;
week1 == week2 // true
참고로 자바는 컴퓨터의 날짜 및 요일, 시간을 프로그램에서 사용할 수 있도록 Date, Calendar, LocalDateTime 등의 클래스를 제공합니다.
열거 객체의 메소드
열거 객체는 열거 상수의 문자열을 내부 데이터로 가지고 있습니다. 모든 열거 타입은 컴파일 시에 Enum 클래스를 상속하도록 되어 있어서, java.lang.Enum 클래스에 선언된 메소드를 사용할 수 있습니다.
| 리턴 타입 | 메소드(매개 변수) | 설명 |
|---|---|---|
| String | name() |
열거 객체의 문자열을 리턴 |
| int | ordinal() |
열거 객체의 순번(0부터 시작)을 리턴 |
| int | compareTo() |
열거 객체를 비교해서 순번 차이를 리턴 |
| 열거 타입 | valueOf(String name) |
주어진 문자열의 열거 객체를 리턴 |
| 열거 배열 | values() |
모든 열거 객체들을 배열로 리턴 |
name()은 열거 객체가 가지고 있는 문자열을 리턴합니다. 열거 타입을 정의할 때 사용한 상수 이름과 동일합니다.
Week today = Week.SUNDAY;
String name = today.name(); // name 변수에는 SUNDAY 문자열이 저장됨
ordinal()은 열거 객체 중 몇 번째 열거 객체인지를 알려 줍니다. 열거 타입을 정의할 때 주어진 순번이며 0부터 시작합니다.
Week today = Week.SUNDAY;
int ordinal = today.ordinal(); // ordinal 변수에는 SUNDAY 객체의 순번(6)이 저장됨
compareTo()는 매개값으로 주어진 열거 객체를 기준으로 전후로 몇 번째 위치하는지를 비교합니다. 순번이 빠르면 음수, 늦으면 양수가 리턴됩니다.
Week day1 = Week.MONDAY;
Week day2 = Week.WEDNESDAY;
int result1 = day1.compareTo(day2); // day2를 기준으로 한 day1의 순번 차이(-2)를 리턴
int result2 = day2.compareTo(day1); // day1을 기준으로 한 day2의 순번 차이(2)를 리턴
valueOf()는 매개값으로 주어지는 문자열과 동일한 문자열을 가지는 열거 객체를 리턴합니다.
Week weekDay = Week.valueOf("SATURDAY"); // Week.SATURDAY 객체를 참조
values()는 열거 타입의 모든 열거 객체들을 배열로 만들어 리턴합니다. 배열의 인덱스는 열거 객체의 순번과 같고, 각 인덱스 값은 해당 순번의 열거 객체 번지입니다.
Week[] days = Week.values();
참조 타입을 다룰 때 기준은 하나입니다. 변수 안에 들어 있는 것은 값이 아니라 번지이고, 실제 데이터는 힙 어딘가에 따로 있습니다. ==가 예상과 다르게 동작하거나 NullPointerException이 나는 상황은 대부분 이 한 문장으로 설명됩니다.
'IT·테크 > 개발' 카테고리의 다른 글
| Git 기본기 — 스테이징부터 되돌리기와 브랜치까지 (0) | 2026.07.22 |
|---|---|
| 자바 연산자 정리 — 우선순위, 오버플로우, NaN과 Infinity (0) | 2026.07.22 |
| 자바 변수와 타입 — 기본 타입 8개와 타입 변환 규칙 (0) | 2026.07.20 |
| 자바 시작하기 — JVM과 바이트 코드, 그리고 개발 환경 (0) | 2026.07.19 |