오늘의 잔디
연휴동안 본가 대전에서 일주일 넘게 실컷 쉬다 왔다.
코딩 머리는 좀 굳었지만
다시 매일매일 하면서 말랑말랑하게 만들어보자자자자자.
오늘의 공부


자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역 3개로 나눌 수 있다.
new 명령어를 사용하면 이 영역을 사용한다.방금 설명한 내용은 쉽게 비유로 한 것이고 실제는 다음과 같다.

static 영역: static 변수들을 보관한다. 뒤에서 자세히 설명한다."hello" 라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리한다. 이 외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리한다. (참고로 문자열을 다루는 문자열 풀은 자바 7부터 힙 영역으로 이동했다.)참고: 스택 영역은 더 정확히는 각 쓰레드별로 하나의 실행 스택이 생성된다. 따라서 쓰레드 수 만큼 스택 영역이 생성된다. 지금은 쓰레드를 1개만 사용하므로 스택 영역도 하나이다. 쓰레드에 대한 부분은 멀티 쓰레드를 학습해야 이해할 수 있다.

자바에서 특정 클래스로 100개의 인스턴스를 생성하면, 힙 메모리에 100개의 인스턴스가 생긴다. 각각의 인스턴스는
내부에 변수와 메서드를 가진다. 같은 클래스로 부터 생성된 객체라도, 인스턴스 내부의 변수 값은 서로 다를 수 있지만, 메서드는 공통된 코드를 공유한다. 따라서 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없다. 메서드는 메서드 영역에서 공통으로 관리되고 실행된다.
정리하면 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행한다.
자바 메모리 구조 중 스택 영역에 대해 알아보기 전에 먼저 스택(Stack)이라는 자료 구조에 대해서 알아보자.
다음과 같은 1, 2, 3 이름표가 붙은 블럭이 있다고 가정하자.

이 블럭을 다음과 같이 생긴 통에 넣는다고 생각해보자. 위쪽만 열려있기 때문에 위쪽으로 블럭을 넣고, 위쪽으로 블럭을 빼야 한다. 쉽게 이야기해서 넣는 곳과 빼는 곳이 같다.

블럭을 빼려면 위에서 부터 순서대로 빼야한다.
블럭은 3 -> 2 -> 1 순서로 뺄 수 있다.
정리하면 다음과 같다.
1(넣기) -> 2(넣기) -> 3(넣기) -> 3(빼기) -> 2(빼기) -> 1(빼기)
후입 선출(LIFO, Last In First Out)
여기서 가장 마지막에 넣은 3번이 가장 먼저 나온다. 이렇게 나중에 넣은 것이 가장 먼저 나오는 것을 후입 선출이라 하고, 이런 자료 구조를 스택이라 한다.
선입 선출(FIFO, First In First Out)
후입 선출과 반대로 가장 먼저 넣은 것이 가장 먼저 나오는 것을 선입 선출이라 한다. 이런 자료 구조를 큐(Queue)라한다.

정리하면 다음과 같다.
1(넣기) -> 2(넣기) -> 3(넣기) -> 1(빼기) -> 2(빼기) -> 3(빼기)
이런 자료 구조는 각자 필요한 영역이 있다. 예를 들어서 선착순 이벤트를 하는데 고객이 대기해야 한다면 큐 자료 구조를 사용해야 한다.
이번시간에 중요한 것은 스택이다. 프로그램 실행과 메서드 호출에는 스택 구조가 적합하다. 스택 구조를 학습했으니,
자바에서 스택 영역이 어떤 방식으로 작동하는지 알아보자.
다음 코드를 실행하면 스택 영역에서 어떤 변화가 있는지 확인해보자.
JavaMemoryMain1
package memory;public class JavaMemoryMain1 {
public static void main(String[] args) {
System.out.println("main start");
method1(10);
System.out.println("main end");
}
static void method1(int m1) {
System.out.println("method1 start");
int cal = m1 * 2;
method2(cal);
System.out.println("method1 end");
}
static void method2(int m2) {
System.out.println("method2 start");
System.out.println("method2 end");
}
}
실행 결과
main start
method1 start
method2 start
method2 end
method1 end
main end
호출 그림

main() 을 실행한다. 이때 main() 을 위한 스택 프레임이 하나 생성된다.main() 스택 프레임은 내부에 args 라는 매개변수를 가진다. args 는 뒤에서 다룬다.main() 은 method1() 을 호출한다. method1() 스택 프레임이 생성된다.method1() 는 m1 , cal 지역 변수(매개변수 포함)를 가지므로 해당 지역 변수들이 스택 프레임에 포함된다.method1() 은 method2() 를 호출한다. method2() 스택 프레임이 생성된다. method2() 는 m2 지역 변수(매개변수 포함)를 가지므로 해당 지역 변수가 스택 프레임에 포함된다.
method2() 가 종료된다. 이때 method2() 스택 프레임이 제거되고, 매개변수 m2 도 제거된다. method2() 스택 프레임이 제거 되었으므로 프로그램은 method1() 로 돌아간다. 물론 method1() 을 처음부터 시작하는 것이 아니라 method1() 에서 method2() 를 호출한 지점으로 돌아간다.method1() 이 종료된다. 이때 method1() 스택 프레임이 제거되고, 지역 변수(매개변수 포함) m1 , cal 도 제거된다. 프로그램은 main() 으로 돌아간다.main() 이 종료된다. 더 이상 호출할 메서드가 없고, 스택 프레임도 완전히 비워졌다. 자바는 프로그램을 정리하고 종료한다.정리
이번에는 스택 영역과 힙 영역이 함께 사용되는 경우를 알아보자.
Data
package memory;
public class Data {
private int value;
public Data(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
JavaMemoryMain2
package memory;
public class JavaMemoryMain2 {
public static void main(String[] args) { System.out.println("main start");
method1();
System.out.println("main end");
}
static void method1() {
System.out.println("method1 start");
Data data1 = new Data(10);
method2(data1);
System.out.println("method1 end");
}
static void method2(Data data2) {
System.out.println("method2 start");
System.out.println("data.value=" + data2.getValue());
System.out.println("method2 end");
}
}
main() -> method1() -> method2() 순서로 호출하는 단순한 코드이다.method1() 에서 Data 클래스의 인스턴스를 생성한다.method1() 에서 method2() 를 호출할 때 매개변수에 Data 인스턴스의 참조값을 전달한다.실행 결과
main start
method1 start
method2 start
data.value=10
method2 end
method1 end
main end
그림을 통해 순서대로 알아보자.

main() 메서드를 실행한다. main() 스택 프레임이 생성된다
main() 에서 method1() 을 실행한다. method1() 스택 프레임이 생성된다.method1() 은 지역 변수로 Data data1 을 가지고 있다. 이 지역 변수도 스택 프레임에 포함된다.method1() 은 new Data(10) 를 사용해서 힙 영역에 Data 인스턴스를 생성한다. 그리고 참조값을 data1에 보관한다.
method1() 은 method2() 를 호출하면서 Data data2 매개변수에 x001 참조값을 넘긴다.method1() 에 있는 data1 과 method2() 에 있는 data2 지역 변수(매개변수 포함)는 둘다 같은 x001 인스턴스를 참조한다.
method2() 가 종료된다. method2() 의 스택 프레임이 제거되면서 매개변수 data2 도 함께 제거된다.
method1() 이 종료된다. method1() 의 스택 프레임이 제거되면서 지역 변수 data1 도 함께 제거된다.
method1() 이 종료된 직후의 상태를 보자. method1() 의 스택 프레임이 제거되고 지역 변수 data1 도 함께 제거되었다.x001 참조값을 가진 Data 인스턴스를 참조하는 곳이 더는 없다.참고: 힙 영역 외부가 아닌, 힙 영역 안에서만 인스턴스끼리 서로 참조하는 경우에도 GC의 대상이 된다.
정리
지역 변수는 스택 영역에, 객체(인스턴스)는 힙 영역에 관리되는 것을 확인했다. 이제 나머지 하나가 남았다. 바로 메서드 영역이다. 메서드 영역이 관리하는 변수도 있다. 이것을 이해하기 위해서는 먼저 static 키워드를 알아야 한다. static 키워드는 메서드 영역과 밀접한 연관이 있다.
이번에는 새로운 키워드인 static 키워드에 대해 학습해보자.
static 키워드는 주로 멤버 변수와 메서드에 사용된다.
먼저 멤버 변수에 static 키워드가 왜 필요한지 이해하기 위해 간단한 예제를 만들어보자.
특정 클래스를 통해서 생성된 객체의 수를 세는 단순한 프로그램이다.
먼저 생성할 인스턴스 내부에 카운트를 저장하겠다.
Data1
package static1;
public class Data1 {
public String name;
public int count;
public Data1(String name) {
this.name = name;
count++;
}
}
생성된 객체의 수를 세어야 한다. 따라서 객체가 생성될 때 마다 생성자를 통해 인스턴스의 멤버 변수인 count 값을 증가시킨다.
참고로 예제를 단순하게 만들기 위해 필드에 public 을 사용했다.
DataCountMain1
public class DataCountMain1 {
public static void main(String[] args) {
Data1 data1 = new Data1("A");
System.out.println("A count=" + data1.count);
Data1 data2 = new Data1("B");
System.out.println("B count=" + data2.count);
Data1 data3 = new Data1("C");
System.out.println("C count=" + data3.count);
}
}
객체를 생성하고 카운트 값을 출력한다.
실행 결과
A count=1
B count=1
C count=1
이 프로그램은 당연히 기대한 대로 작동하지 않는다. 객체를 생성할 때 마다 Data1 인스턴스는 새로 만들어진다. 그리고 인스턴스에 포함된 count 변수도 새로 만들어지기 때문이다.

처음 Data1("A") 인스턴스를 생성하면 count 값은 0 으로 초기화 된다. 생성자에서 count++ 을 호출했으므로 count 의 값은 1 이 된다.

다음으로 Data1("B") 인스턴스를 생성하면 완전 새로운 인스턴스를 생성한다. 이 새로운 인스턴스의 count 값은 0 으로 초기화 된다. 생성자에서 count++ 을 호출했으므로 count 의 값은 1 이 된다.

다음으로 Data1("C") 인스턴스를 생성하면 이전 인스턴스는 관계없는 새로운 인스턴스를 생성한다. 이 새로운 인스턴스의 count 값은 0 으로 초기화 된다. 생성자에서 count++ 을 호출했으므로 count 의 값은 1 이 된다.
인스턴스에 사용되는 멤버 변수 count 값은 인스턴스끼리 서로 공유되지 않는다. 따라서 원하는 답을 구할 수 없다. 이 문제를 해결하려면 변수를 서로 공유해야 한다.
이번에는 카운트 값을 저장하는 별도의 객체를 만들어보자.
Counter
package static1;
public class Counter {
public int count;
}
Data2
package static1;
public class Data2 {
public String name;
public Data2(String name, Counter counter) {
this.name = name;
counter.count++;
}
}
Data2 클래스를 만들었다. 여기에는 count 멤버 변수가 없다. 대신에 생성자에서 Counter 인스턴스를 추가로 전달 받는다.counter 인스턴스에 있는 count 변수의 값을 하나 증가시킨다.DataCountMain2
package static1;
public class DataCountMain2 {
public static void main(String[] args) {
Counter counter = new Counter();
Data2 data1 = new Data2("A", counter);
System.out.println("A count=" + counter.count);
Data2 data2 = new Data2("B", counter);
System.out.println("B count=" + counter.count);
Data2 data3 = new Data2("C", counter);
System.out.println("C count=" + counter.count);
}
}
실행 결과
A count=1
B count=2
C count=3
Counter 인스턴스를 공용으로 사용한 덕분에 객체를 생성할 때 마다 값을 정확하게 증가시킬 수 있다.

Data2("A") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킨다.
count 값은 1이 된다.

Data2("B") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킨다.
count 값은 2가 된다.

Data2("C") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킨다.
count 값은 3이 된다.

결과적으로 Data2 의 인스턴스가 3개 생성되고, count 값도 인스턴스 숫자와 같은 3으로 정확하게 측정된다.
그런데 여기에는 약간 불편한 점들이 있다.
Data2 클래스와 관련된 일인데, Counter 라는 별도의 클래스를 추가로 사용해야 한다.특정 클래스에서 공용으로 함께 사용할 수 있는 변수를 만들 수 있다면 편리할 것이다.
static 키워드를 사용하면 공용으로 함께 사용하는 변수를 만들 수 있다.
Data3
package static1;public class Data3 {
public String name;
public static int count; //static
public Data3(String name) {
this.name = name;
count++;
}
}
Data3 을 만들었다.static int count 부분을 보자. 변수 타입( int ) 앞에 static 키워드가 붙어있다.static 을 붙이게 되면 static 변수, 정적 변수 또는 클래스 변수라 한다.count 의 값을 하나 증가시킨다.DataCountMain3
package static1;
public class DataCountMain3 {
public static void main(String[] args) {
Data3 data1 = new Data3("A");
System.out.println("A count=" + Data3.count);
Data3 data2 = new Data3("B");
System.out.println("B count=" + Data3.count);
Data3 data3 = new Data3("C");
System.out.println("C count=" + Data3.count);
}
}
코드를 보면 count 정적 변수에 접근하는 방법이 조금 특이한데 Data3.count 와 같이 클래스명에 .(dot)을 사용한다. 마치 클래스에 직접 접근하는 것 처럼 느껴진다.
실행 결과
A count=1
B count=2
C count=3

static 이 붙은 멤버 변수는 메서드 영역에서 관리한다.static 이 붙은 멤버 변수 count 는 인스턴스 영역에 생성되지 않는다. 대신에 메서드 영역에서 이 변수Data3("A") 인스턴스를 생성하면 생성자가 호출된다count++ 코드가 있다. count 는 static 이 붙은 정적 변수다. 정적 변수는 인스턴스 영역이 아니라 메서드 영역에서 관리한다. 따라서 이 경우 메서드 영역에 있는 count 의 값이 하나 증가된다.
Data3("B") 인스턴스를 생성하면 생성자가 호출된다count++ 코드가 있다. count 는 static 이 붙은 정적 변수다. 메서드 영역에 있는 count 변수의 값이 하나 증가된다.
Data3("C") 인스턴스를 생성하면 생성자가 호출된다count++ 코드가 있다. count 는 static 이 붙은 정적 변수다. 메서드 영역에 있는 count 변수의 값이 하나 증가된다.
count 변수의 값은 3이 된다.static 이 붙은 정적 변수에 접근하려면 Data3.count 와 같이 클래스명 + . (dot) + 변수명으로 접근하면 된다.Data3 의 생성자와 같이 자신의 클래스에 있는 정적 변수라면 클래스명을 생략할 수 있다.static 변수를 사용한 덕분에 공용 변수를 사용해서 편리하게 문제를 해결할 수 있었다.
정리
static 변수는 쉽게 이야기해서 클래스인 붕어빵 틀이 특별히 관리하는 변수이다. 붕어빵 틀은 1개이므로 클래스 변수도 하나만 존재한다. 반면에 인스턴스 변수는 붕어빵인 인스턴스의 수 만큼 존재한다.