본문 바로가기
320x100
320x100

최근에 다시 자바공부를 하다가 이런 면접 질문이 있는 것을 봤다

 

Java는 Call by Value 일까요? 아님 Call by Reference 일까요?

사실 이 질문 자체로는 틀린 질문이라 생각한다

일단 맞는 질문으로 변환해보면

Call by Value 와 Call by Reference 의 차이점에 대해서 말해주세요
그럼, Java는 Call by Reference 방식을 지원할까요?

라는 꼬리질문식 질의문이 되어야 맞는다고 생각한다

 

다른 글에서 쓴 내용이긴 하지만 영어 자체를 하나하나 짤라서 해석해보자

 

Call 이란 무엇일까?

내가 vba다음으로 배운 언어인.. C언어를 예로 들어본다

C언어를 배울때 나오는 것은 함수의 선언/호출/정의부 용어가 나온다

영어로는 Declaration/Call/Definition라고 한다

함수 선언/호출/정의

그럼 이제 알았다. Call은 함수를 불러다 쓰는 부분이라는 것이다

 

그럼 by Value와 by Reference를 해석해보자

by Value: 값에 의해 / by Ref(생략): 참조에 의해

 

앞에서 찾은 Call을 붙여서 다시 해석해보자 Call By Value: 값에 의한 호출 / Call by Ref: 참조에 의한 호출

 

사전적 직독직해 말고, 두가지 차이가 발생하는 방식을 서술하면

Java

- Call By Value: 메서드가 호출될 때 인자로 전달된 변수의 "값"이 복사되어 전달

- Call By Ref: 객체의 참조(주소)가 전달될 때도 "값에 의한 호출"로 처리. 참조(객체의 메모리 주소)를 복사해서 전달하기 때문이다

 

자 그럼 간단하게 Call By Value 코드를 살펴보자

코드

혹시나 자바나 프로그래밍을 처음 배우고 있다면 눈으로 먼저 결과를 예상해보자

코드의 결과를 예상해보는 것은 좋은 연습이다(나중에 예측가능한 결과를 내기 위해 코드나 아키텍처를 바꾸기도 함)

음... main 함수에서 a라는 변수를 1000으로 초기화했고, add함수를 호출해서 a를 인자로 넘기고

add함수 내에서 1000을 더했으니..

before add: 1000 / after add: 2000이 나오지 않을까..?

 

결과

어? 왜 1000이지..?

사실 "어?" 라는 말은 개발자들 사이에서 금기이다.. ㅋㅋ 특히 배포할때나 개발할때 신입개발자나 주니어가 어?라고 하는 순간 살얼음 분위기 스타트ㅋㅋㅋ

 

C언어부터 시작하거나 운영체제나 컴퓨터구조 수업을 들었다면 프로그램은 모두 메모리 위에서 동작한다는 것을 배웠을 것이다

흔히 메모리 영역이 코드/스택/힙/데이터 라고 알고있겠지만

자바는 스택/힙/메서드(=static, =메타스페이스[자바8이후])/Native(JNI)/PC 레지스터 영역으로 나뉜다

어쨌든, 저 main 메서드나 add 메서드 안에서의 모든 일들도 위에서 언급한 영역 중 하나에 메모리가 적재되어서 값이 쓰인다는 말이다

 

그럼 다시 돌아가서 Call By Value를 살펴보자

[ Call By Value: 메서드가 호출될 때 인자로 전달된 변수의 "값"이 복사되어 전달 ]

값이 복사..? 음.. 다른 메모리에 값이 복사된다는 말이겠군!!

1. 0xab : 1000

2. add 내부) 0a7c: 1000 (값만 복사, 메모리 위치는 다른 곳)

 

ㅇㅋ 왜 그런지에 대한 가정은 세웠고.. 확인해보자!!

확인해보기에 앞서 한가지 짚고 넘어가야 할 부분이 있다

int a = 1000;

이 코드는 VM 즉, JVM의 메모리 영역 중 어디에 적재될까? 바로 스택 영역이다

그럼 스택 영역의 메모리주소를 알아보면 되겠다!! 는 안된다. 자바는 메모리에 직접적인 접근을 막아놨다(포인터 없음)

그래서 C, C++보다 더 안정적으로 프로그래밍을 할 수 있다는 장점이 있다(포인터 사용은 초급 개발자들한테 위험)

 

ㅇㅋ 그럼 두번째 방법.....

자바에서는 hashCode라는 것을 통해 객체주소가 해싱된 값을 직간접적으로 확인해서 == 등으로 비교해볼 수 있다는데!!

hashCode를 이용하면 되겠네!! 또한 안된다. 왜냐하면 윗줄에 키워드가 있다 "객체주소가 해싱된", "객체"

우리가 지금 보려고 하는건 객체일까? 아니다! int a, 즉 primitive. 원형 타입이다(원형탈모 아님)

자바에서는 Primitive, Wrapper(Reference) 크게 2가지 타입이 있다

더 나아가서 모든 Reference 타입은 Object를 상속받고 있기 때문에 hashcode 비교가 가능하다

primitive 타입의 변수는 hashCode 메서드가 없다

 

여기서 귀여운 태클이 들어올 수 있다

java.lang 패키지의 System 스태틱 메서드인 identityHashCode를 이용해보는건!?

System.identityHashCode()

오~ 좋은생각인데? 라고 말한 사람이 있다면... 아쉽게 발바닥정도로만 박수를 쳐줄 수 있을 것 같다

그 이유는 System.identityHashCode() 코드를 살펴보면 알 수 있다

System 객체의 identityHashCode()
System final 클래스

identityHashCode() 메서드의 singature중 parameter를 살펴보자 Object x 이다

이 말뜻은 무엇이냐하면 Primitive 타입을 전달하면 자동으로 Wrapping되어서 Wrapper 클래스로 변환되어서 argument로 들어간다는 뜻이다 => Auto Boxing(오토박싱)

그럼 위의 말이 맞는지 코드로 확인해보자

반복된 해시코드 호출

결과를 보기전에 예상해보자

음... 같은 해시코드가 나오지 않을까?

다른 해시코드

틀렸다. 저 함수를 호출하면 할수록 오토박싱이 일어나서 Wrapper 객체를 인자로 사용하게 되고, 그 객체는 Heap 메모리에 적재되어서 계속 다른 메모리를 참조하고, 해싱된 결과도 다르게 나오는 것이다

여기서 객체는 Heap 메모리에 저장된다는 포인트를 언급했다

그럼 정말 맞는지, 이번에는 오토박싱이 일어나지 않는 Wrapper 클래스로 테스트해볼 것이다

primitive 타입과 wrapper(ref) 타입의 identityHashCode 비교

이번에는 빠르게 맞는 예상을 해본다

아마도 Integer b는 오토박싱이 일어나지 않으니까 같은 메모리주소를 출력하지 않을까?

딩동댕

맞다. Integer b는 오토박싱이 일어나지 않으므로 같은 해시주소를 반환한다

실제로 동작하는 오토박싱 코드

결론적으로 Integer.valueOf(a)로 동작하게 되고, 내부를 확인해보면 결국 new 연산자로 새로 힙메모리에 객체를 올리는 것과 같은 결과를 도출하게 된다

valueOf는 결국 new

 

하아...그래서 결국 스택영역에 쌓이는 primitive type(또는 참조형의 주소값)은 새 메모리에 적재되는지 자바에서는 확인할 수 없는거야..?

라고 생각하는 당신을 위해 JNI, 즉 Java Native Interface를 사용해서 볼 수 있는 방법을 제시해본다

 

간단하다. 그냥 java코드에 native 메서드를 추가하고 그 메서드를 호출하면 된다

native 메서드는 c로 작성하고, library로 만들어서 자바 소스코드와 함께 컴파일하고 java 명령어로 c 라이브러리와 함께 바이트코드를 실행하면 된다.

 

1. java코드 작성

CallByValueJNI.java

 

2. c 코드 작성

CallByValueJNI.c

C언어를 해본 사람은 알겠지만 %p는 메모리주소를 출력하는 포맷이다

 

3. java 파일 컴파일 해서 바이트코드(.class)로 변환

나는 JDK21을 사용했기 때문에 21버전으로 컴파일하는 명령어를 사용했다

# https://code-boki.tistory.com/

javac -source 21 -target 21 CallByValueJNI.java

java 소스코드 컴파일(.class)

 

4. 컴파일된 byte코드를 참고해서 c소스코드에서 사용할 헤더파일 생성

# https://code-boki.tistory.com/

javac -h . CallByValueJNI.java

c소스파일에서 사용할 헤더파일 생성

 

5. c코드 컴파일 및 java 실행시에 같이 추가할 공유 라이브러리 생성

나는 MacOS를 사용중이라서 .dylib 확장자를 사용했지만 Windows의 경우 .dll, Linux의 경우는 .so 확장자를 사용하면 된다

마지막으로 -I옵션 첫번째로 jni.h 헤더파일이 있는 경로를 지정하고, 두번째로는 jni_md.h 헤더파일이 있는 추가 경로를 지정하면 되는데, MacOS의 경우 darwn, Windows의 경우 win32, Linux의 경우 linux를 사용하면 된다

# https://code-boki.tistory.com/

gcc -shared -o libNativeLib.dylib -fPIC CallByValueJNI.c -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin"

 

dylib 파일 생성

 

6. Java 프로그램을 실행하면서 네이티브 메서드를 호출할 수 있도록 JVM이 네이티브 라이브러리를 찾을 수 있게 경로를 지정해서 실행한다

# https://code-boki.tistory.com/

java -Djava.library.path=. CallByValueJNI

코드와 실행 결과를 비교해보자

java code
Java + native lib(c) 실행결과

 

main() 메서드에서의 int a값은 1000이고 주소값은 0x16c04aa6c였다

add() 메서드 내에서의 int a값은 내부적으로 1000이 더해져서 2000이 되었고, 그 변수의 주소값은 0x16c04a9fc이다

add() 호출이 종료되고 main() 메서드에서 다시 출력한 a의 값은 변한것 없이 1000이고, 주소값은 0x16c04aa6c이다

 

결론적으로

add()안에서 지지고 볶고 해봤자 우리집이 아닌 다른집에 가서 어지럽히고 다시 우리집으로 온 것과 똑같은 것이다

 

cleanup으로 생성된 파일들은 아래의 명령어로 지우면 된다

# https://code-boki.tistory.com/

rm -f CallByValueJNI.class CallByValueJNI.h libNativeLib.dylib

우리에게 중요한 관심사는 컴파일된 목적/바이트코드 파일이 아니라 Java 코드가 native C언어와 함께 실행되었다는 사실이 중요한 것이니 말이다

 

make 파일 일부

위의 과정들이 귀찮으면 make파일로 만들어서 make 한번에 다 실행되도록 만들던가 shell script로 작성하면 된다

중간중간 과정들을 silent 시키려고 앞에 @를 붙여줬다

 


 

 

이 글에서 primitive 타입 변수의 Call By Value를 살펴봤다

 

Call by Value 와 Call by Reference 의 차이점에 대해서 말해주세요
그럼, Java는 Call by Call by Reference 방식을 지원할까요?

 

이 질문에 대한 답은 이 시리즈의 맨 마지막에 답할 예정이다

 

이어서 다음 글에서는 Reference 타입(Object, Array, Wrapper) 변수에 대해 Call By Value 방식이 어떻게 작동하는지를 살펴볼 예정이다

320x100

댓글