프로그래밍 언어가 어떻게 메모리 자원을 관리하고 저장하는지 궁금한 계기가 생겼고 C언어의 맛을 살짝 보았다.
그래서 새롭게 알게 된 지역 변수,전역 변수, 상수 그리고 스택, 힙 영역등 메모리 구조를 통해서 바라본 변수 타입에 대한 내용을 기록하려한다.
그런데 왜 C언어가 아닌 자바로 키워드를 잡았을까?
그 이유는 바로 Java에는 독특한 타입인 String 이 있었기 때문이다.
서론을 끝마치고 어서 알아보자!
위의 이미지로 대략적인 흐름을 이해할 수 있는데
메모리 구조에서는 총 4개의 메모리 공간을 확인할 수 있다.
여기서 핵심적인 부분은 데이터 영역에 저장된 데이터는 프로그램이 종료될 때 까지 남아있다는 것입니다.
전역 변수가 지역 변수와 달리 선언된 함수 외부에서도 호출될 수 있는건 할당받는 메모리 공간의 해제 작업이 일어나기 않고 남아 있기때문(종료까지 소멸되지 않으므로)
이러한 부분을 응용한다면 우리가 코드를 상수로 선언하는 이유를 알 수 있다.
// 정수형 상수 선언
public static final int MAX_VALUE = 100;
// 문자열 상수 선언
public static final String COMPANY_NAME = "ABC Corporation";
위와 같이 상수를 선언하는 이유는 가독성,불변성(수정 허용 x)의 이유도 있지만 메모리에서 얻는 이점을 말하면 이렇다.
장점
단점
상수 선언의 이유를 데이터 영역을 이해함으로서 메모리에서 얻을 수 있는 이점과 연관시킬 수 있는것을 확인했습니다.
스택(Stack)
힙(Heap)
스택(Stack)과 달리 힙 영역은 사용자가 동적으로 메모리를 할당하는 공간이다.
즉, 메모리의 크기가 일정하지 않을뿐더러 스택에 비해 차지하는 공간의 크기 또한 크다.
즉, 하나의 객체를 여러 참조 변수에서 공유해서 사용한다면
메모리를 효율적으로 관리할 수 있기 때문이아닐까
생각해 보았다.
그렇다면 참조 타입의 경우 매개변수로서 어떻게 동작되는지 궁금하였다.
결론부터 말한다면 CallByValue 형식으로 값이 전달된다.
(CallByReference가 아닌 CallByValue 인 이유는
[자바가 언제나 Call By Value인 이유]
글을 참고했습니다.)
즉, 객체 자체가 전달되는 것이 아닌 객체의 참조 값(Reference)의 복사본이 전달되는것이다.
예시 코드
class MyClass {
int value;
MyClass(int value) {
this.value = value;
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass(10);
modifyObject(obj);
System.out.println(obj.value); // 출력 결과: 20
}
public static void modifyObject(MyClass obj) {
obj.value = 20; // 객체의 속성 값을 변경
}
}
그렇기 때문에 객체가 매개 변수로 전달되어도 복사된 참조 값이 넘어오기에 동일한 객체를 수정하는 것과 같으나
함수 호출과 종료시 생성되고 소멸되는 것을
알 수 있습니다.
자바(Java)에서 String 을 생성하는 방식은 총 2가지가 있다.
바로 리터럴("") 생성 방식과 new 연산자 생성 방식이다.
public class Main {
public static void main(String[] args) {
// 리터럴로 생성된 문자열
String str1 = "hello";
// new 키워드를 사용하여 새로운 String 객체 생성
String str2 = new String("hello");
}
}
2가지 방식에서 주목해야 할 부분은 리터럴로 생성된 str1
이다.
예를 들어서 다음과 같은 예시 코드를 살펴보자.
public class Main {
public static void main(String[] args) {
String a = "hello";
String b = "hello";
System.out.println("a 주소: " + System.identityHashCode(a));
// a 주소: 1072591677
System.out.println("b 주소: " + System.identityHashCode(b));
// b 주소: 1072591677
a = a + " world";
System.out.println("a 값: " + a); // a 값: hello world
System.out.println("b 값: " + b); // b 값: hello
}
}
위 예시 코드에서 두 개의 변수가 동일한 문자열 리터럴을 가리키며 동일한 주소값을 가진다.
하지만 새로운 문자열을 생성하여 변수 a에 할당하더라도
b의 값은 변하지않는다.
그 이유가 무엇일까?
바로 리터럴로 생성된 문자열은 불변(immutable) 의 속성을 가지기 때문이다.
즉, 불변하기때문에 동일한 문자열에 대해서 값을 새로 생성하지 않으며 변경되더라도 기존 값이 소멸되거나 사라지지않고 불변성을 유지한다.
그리고 String 타입이 불변성을 유지할 수 있는 이유는
Heap 영역에 속한 String pool이 있기 때문이다.
new 연산자로 생성된 String은 Heap Area 에 배치되지만,
리터럴로 생성된 값은 String pool에 배치되는것을 확인할 수 있다.
public class Main {
public static void main(String[] args) {
// String pool 저장
String str = "hello";
String str2 = "hello";
// Heap Area 저장
String str3 = new String("hello");
}
}
자바에서 리터럴로 생성된 문자열이 불변(immutable)의 속성을 지니는 것은 String pool에 의해 관리되기 때문인 것을 알 수 있습니다.
문자열은 new 로 생성하는 것 보다 리터럴로 생성하는 것이 String pool의 내부 최적화의 이점을 얻을 수 있습니다.
하지만 new 연산자로 생성해도 intern()메서드를 통해
String pool에 저장될 수 있습니다.
public class Main {
public static void main(String[] args) {
String str = "hello";
String str2 = "hello";
// Heap Area -> String pool
String str3 = new String("hello").intern();
System.out.println(str2 == str3); //true
}
}
intern() 메서드의 내부 동작은 다음과 같습니다.
하지만 주의해야 할 점은 intern() 메서드는 String pool 에 값의 유무를 검색하므로 추가적인 로직이 생기며,
동적 문자열등도 String pool 에 input 시킬 수 있기때문에
사용이 권장되지 않습니다.
(번외) String에서 == 이 아닌 equals를 사용하는 이유
String 값 비교할 때에 동일 비교 연산자인 ==
대신에 equals()
를 사용하는 이유는 무엇일까?
어차피 String pool에 저장되면 동일한 주소 값을 가지므로
동일 연산자==
를 사용해도 되는 것 아닐까?
ORM, DB등에서 조회되는 동적 문자열은 String pool에 저장되지 않는다.
그러므로==
보다equals()
를 사용하는 것이 더 적합하고 안전한 방식이다.