[Java] 기본형과 참조형
김영한의 실전 자바 - 기본편 강의 내용을 정리한 글이다.
1. 기본형 vs 참조형 — 시작
자바에서 참조형을 제대로 이해하는 것은 정말 중요하다. 변수의 데이터 타입을 가장 크게 보면 기본형과 참조형으로 분류할 수 있다.
- 기본형(Primitive Type):
int,long,double,boolean처럼 변수에 사용할 값을 직접 넣을 수 있는 데이터 타입이다. - 참조형(Reference Type):
Student student1,int[] students와 같이 데이터에 접근하기 위한 참조(주소)를 저장하는 데이터 타입이다. 참조형은 객체 또는 배열에 사용된다.
쉽게 말해서 기본형 변수에는 직접 사용할 수 있는 값이 들어있지만, 참조형 변수에는 위치(참조값)가 들어가 있다. 참조형 변수를 통해서 뭔가를 하려면 결국 참조값을 통해 해당 위치로 이동해야 한다.
기본형 vs 참조형 — 기본
- 기본형은 숫자
10,20과 같이 실제 사용하는 값을 변수에 담을 수 있다. 그래서 해당 값을 바로 사용할 수 있다. - 참조형은 실제 사용하는 값을 변수에 담는 것이 아니다. 이름 그대로 실제 객체의 위치(참조, 주소)를 저장한다. 참조형에는 객체와 배열이 있다.
- 객체는
.(dot)을 통해서 메모리 상에 생성된 객체를 찾아가야 사용할 수 있다. - 배열은
[]를 통해서 메모리 상에 생성된 배열을 찾아가야 사용할 수 있다.
- 객체는
기본형 vs 참조형 — 계산
- 기본형은 들어있는 값을 그대로 계산에 사용할 수 있다. (더하고 빼고, 사용하고 등등 — 숫자 같은 것들은 바로 계산할 수 있다)
- 참조형은 들어있는 참조값을 그대로 사용할 수 없다. 주소지만 가지고는 할 수 있는 것이 없다. 주소지에 가야 실체가 있다. (더하고 빼고 사용하고 못함 — 참조값만 가지고는 계산할 수 있는 것이 없다)
기본형은 연산이 가능하지만 참조형은 연산이 불가능하다.
기본형은 변수에 실제 사용하는 값이 담겨있다. 따라서 +, -와 같은 연산이 가능하다.
참조형은 변수에 객체의 위치인 참조값이 들어있다. 참조값은 계산에 사용할 수 없다. 따라서 오류가 발생한다.
물론 다음과 같이 .을 통해 객체의 기본형 멤버 변수에 접근한 경우에는 연산을 할 수 있다.
쉽게 이해하는 팁
기본형을 제외한 나머지는 모두 참조형이다.
- 기본형은 소문자로 시작한다.
int,long,double,boolean모두 소문자로 시작한다.- 기본형은 자바가 기본으로 제공하는 데이터 타입이다. 이러한 기본형은 개발자가 새로 정의할 수 없다. 개발자는 참조형인 클래스만 직접 정의할 수 있다.
- 클래스는 대문자로 시작한다.
Student- 클래스는 모두 참조형이다.
참고 — String
자바에서String은 특별하다.String은 사실 클래스다. 따라서 참조형이다. 그런데 기본형처럼 문자 값을 바로 대입할 수 있다. 문자는 매우 자주 다루기 때문에 자바에서 특별하게 편의 기능을 제공한다.String에 대한 자세한 내용은 뒤에서 설명한다.
2. 기본형 vs 참조형 — 변수 대입
대원칙: 자바는 항상 변수의 값을 복사해서 대입한다.
자바에서 변수에 값을 대입하는 것은 변수에 들어 있는 값을 복사해서 대입하는 것이다. 기본형, 참조형 모두 항상 변수에 있는 값을 복사해서 대입한다. 기본형이면 변수에 들어 있는 실제 사용하는 값을 복사해서 대입하고, 참조형이면 변수에 들어 있는 참조값을 복사해서 대입한다.
이 대원칙을 이해하면 복잡한 상황에도 코드를 단순하게 이해할 수 있다.
기본형 대입
참조형 대입
기본형은 변수에 값을 대입하더라도 실제 사용하는 값이 변수에 바로 들어있기 때문에 해당 값만 복사해서 대입한다고 생각하면 쉽게 이해할 수 있다. 그런데 참조형의 경우 실제 사용하는 객체가 아니라 객체의 위치를 가리키는 참조값만 복사된다. 쉽게 이야기해서 실제 건물이 복사가 되는 것이 아니라 건물의 위치인 주소만 복사되는 것이다. 따라서 같은 건물을 찾아갈 수 있는 방법이 하나 늘어날 뿐이다.
기본형과 변수 대입 — 상세
VarChange1 예제를 통해 기본형 변수 대입을 자세히 살펴본다.
실행 결과는 다음과 같다.
int b = a라고 했을 때 변수에 들어있는 값을 복사해서 전달한다. 따라서 a=20, b=30이라고 했을 때 각각 본인의 값만 변경되는 것을 확인할 수 있다.
변수의 대입은 변수에 들어있는 값을 복사해서 대입한다. 변수 a에 들어있는 값 10을 복사해서 변수 b에 대입한다. 변수 a 자체를 b에 대입하는 것이 아니다. 이후 a를 변경해도 b에는 아무런 영향을 주지 않는다.
참조형과 변수 대입 — 상세
참조형 예시를 위해 Data 클래스를 만든다. 이 클래스는 단순히 int value라는 멤버 변수를 하나 가진다.
VarChange2 예제로 참조형 변수 대입을 살펴본다.
실행 결과는 다음과 같다.
변수의 대입은 변수에 들어있는 값을 복사해서 대입한다. 변수 dataA에는 참조값 x001이 들어있다. Data dataB = dataA에서 변수 dataA에 들어있는 참조값 x001을 복사해서 변수 dataB에 대입한다. 참고로 변수 dataA가 가리키는 인스턴스를 복사하는 것이 아니다. 변수에 들어있는 참조값만 복사해서 전달한다.
이제 dataA와 dataB에 들어있는 참조값은 같다. 따라서 둘 다 같은 x001 Data 인스턴스를 가리킨다.
dataA.value = 20을 실행하면 dataA가 가리키는 x001 인스턴스의 value 값을 10에서 20으로 변경한다. 그런데 dataA와 dataB는 같은 x001 인스턴스를 참조하기 때문에 dataA.value와 dataB.value는 둘 다 같은 값인 20을 출력한다.
마찬가지로 dataB.value = 30을 실행하면 dataB가 가리키는 x001 인스턴스의 value 값을 20에서 30으로 변경한다. dataA와 dataB는 같은 x001 인스턴스를 참조하기 때문에 dataA.value와 dataB.value는 같은 값인 30을 출력한다.
여기서 핵심은 Data dataB = dataA라고 했을 때 변수에 들어있는 값을 복사해서 사용한다는 점이다. 그런데 그 값이 참조값이다. 따라서 dataA와 dataB는 같은 참조값을 가지게 되고, 두 변수는 같은 객체 인스턴스를 참조하게 된다.
3. 기본형 vs 참조형 — 메서드 호출
대원칙: 자바는 항상 변수의 값을 복사해서 대입한다.
메서드를 호출할 때 사용하는 매개변수(파라미터)도 결국 변수일 뿐이다. 따라서 메서드를 호출할 때 매개변수에 값을 전달하는 것도 앞서 설명한 내용과 같이 값을 복사해서 전달한다.
기본형과 메서드 호출
실행 결과는 다음과 같다.
메서드를 호출할 때 매개변수 x에 변수 a의 값을 전달한다. 이 코드는 다음과 같이 해석할 수 있다.
자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입한다. 따라서 변수 a, x 각각 숫자 10을 가지고 있다. 메서드 안에서 x = 20으로 새로운 값을 대입한다. 결과적으로 x의 값만 20으로 변경되고, a의 값은 10으로 유지된다. 메서드가 종료되면 매개변수 x는 제거된다.
참조형과 메서드 호출
실행 결과는 다음과 같다.
메서드를 호출할 때 매개변수 dataX에 변수 dataA의 값을 전달한다. 이 코드는 다음과 같이 해석할 수 있다.
자바에서 변수에 값을 대입하는 것은 항상 값을 복사해서 대입한다. 변수 dataA는 참조값 x001을 가지고 있으므로 참조값을 복사해서 전달했다. 따라서 변수 dataA, dataX 둘 다 같은 참조값인 x001을 가지게 된다. 이제 dataX를 통해서도 x001에 있는 Data 인스턴스에 접근할 수 있다.
메서드 안에서 dataX.value = 20으로 새로운 값을 대입한다. 참조값을 통해 x001 인스턴스에 접근하고 그 안에 있는 value의 값을 20으로 변경했다. dataA, dataX 모두 같은 x001 인스턴스를 참조하기 때문에 dataA.value와 dataX.value는 둘 다 20이라는 값을 가진다.
기본형과 참조형의 메서드 호출 정리
자바에서 메서드의 매개변수(파라미터)는 항상 값에 의해 전달된다. 그러나 이 값이 실제 값이냐, 참조(메모리 주소)값이냐에 따라 동작이 달라진다.
- 기본형: 메서드로 기본형 데이터를 전달하면, 해당 값이 복사되어 전달된다. 이 경우, 메서드 내부에서 매개변수(파라미터)의 값을 변경해도, 호출자의 변수 값에는 영향이 없다.
- 참조형: 메서드로 참조형 데이터를 전달하면, 참조값이 복사되어 전달된다. 이 경우, 메서드 내부에서 매개변수(파라미터)로 전달된 객체의 멤버 변수를 변경하면, 호출자의 객체도 변경된다.
4. 참조형과 메서드 호출 — 활용
참조형은 메서드를 호출할 때 참조값을 전달한다. 따라서 메서드 내부에서 전달된 참조값을 통해 객체의 값을 변경하거나, 값을 읽어서 사용할 수 있다.
메서드에 객체 전달
중복 코드를 메서드로 분리하는 예시다. Student 클래스를 만들고 initStudent(), printStudent() 메서드를 통해 중복을 제거한다.
initStudent(Student student, ...): 전달한 학생 객체의 필드에 값을 설정한다.printStudent(Student student, ...): 전달한 학생 객체의 필드 값을 읽어서 출력한다.
참조형은 메서드를 호출할 때 참조값을 전달한다. 이 메서드를 호출하면서 student1을 전달한다. 그러면 student1의 참조값이 매개변수 student에 전달된다. 이 참조값을 통해 initStudent() 메서드 안에서 student1이 참조하는 것과 동일한 x001 Student 인스턴스에 접근하고 값을 변경할 수 있다.
메서드에서 객체 반환
한 발 더 나아가 객체를 생성하고 초기값을 설정하는 부분을 하나의 메서드로 합쳐보자.
createStudent()라는 메서드를 만들고 객체를 생성하는 부분도 이 메서드 안에 함께 포함했다. 이제 이 메서드 하나로 객체의 생성과 초기값 설정을 모두 처리한다.
그런데 메서드 안에서 객체를 생성했기 때문에 만들어진 객체를 메서드 밖에서 사용할 수 있게 돌려주어야 한다. 메서드는 호출 결과를 반환(return)할 수 있다. 메서드의 반환 기능을 사용해서 만들어진 객체의 참조값을 메서드 밖으로 반환하면 된다.
createStudent()는 생성한 Student 인스턴스의 참조값을 반환한다. 이렇게 반환된 참조값을 student1 변수에 저장했다. 앞으로는 student1을 통해 Student 인스턴스를 사용할 수 있다.
5. 변수와 초기화
변수의 종류
- 멤버 변수(필드): 클래스에 선언한다.
- 지역 변수: 메서드에 선언한다. 매개변수도 지역 변수의 한 종류이다.
지역 변수는 이름 그대로 특정 지역에서만 사용되는 변수라는 뜻이다. 예를 들어서 변수 x는 changePrimitive() 메서드의 블록에서만 사용된다. changePrimitive() 메서드가 끝나면 제거된다. a 변수도 마찬가지이다. main() 메서드가 끝나면 제거된다.
변수의 값 초기화
- 멤버 변수: 자동 초기화
- 인스턴스의 멤버 변수는 인스턴스를 생성할 때 자동으로 초기화된다.
- 숫자(
int) =0,boolean=false, 참조형 =null(null값은 참조할 대상이 없다는 뜻으로 사용된다) - 개발자가 초기값을 직접 지정할 수 있다.
- 지역 변수: 수동 초기화
- 지역 변수는 항상 직접 초기화해야 한다.
실행 결과는 다음과 같다.
value1은 초기값을 지정하지 않았지만 멤버 변수는 자동으로 초기화된다. 숫자는 0으로 초기화된다. value2는 10으로 초기값을 지정해두었기 때문에 객체를 생성할 때 10으로 초기화된다.
6. null
참조형 변수에는 항상 객체가 있는 위치를 가리키는 참조값이 들어간다. 그런데 아직 가리키는 대상이 없거나, 가리키는 대상을 나중에 입력하고 싶다면 어떻게 해야 할까? 참조형 변수에서 아직 가리키는 대상이 없다면 null이라는 특별한 값을 넣어둘 수 있다. null은 값이 존재하지 않는, 없다는 뜻이다.
택배를 보낼 때 제품은 준비가 되었지만, 보낼 주소지가 아직 결정되지 않아서 주소지가 결정될 때까지는 주소지를 비워두어야 할 수 있다. 이런 상황에서 null을 활용한다.
GC — 아무도 참조하지 않는 인스턴스의 최후
data에 null을 할당했다. 따라서 앞서 생성한 x001 Data 인스턴스를 더는 아무도 참조하지 않는다. 이렇게 아무도 참조하지 않게 되면 x001이라는 참조값을 다시 구할 방법이 없다. 따라서 해당 인스턴스에 다시 접근할 방법이 없다.
이렇게 아무도 참조하지 않는 인스턴스는 사용되지 않고 메모리 용량만 차지할 뿐이다. C와 같은 과거 프로그래밍 언어는 개발자가 직접 명령어를 사용해서 인스턴스를 메모리에서 제거해야 했다. 만약 실수로 인스턴스 삭제를 누락하면 메모리에 사용하지 않는 객체가 가득해져서 메모리 부족 오류가 발생하게 된다.
자바는 이런 과정을 자동으로 처리해준다. 아무도 참조하지 않는 인스턴스가 있으면 JVM의 GC(가비지 컬렉션)가 더 이상 사용하지 않는 인스턴스라 판단하고 해당 인스턴스를 자동으로 메모리에서 제거해준다. 객체는 해당 객체를 참조하는 곳이 있으면, JVM이 종료될 때까지 계속 생존한다. 그런데 중간에 해당 객체를 참조하는 곳이 모두 사라지면 그때 JVM은 필요 없는 객체로 판단하고 GC(가비지 컬렉션)를 사용해서 제거한다.
7. NullPointerException
NullPointerException은 이름 그대로 null을 가리키다(Pointer)인데, 이때 발생하는 예외(Exception)다. null은 없다는 뜻이므로 결국 주소가 없는 곳을 찾아갈 때 발생하는 예외이다.
객체를 참조할 때는 .(dot)을 사용한다. 이렇게 하면 참조값을 사용해서 해당 객체를 찾아갈 수 있다. 그런데 참조값이 null이라면 값이 없다는 뜻이므로, 찾아갈 수 있는 객체(인스턴스)가 없다. NullPointerException은 이처럼 null에 .(dot)을 찍었을 때 발생한다.
data 참조형 변수에는 null 값이 들어가 있다. 그런데 data.value = 10이라고 하면 어떻게 될까? 이는 다음과 같이 해석된다.
결과적으로 null 값은 참조할 주소가 존재하지 않는다는 뜻이다. 따라서 참조할 객체 인스턴스가 존재하지 않으므로 java.lang.NullPointerException이 발생하고, 프로그램이 종료된다.
멤버 변수와 null
지역 변수의 경우에는 null 문제를 파악하는 것이 어렵지 않다. 다음과 같이 멤버 변수가 null인 경우에는 주의가 필요하다.
BigData 클래스는 Data data, int count 두 변수를 가진다.
BigData를 생성하면 BigData의 인스턴스가 생성된다. 이때 BigData 인스턴스의 멤버 변수에 초기화가 일어난다. BigData의 data 멤버 변수는 참조형이므로 null로 초기화된다. count 멤버 변수는 숫자이므로 0으로 초기화된다.
bigData.count를 출력하면0이 출력된다.bigData.data를 출력하면 참조값인null이 출력된다. 이 변수는 아직 아무것도 참조하고 있지 않다.bigData.data.value를 출력하면data의 값이null이므로null에.(dot)을 찍게 되고, 따라서 참조할 곳이 없으므로NullPointerException예외가 발생한다.
이 문제를 해결하려면 Data 인스턴스를 만들고 BigData.data 멤버 변수에 참조값을 할당하면 된다.
bigData.data.value의 실행 과정은 다음과 같다.
정리:
NullPointerException이 발생하면null값에.(dot)을 찍었다고 생각하면 문제를 쉽게 찾을 수 있다.
8. 정리
기본형 vs 참조형 — 기본
| 구분 | 기본형 (Primitive) | 참조형 (Reference) |
|---|---|---|
| 저장 내용 | 실제 값을 직접 저장 | 객체의 위치(참조값, 주소)를 저장 |
| 예시 | int, long, double, boolean |
클래스, 배열 (Student, int[]) |
| 이름 규칙 | 소문자로 시작 | 대문자로 시작 (클래스명) |
| 산술 연산 | 가능 (a + b) |
불가능 (참조값으로 계산 불가) |
| null 할당 | 불가능 | 가능 |
| 개발자 정의 | 불가능 (자바 내장) | 가능 (클래스 직접 정의) |
기본형 vs 참조형 — 대입
자바는 항상 변수의 값을 복사해서 대입한다.
- 기본형과 참조형 모두 대입 시 변수 안에 있는 값을 읽고 복사해서 전달한다.
- 기본형은 사용하는 값을 복사해서 전달하고, 참조형은 참조값을 복사해서 전달한다. 실제 인스턴스가 복사되는 것이 아니다. 인스턴스를 가리키는 참조값을 복사해서 전달하는 것이다. 따라서 하나의 인스턴스를 여러 곳에서 참조할 수 있다.
- 헷갈리면 그냥 변수 안에 들어간 값을 떠올려보자. 기본형은 사용하는 값이, 참조형은 참조값이 들어있다. 변수에 어떤 값이 들어있든간에 그 값을 그대로 복사해서 전달한다.
기본형 vs 참조형 — 메서드 호출
- 메서드 호출 시 기본형은 메서드 내부에서 매개변수(파라미터)의 값을 변경해도 호출자의 변수 값에는 영향이 없다.
- 메서드 호출 시 참조형은 메서드 내부에서 매개변수(파라미터)로 전달된 객체의 멤버 변수를 변경하면, 호출자의 객체도 변경된다.
변수 초기화 규칙
- 멤버 변수(필드)는 인스턴스 생성 시 자동으로 초기화된다. 숫자는
0,boolean은false, 참조형은null. - 지역 변수는 자동 초기화가 되지 않으므로 반드시 직접 초기화해야 한다. 초기화하지 않고 사용하면 컴파일 오류가 발생한다.