자바를 배우다 보면 가장 먼저 마주치는 개념 중 하나가 ‘기본형’과 ‘참조형’입니다. 이 개념은 자바 프로그래밍 전반에 걸쳐 매우 중요하게 작용합니다. 메모리 구조부터 데이터 접근 방식, 그리고 객체 지향 프로그래밍에서의 활용까지 광범위하게 영향을 주기 때문이죠. 이번 글에서는 자바의 기본형과 참조형이 무엇인지, 그리고 이 둘의 차이는 무엇인지 쉽고 재미있게 살펴보겠습니다.
우리는 매일 여러 종류의 데이터를 다룹니다. 예를 들어, 점심 메뉴를 고를 때 ‘치킨(문자열)’, ‘피자(문자열)’, ‘돈까스(문자열)’ 같은 텍스트 정보를 쓰기도 하고, 영양정보를 위해 ‘800kcal(int)’, ‘2300원(int)’ 같은 숫자 정보도 사용하죠. 자바에서는 이런 데이터를 ‘타입’이라는 개념으로 관리합니다.
자바에는 크게 기본형(primitive type)과 참조형(reference type)이라는 두 가지 분류가 존재합니다. 말 그대로 기본형은 가장 간단하고 기본적인 데이터 형태를 의미합니다. 참조형은 객체(또는 배열, 문자열 등)를 가리키는 ‘참조값’을 저장하는 타입을 말합니다.
이 글을 다 읽으시면, 다음 질문에 자신 있게 답할 수 있을 거예요.
자바에서 제공하는 기본형은 총 8가지입니다.
byte
, short
, int
, long
float
, double
char
boolean
이들은 자바 언어 차원에서 직접 제공하는 것이며, 모두 소문자 키워드로 표기합니다.
int
는 4바이트(32비트), byte
는 1바이트(8비트) 크기를 가집니다.int number = 10;
이 코드를 실행하면, number
라는 변수가 메모리에 직접 10을 저장하게 됩니다.null
을 할당할 수도 없습니다(Integer
등 래퍼 클래스를 사용하면 예외적으로 가능하지만, 이 경우는 기본형이 아닌 참조형으로 분류됨).가장 흔하게 보는 예는 아래와 같습니다.
int age = 25;
double height = 175.5;
boolean isStudent = false;
char grade = 'A';
age
는 25라는 정수 값을 담고,height
는 175.5라는 실수 값을 담으며,isStudent
는 false라는 논리 값을 담습니다.grade
는 'A'라는 문자 하나를 저장하죠.참조형에는 크게 다음과 같은 것들이 있습니다.
String
, Integer
, ArrayList
, 사용자 정의 클래스 등 자바에서 우리가 직접 만드는 모든 객체(예: new
로 생성하는 클래스 인스턴스)와 배열, 그리고 문자열도 여기에 해당합니다.
String greeting = "Hello";
이라면, greeting
이라는 변수가 "Hello"
라는 문자열 객체의 메모리 주소를 참조하게 됩니다.String
클래스는 length()
, substring()
같은 메서드를 제공합니다.null
을 가질 수 있습니다.String name = "John Doe";
String emptyString = null; // 객체를 참조하지 않는 상태
int[] scores = {90, 85, 100}; // 배열도 참조형
ArrayList<String> list = new ArrayList<>();
list.add("사과");
list.add("바나나");
name
은 "John Doe"
라는 문자열 객체를 참조합니다.emptyString
은 null
이므로 어떤 문자열 객체도 참조하지 않습니다.scores
는 정수형 데이터를 담고 있는 배열 객체를 참조합니다.list
는 ArrayList<String>
객체를 생성한 뒤 그 참조값을 저장하고, 리스트에 여러 문자열을 추가할 수 있습니다.구분 | 기본형(Primitive) | 참조형(Reference) |
---|---|---|
메모리 저장 방식 | 스택(Stack)에 값 자체 저장 | 스택에 참조(주소) 저장, 실제 데이터는 힙(Heap)에 존재 |
변수 크기 | 고정 (예: int 는 4바이트) | 참조값(주소)의 크기 자체는 고정이나, 객체 내용은 가변적 |
null 사용 | 불가능 (boolean 등은 null 불가) | 가능 (객체를 가리키지 않는 상태) |
메서드 보유 | 없음 (원시 값이므로) | 클래스와 객체는 여러 메서드 보유 가능 |
표현 형태 | 소문자 키워드 (예: int , double ) | 대문자로 시작(클래스, 인터페이스), 배열, String 등 다양 |
이 차이를 명확히 이해하면 메모리 사용 최적화, 성능 향상, NullPointerException 방지 등의 측면에서 유용합니다. 기본형은 메모리를 적게 사용하고 접근 속도가 빠르지만, 참조형은 훨씬 더 유연하고 객체 지향적인 기능들을 지원합니다.
성능 고려
성능이 중요한 부분(예: 대규모 반복 연산)에서는 불필요한 객체 생성보다 기본형을 선호하는 경우가 많습니다. 예를 들어, 로깅하지 않을 단순 카운트 변수라면 Integer
대신 int
를 쓰는 편이 더 효율적일 수 있습니다.
Null 처리
데이터베이스 연동 시, 특정 컬럼 값이 존재하지 않을 수도 있습니다. 이럴 때 Integer
, Double
같은 참조형 래퍼 클래스를 쓰면 null
상태를 표현하기가 편리합니다. 하지만 기본형 int
, double
은 null
을 표현할 수 없죠.
컬렉션(Generics) 사용
자바 컬렉션 프레임워크(ArrayList
, HashMap
등)는 기본형을 직접 담을 수 없습니다. 대신 Integer
, Double
과 같은 박싱(boxing)된 래퍼 클래스를 사용해야 합니다. 실무에서 데이터를 리스트나 맵으로 관리할 때는 이 점을 꼭 고려해야 합니다.
오토박싱(Auto-Boxing)과 언박싱(Unboxing) 이슈
자바 5부터 오토박싱, 언박싱 기능이 제공되어 코드가 간결해졌지만, 성능 상의 오버헤드가 발생할 수 있습니다. 반복문 안에서 박싱/언박싱이 빈번하게 일어나는 경우, 예상치 못한 성능 저하가 발생할 수 있습니다.
.
을 찍고 메서드를 호출할 수 없다는 점을 기억하세요. 예를 들어, int a = 10; a.length();
는 불가능합니다.int
vs Integer
, double
vs Double
처럼 기본형과 래퍼 클래스는 용도가 다릅니다. 래퍼 클래스를 사용할 때는 null
처리와 오토박싱에 유의하세요.String
은 자바에서 매우 자주 쓰이지만, 결국은 참조형입니다. 변경이 불가능한(Immutable) 객체라는 특성 때문에 다른 참조형과 조금 다르게 동작하니, 필요하다면 StringBuilder
나 StringBuffer
를 사용하는 방법도 알아 두세요.ArrayList
나 LinkedList
같은 것은 동적으로 크기를 조정할 수 있는 참조형입니다. 상황에 맞춰서 사용하면 됩니다.자바에서 기본형은 값 자체를 스택에 저장하고, 참조형은 객체의 주소(참조값)를 스택에 저장하고 실제 데이터는 힙에 위치합니다. 이 차이점은 메모리 할당, 성능, null
처리, 그리고 객체 지향 프로그래밍의 설계 측면에서 매우 중요한 영향을 미칩니다.
null
불가, 박싱/언박싱 불필요 null
가능, 컬렉션 사용 시 필수실무에서도 이 차이를 이해하고 적절하게 적용하면, 더 견고하고 최적화된 코드를 작성할 수 있습니다. 초보 때는 이 둘의 차이가 다소 추상적으로 느껴지지만, 예제를 많이 접하고 직접 코드를 실행해 보면 자연스럽게 익숙해질 것입니다.
이 글이 여러분이 자바를 공부하고 실무에 적용할 때 조금이나마 도움이 되길 바랍니다. 다음에 더 재미있고 실용적인 자바 이야기를 가지고 돌아올게요!
자바에서 ‘참조형(Reference Type)’은 객체, 배열, 문자열 등 힙(Heap) 메모리에 생성된 실제 데이터를 가리키는 주소값을 저장하는 타입입니다. 간단히 말해, ‘실제 데이터가 어디 있는지’를 기억하고 있는 ‘포인터 개념’이라고 볼 수 있지만, C/C++의 포인터와 달리 직접 메모리 주소에 접근하거나 조작할 수는 없도록 안전하게 추상화되어 있습니다. 이번 글에서는 초보자도 이해하기 쉽도록, 그러나 조금 더 깊은 부분까지 파고들어가 보겠습니다.
String greeting = "Hello";
// greeting 변수는 힙에 있는 "Hello" 객체의 주소(참조값)을 저장
자바는 모든 인자를 ‘값에 의한 호출(call by value)’로 처리합니다.
public class ReferenceExample {
public static void main(String[] args) {
Person p = new Person("John");
modifyPerson(p);
// p.name 은 "Mike"로 변경되어 있음
}
static void modifyPerson(Person person) {
person.name = "Mike"; // 원본 객체의 name 변경
person = new Person("Sarah"); // 새로운 객체 할당, 원본 p는 영향을 받지 않음
}
}
자바 애플리케이션이 실행되면, JVM은 크게 메서드(Method) 영역, 스택(Stack) 영역, 힙(Heap) 영역으로 나뉘어 메모리를 사용합니다.
스택(Stack)
힙(Heap)
new
키워드 등으로 생성되는 실제 객체가 저장되는 영역 메서드(Method) 영역
참조형 객체는 힙에 만들어지므로, 객체가 커지거나 늘어날 수 있습니다. 배열 같은 경우는 길이가 고정되지만, ArrayList
, HashMap
등은 내부적으로 배열 크기를 조정하며 데이터를 담습니다. 이처럼 힙 영역은 런타임에 유연하게 확장될 수 있으나, 스택은 메서드 호출이 끝나면 사라지는 일시적인 구조라는 점이 특징입니다.
public class MyClass { ... }
), 그리고 표준 라이브러리의 모든 클래스(String
, Integer
, ArrayList
, HashMap
등)도 참조형입니다. int[] arr
를 선언하면, arr
라는 참조 변수는 힙 메모리에 생성된 배열 객체를 가리킵니다.String[] names = {"John", "Doe", "Mike"};
// names -> 힙에 생성된 String[] 배열 객체 -> 각각의 요소는 또다른 String 객체를 참조
자바에서 String
은 참조형이면서 불변(Immutable)한 특성을 가집니다.
String
객체를 한 번 생성하면 내부 값(문자열)은 바뀌지 않습니다. String
객체가 생성됩니다. String a = "Hello";
String b = "Hello";
// a와 b는 같은 "Hello" 객체(리터럴)를 가리킴
이러한 특성 덕분에 문자열이 자주 변경되는 시나리오에서는 StringBuilder
나 StringBuffer
를 사용하는 것이 좋습니다.
C/C++처럼 malloc
, free
를 직접 관리하지 않아도 된다는 점은 자바의 큰 장점입니다. 하지만 힙에 생성한 객체를 전혀 관리하지 않아도 되는 것은 아닙니다.
null
로 처리해 불필요한 객체를 빨리 해제할 수 있도록 유도하는 최적화 전략을 쓸 때도 있습니다. (단, 너무 자주 null
처리는 오히려 가독성을 해칠 수 있으니 상황에 맞게 사용해야 합니다.)C/C++의 포인터는 개발자가 직접 메모리 주소를 조작할 수 있지만, 자바의 참조는 추상화된 ‘주소값’으로, 프로그래머가 포인터 연산(덧셈, 뺄셈 등)을 할 수 없습니다.
null
이 들어갈 수 있기 때문에, 객체 메서드를 호출하기 전에 null 체크가 필수입니다.obj.toString()
을 호출했는데 obj
가 null
이라면 NullPointerException
이 발생하죠.Optional<T>
를 사용해서 좀 더 안전하게 null 처리를 하는 방식이 권장되기도 합니다.Person original = new Person("John");
Person shallowCopy = original; // 얕은 복사(동일 객체 참조)
Person deepCopy = new Person(original.name); // 깊은 복사(새 객체 생성)
int i = 10; list.add(i);
→ 실제로는 Integer.valueOf(i)
가 일어남자바의 참조형(Reference Type)은 객체 지향 프로그래밍을 구현하는 데 핵심적인 요소입니다. ‘실제 객체(데이터)는 힙에 있고, 스택에는 주소가 저장되는 구조’를 이해하면, 메모리 관리, 성능 최적화, 예외 처리, 코드 설계 등에서 훨씬 더 탄탄한 코드를 작성할 수 있게 됩니다.
null
체크 없이 메서드를 호출하면 NullPointerException
이 터질 수 있다. 이해를 더 높이기 위해서는 직접 배열, 리스트, 클래스, 인터페이스 등을 만들어 보고, 어떤 시점에 객체가 생성되고 참조가 유지·사라지는지 디버거로 확인해 보는 것이 가장 좋습니다. 이를 통해 자바의 메모리 모델과 참조형의 작동 방식을 체득할 수 있을 것입니다.
Q: “참조형을 제대로 사용하려면 어떤 연습을 해봐야 하나요?”
A: 클래스와 배열, 컬렉션을 다양하게 다루며, 객체 간 의존 관계를 설계하고 테스트해 보세요. 디버거로 스택과 힙에 어떤 변화가 일어나는지 시각적으로 확인해보는 것도 큰 도움이 됩니다.
클래스를 사용하지 않고, 학생 정보를 관리한다고 가정해봅시다:
package ref;
public class Method2WithoutClass {
public static void main(String[] args) {
String student1_name = "철수";
int student1_age = 12;
int student1_grade = 40;
String student2_name = "영희";
int student2_age = 8;
int student2_grade = 70;
printStudent(student1_name, student1_age, student1_grade);
printStudent(student2_name, student2_age, student2_grade);
}
static void printStudent(String name, int age, int grade) {
System.out.println("Student's name: " + name + ", age: " + age + ", grade: " + grade);
}
}
관련된 데이터가 따로따로 관리된다
student1_name
, student1_age
, student1_grade
… 같은 변수들이 계속 생기면서 복잡도가 증가합니다.재사용성이 떨어진다
데이터를 넘길 때 실수할 가능성이 크다
printStudent(student1_name, student1_age, student1_grade);
같은 호출에서 매개변수 순서를 실수하면 엉뚱한 값이 출력될 수 있습니다.이제 다시 클래스를 활용한 코드를 살펴봅시다.
package ref;
public class Method2 {
public static void main(String[] args) {
Student student1 = createStudent("철수", 12, 40);
Student student2 = createStudent("영희", 8, 70);
printStudent(student1);
printStudent(student2);
}
static Student createStudent(String name, int age, int grade){
Student student = new Student();
student.name = name;
student.age = age;
student.grade = grade;
return student;
}
static void printStudent(Student student) {
System.out.println("Student's name: " + student.name + ", age: " + student.age + ", grade: " + student.grade);
}
}
학생 정보를 객체(클래스)로 묶어 관리
Student
클래스를 사용하여 학생 정보를 하나의 객체로 만들었습니다.student1
과 student2
가 각각 객체(인스턴스)가 되므로, 변수가 많아져도 관리가 용이합니다.객체를 생성하는 메서드(createStudent
)를 활용하여 중복 코드 제거
createStudent
메서드로 분리하여 중복을 최소화했습니다.createStudent
메서드 하나만 수정하면 됩니다.데이터를 안전하게 전달
printStudent
메서드가 Student
객체 하나만 받도록 설계되어, 데이터의 순서를 실수할 가능성이 사라집니다.(name, age, grade)
를 매개변수로 직접 받았다면, 실수로 순서를 바꿔 입력할 위험이 있었을 것입니다.예를 들어, 학생의 학년(schoolYear
)을 추가하고 싶다고 가정해봅시다.
String student1_name = "철수";
int student1_age = 12;
int student1_grade = 40;
int student1_schoolYear = 6;
class Student {
String name;
int age;
int grade;
int schoolYear;
}
Student
클래스 안에 schoolYear
필드를 추가하면 기존 코드 대부분을 수정하지 않고도 기능을 확장할 수 있습니다.클래스를 사용하는 것은 객체지향 프로그래밍(OOP, Object-Oriented Programming)의 핵심 원칙을 따르는 것입니다.
캡슐화(Encapsulation)
Student
클래스 안에 묶어서 보호합니다.createStudent
, printStudent
)를 통해 데이터를 다루므로, 외부 코드가 직접 데이터를 수정할 필요가 없습니다.재사용성(Reusability)
Student
클래스를 한 번 만들면, 어디서든 객체를 생성하여 활용할 수 있습니다.Student
클래스를 가져와 재사용할 수 있습니다.유지보수성(Maintainability)
✔️ 관련 데이터를 하나로 묶어 관리할 수 있다.
✔️ 객체를 쉽게 생성하고 재사용할 수 있다.
✔️ 유지보수와 확장성이 좋아진다.
✔️ 코드가 더욱 가독성이 높아지고, 실수를 줄일 수 있다.
💡 클래스는 단순한 코드 작성 도구가 아니라, 코드의 효율성과 유지보수를 높이는 필수 개념입니다!