저번 Primitive type글에 이어서 작성한다
저번 글을 못봤다면, 여기 Java 카테고리에 있는 이전 글을 살펴보면 된다
이전 글의 서두를 옮겨와봤다
최근에 다시 자바공부를 하다가 이런 면접 질문이 있는 것을 봤다
Java는 Call by Value 일까요? 아님 Call by Reference 일까요?
사실 이 질문 자체로는 틀린 질문이라 생각한다
일단 맞는 질문으로 변환해보면
Call by Value 와 Call by Reference 의 차이점에 대해서 말해주세요
그럼, Java는 Call by Reference 방식을 지원할까요?
라는 꼬리질문식 질의문이 되어야 맞는다고 생각한다
자, 그럼 왜 저런 질문을 하는걸까?
일단 질문의 의도는 2가지이다
1. 헷갈리게 만들기
2. Java의 기본 동작과정을 잘 이해하고 있는가?를 알기 위해
- 헷갈리게 만들기는 사실 단순하다. Java는 Primitive Type/Reference Type이 있는데 Reference Type이 있으므로 공부가 부족한 개발자는 아주 단순하게 음! 자바는 Call By Reference구나! 라고 오해하게 만들기 쉽기 때문이다. 단순히 문자열이 일치해서 혼란을 주는 경우이다
이번에는 Reference Type의 값 복사를 테스트해본다
저번에 작성했던 JNI에 사용될 c코드는 primitive 타입만을 위한 소스코드였기때문에 이번에는 Object 타입도 출력할 수 있도록 수정하려다가.. 자바 파일도 새로 작성하는 김에 c코드 또한 새로 만들었다
두번째 Function의 맨 마지막 paramter를 보면 jobjet obj라고 되어있다
이 부분이 자바에서는 Object obj로 치환되어서 들어갈 것이다
그리고 %p는 c언어에서 주소값을 출력하는 표현이다
이어서 자바 코드를 작성해보자
이번에는 Primitive type인 int 자료형을 사용하지 않는 대신, Reference type이면서 Wrapper 클래스인 Integer 자료형을 사용해봤다
덧붙여서 String.format이나 문자열 + 방식이 아닌, MessageFormat의 format 메서드를 이용했다
그리고 이제 Object 객체를 상속받은 Integer를 사용하기 때문에 hashCode등도 사용이 가능하다
근데 나는 값 해시보다는 System상에서 메모리 해시값을 보고싶기 때문에 System의 identityHashCode를 사용할 것이다
여기서 다루는 관심사는 값의 변경, 해시코드, 메모리 주소 3가지이다
다시 복습해보자
- Stack 영역
- 기본형 변수(Primitive type): 값을 저장
- 참조형 변수(Reference type): 참조값(주소값)을 저장
- Heap 영역
- 객체 자체를 저장
그리고 Call by Value에서 참조형 변수가 인자로 사용될때는 해당 객체의 참조(reference)가 값으로 전달된다. 즉, 객체의 주소값이 복사되어 함수에 전달되는 것이다
이해하기 어렵겠지만 조금 쉽게 말해보면.. 메서드가 호출될 때마다 새로운 스택 프레임이 생성되고, 이 스택 프레임에 각 변수의 복사본이 저장된다. 따라서 메서드 내부의 참조 변수는 원래 변수와는 별개의 스택 메모리 주소를 갖게 된다는 것이다.
메서드 내부의 스택 메모리에 올라간 변수 또한 복사된 참조(주소값)을 갖고 있고, 이 값으로 힙 메모리의 값을 변경할 수는 있지만 원천적으로 메서드 외부에서의 변수와는 다른 메모리 주소를 갖고 있다.
휴.. 설명하기 어렵다. 모든 것은 실천적인 코드로 증명해야한다!!!
위의 코드를 실행해보면..?
결과를 예상해보자. 틀려도 좋다
음.... 아마도 add 메서드 내부에서만 값이 3000으로 되고, main 메서드에서는 그대로 2000이 유지될 것 같고
main 메서드와 add 메서드 내부에서 사용된 a는 각각 다른 주솟값을 가질 것이고(Call By Value인 이유)
hashCode도 같지 않을까..? 객체가 가리키는 주소를 해싱한 거니깐(Heap메모리의 객체 주소)
예상한 대부분이 맞았다. 하지만 add 메서드 내부에서 덧셈한 이후의 hashCode가 바뀌었다
왜 b+=1000이 실행되기 전의 해시코드와 이후의 해시코드가 다른걸까?
답을 말하는 걸 3초정도 기다려주겠다
3.....2.......1......
답은 Wrapper 클래스는 불변(Integer는 Immutable)이기 때문이다
- 불변객체란?
한마디로 한 번 메모리에 올라가면 객체의 상태를 변경할 수 없는 객체(값이 변경될 경우 새로운 객체가 생성된다)
만일 그 객체의 값을 변경하면, 변경된(새로운) 값이 또 다른 힙메모리에 적재되는 방식이다
- Wrapper Class(불변)
Byte, Short, Integer, Long, Float, Double, Character, Boolean
또한 자바에서는 연산자 오버로딩을 명시적으로 막아놨기때문에 기본자료형끼리의 연산만 지원한다
그말인즉슨, +-*/% 하는 사칙연산에 있어서 unboxing, boxing, autoboxing 등이 일어날 수 있다는 얘기와 같다
다시말해 a += 1000;은 a = Integer.valueOf(a.intValue() + 1000);이 된다는 뜻이다
불변객체이기때문에 상태를 바꾸면 새로운 힙 메모리에 올라가는 것이다
추가적으로 valueOf를 호출하게 되는데, 함수 내부를 보면 new 키워드로 새로운 메모리에 적재시킨다는 것을 알 수 있다
ㅇㅋ......그럼 가정을 또 세워보자
a += 1000; 라인 이전에는 메모리해시가 같다는 사실을 알았다
그럼 만약에 불변객체가 아닌 가변객체를 사용하면 메서드 내부에서 바꾼 상태가 외부에 영향을 미치면서, 같은 해시코드를 유지할까?
아쉽게도 사칙연산을 지원하는 숫자형 객체는 다 불변인 것으로 알고있다(BigDecimal, BigInteger 또한..)
하지만!! AtomicXX는 가변(Mutable)객체이다~!
이참에 기존 메서드명도 add에서 addImmutableVariable로 바꾸고, addMutableVariable 이라는 함수를 새로 만들어봤다
출력 결과를 보기전에 예상해보자
음.....가변객체라고 했으니 메서드 내에서 상태를 변경해도 원본이 변할것이고, Integer 와 다르게 내부적으로 언박싱/박싱/오토박싱 등이 일어나지 않으니까 결과값으로 3000, 같은 해시코드
but 메인메서드의 atomicInteger의 스택메모리 주소와 add..메서드의 atomicInteger의 스택메모리 주소만 다를 것이다
값(상태 변경): 2000(main) -> 3000(add) -> 3000(main)
해시코드: 865113938(main) -> 865113938(add) -> 865113938(main)
메모리: 0x16bd72a58(main) -> 0x16bd729e8(add) -> 0x16bd72a58(main)
+ add메서드의 스택 영역에 있는 0x16bd729e8는 힙메모리의 주소를 가리키기 때문에 내부 상태 변경이 언제든지 가능하다
나는 단순히 연산자 대신에 사용할 수 있는 덧셈 클래스가 어떤게 있을까 해서 AtomicInteger를 갖고 왔지만, 이 객체는 멀티스레드환경에서 원자적 연산(다른 스레드가 그 연산이 끝나기 전에 중간 상태를 볼 수 없도록 보장된 연산)이 필요할때 사용한다
그럼 이제 Immutable인 Integer와 Mutable인 AtomicInteger를 풀 코드로 비교해보자
log보다 print로 처리한 점, 중복 코드가 많은 점 등은 양해를 바란다...ㅎㅎ
logger로 하면 스레드명이나 시간까지 나와서 이런 배움 또는 블로깅용 코드에는 크게 적합하지는 않은 것 같다
값, 해시코드, 메모리 주소로 동작과정을 살펴봤다
Verification
이제는 검증 타임이다
자바는 Call By Value로만 동작한다 < 이것을 검증해본다
만약 Call By Reference로 동작한다면, 메모리 주소를 직접 참조하고 있어서 다른 힙 메모리 객체로 바꿀 수도 있지 않을까?
그리고 메서드를 탈출(return)한다고 해도 메모리 주소를 직접 참조하고 가리킨 곳을 바꾼 것이기때문에 origin 객체가 변경되어 있을 것으로 기대한다
// 추가라고 적힌 부분의 코드를 살펴보자
만약 Call by Value가 아닌 Call by Reference로 동작했다면, Integer와 AtomicInter의 값은 둘 다 5000으로 바뀌어 있을 것으로 기대한다(아 이거보니 테스트코드 짜고싶긴 하다.. actual/expected)
결과는..?
요약하면 <Immutable Variable>에서는 0x16bd42a58 주소값을 가진 스택영역의 변수가 Integer의 heap 메모리에 있는 객체의 주소값을 참조하고 있었고, add 함수 내부에서는 0x16bd429e8 주소값을 가진 친구가 복사를 한 것이다
0x16bd429e8로 쌩난리를 쳐봐도, 0x16bd42a58에는 영향을 미치지 않기 때문에 함수가 종료가 되면 origin에는 영향을 주지 않는다
What is Real Call by Reference?
그럼 정말 Call by Ref는 어떤걸까?
C++ 코드를 가져와봤다. C로도 가능하다
자바에서는 본 적 없는 &(엠퍼센드) 연산자를 볼 수 있다
결과를 살펴보자
C, C++ 에서는 포인터와 & 연산자를 통해 메모리의 주소에 direct access를 할 수 있기 때문에 Call By Reference가 가능하다
Java 에서는 Call By Value만을 지원하며, Reference Type을 함수인자로 전달할 경우 참조하는 메모리의 주소값을 복사해서 스택영역에 올려놓기 때문에 객체의 속성을 컨트롤하는것은 가능하지만, 객체 자체를 조작하는 건 불가능하다
다시 면접질문으로 돌아와서
Java는 Call by Value 일까요? 아님 Call by Reference 일까요?
의 질문을 바꿔보면
Java는 Call By Value와 Call By Reference 방식 중에 어떤 것을 사용할까요? 추가로 Call By Reference를 지원할까요?
라고 하는게 맞는 것 같다
Java는 모든 호출이 Call By Value로 동작합니다. 그리고 Call By Reference를 지원하지 않습니다
다음 포스팅으로는 가변객체의 Call By Value의 사례에 대해서 올릴 예정이다
'Program Language > Java' 카테고리의 다른 글
Reference type(Mutable Object)의 Call By Value 살펴보기 2 (0) | 2024.08.23 |
---|---|
Primitive type의 Call By Value 살펴보기(feat. JNI) (0) | 2024.08.20 |
Whitespace Characters 제거하기 + 여러가지 (2) | 2022.11.26 |
댓글