들어가기 전에
이번 포스팅은 스프링 입문을 위한 자바 객체 지향의 원리(김종민 저)를 읽으면서 자바 프로그램 개발 및 구동에 대해 정리한 글입니다. 이해한 내용을 토대로 정리한 글이기 때문에, 책으로 읽어보는 것을 권장합니다.
자바 프로그램 구동 방식(Write Once Run Anywhere)
현실 세계 | 가상 세계(자바) |
소프트웨어 개발 도구 | JDK(Java Development Kit, 자바 개발 도구) |
운영체제 | JRE(Java Runtime Environment, 자바 실행 환경) |
하드웨어 (물리적 컴퓨터) | JVM(Java Virtual Machine, 자바 가상 기계) |
- 현실 세계에서 소프트웨어는 개발자가 개발 도구를 이용해 개발하고 운영체제를 통해 물리적 컴퓨터인 하드웨어 상에서 구동됩니다.
- 자바의 가상 세계도 이와 비슷합니다. 자바 개발 도구인 JDK를 이용해 개발된 프로그램은 JRE에 의해 가상의 컴퓨터인 JVM 상에서 구동됩니다.
- 배포되는 JDK는 JRE를 포함하고, JRE는 JVM을 포함하는 형태로 배포됩니다.
- 즉, JDK는 자바 소스 컴파일러인 javac.exe를 포함하며 JRE는 자바 프로그램 실행기인 java.exe를 포함합니다.
객체 지향 프로그램의 메모리 사용 방식
기계어를 포함한 모든 프로그래밍 언어의 공통된 메모리 사용 방식은 아래와 같습니다.
객체 지향 프로그램에서는 데이터 저장 영역을 다시 세 개의 영역으로 분할해서 사용합니다.
소스를 분석할 때 도움이 될 수 있도록 데이터 저장 영역에 대해 각 영역에 어떤 항목이 저장되고 관리되는지 알아보겠습니다.
- 데이터 저장 영역은 T처럼 보이기 때문에 아래에서는 T 메모리 구조라고 지칭하겠습니다.
main() 메소드를 통해 알아보는 T 메모리 구조(1)
main() 메소드는 프로그램이 실행되는 시작점입니다. main() 메소드가 실행될 때 T 메모리에 어떤 일이 벌어지는지 알아보도록 하겠습니다.
public class Start {
public static void main(String[] args) {
System.out.println("Hello OOP!");
}
}
- 먼저 JRE는 프로그램 안에 main() 메소드가 있는지 확인하며, JRE는 main() 메소드를 발견하면 프로그램 실행을 위한 사전 준비에 착수합니다.
- 사전 준비로 JRE는 가상의 기계인 JVM에 전원을 넣어 부팅하며, 부팅된 JVM은 목적 파일을 받아 그 목적 파일을 실행합니다.
- JVM이 제일 먼저 하는 일은 전처리라고 하는 과정입니다.
[ main() 메소드가 실행되기 전 JVM에서 수행하는 전처리 작업들 ]
- 모든 자바 프로그램이 반드시 포함하는 패키지인 java.lang 패키지를 T 메모리의 스태틱 영역에 둡니다.
- 그 후, JVM은 개발자가 작성한 모든 클래스와 임포트 패키지들을 스태틱 영역에 둡니다.
- 실제로는 해당 패키지 또는 클래스가 처음으로 사용될 때 로딩됩니다. 설명의 편의상 모든 클래스라고 작성하였습니다.
- 전처리 과정이 완료되면, main() 메소드를 수행하기 위해 스택 영역에 main() 메소드용 스택 프레임(stack frame)이 할당됩니다.
- 스택 프레임은 여는 중괄호({)를 만날 때마다 하나씩 생깁니다(클래스 정의 시 사용하는 여는 중괄호 제외).
- 이렇게 스택 프레임이 할당되면, 메소드의 파라미터(String[] args)를 저장할 수 있는 변수 공간을 스택 프레임 맨 밑에 확보합니다.
여기까지 진행하면, main() 메소드 안의 첫 명령문을 실행할 수 있게 됩니다.
- 위 코드 예시에서 System.out.println("Hello OOP!");를 실행하게 되면 사실상 T 메모리 공간에는 변화가 없습니다.
- 코드 실행 공간에서 실행되어 GPU에 화면 출력을 의뢰하는 작업이 이루어지기 때문입니다.
- 소스를 실행하다가 닫는 중괄호(})를 만나게 되면, 해당 메소드의 스택 프레임은 소멸합니다.
- main() 메소드는 프로그램의 시작점이기도 하고, 끝을 의미하기도 하기 때문에 main() 메소드가 끝나면 JRE는 JVM을 종료하고 JRE 자체도 운영체제 상의 메모리에서 사라집니다.
main() 메소드를 통해 알아보는 T 메모리 구조(2)
이번에는 main() 메소드 구문에 변수 할당하는 부분을 추가하여 T 메모리 구조를 알아보도록 하겠습니다.
public class Start2 {
public static void main(String[] args) {
int i;
i = 10;
}
}
- 먼저, JRE가 JVM을 구동시키고 JVM이 전처리 하는 과정 및 전처리 이후 main() 메소드를 수행하기 위해 스택 영역에 main() 메소드용 스택 프레임 및 파라미터를 위한 변수 공간 할당하는 것까지는 동일합니다.
- 코드의 3번째 줄에 존재하는 int i; 명령을 수행하는 부분부터 이전 과정과 달라집니다. 먼저 이 명령어는 4바이트짜리 정수 저장 공간을 main() 스택 프레임안에 만들어라라고 해석할 수 있습니다.
- 스택 프레임 안에 변수 공간은 아래에서부터 차곡차곡 만들어지며, 처음에 어떤 값을 줄 지 모르기 때문에 쓰레기값(?)을 가지게 됩니다.
- 이때 생성된 변수는 스택 프레임 안에서 사용되는 변수로 지역 변수라고 합니다.
- 그 다음 라인에서 i = 10; 명령어를 수행함으로써 i라는 변수 공간을 10으로 초기화하게 됩니다.
- 이후 과정은 앞선 예시와 동일하게 닫는 중괄호(})를 만나 스택 프레임 제거 및 프로그램 종료가 진행된다고 볼 수 있습니다.
main() 메소드를 통해 알아보는 T 메모리 구조(3)
이번에는 한 단계 더 나아가, main() 메소드 내에 중괄호({})가 추가로 있는 블록 구문이 존재할 때의 T 메모리 구조에 대해 알아보겠습니다.
public class Start3 {
public static void main(String[] args) {
int i =10;
int k = 20;
if(i == 10) {
int m = k + 5;
k = m;
} else {
int p = k + 10;
k = p;
}
// k = m + p; // 사용 불가(m은 if(true) 스택 프레임 내 변수)
}
}
- 먼저, 위 코드의 4번째 줄이 완료되었을 때의 T 메모리 구조는 아래와 같습니다.
- 6번째 줄을 보면 if문에 여는 중괄호({)가 있기 때문에 새로운 스택 프레임이 시작됩니다.
- 이때 시작되는 스택 프레임은 i == 10이 참이므로 if(true) 스택 프레임이 생성된다고 볼 수 있으며, main() 메소드 내에서 생성된 것이기 때문에 main() 스택 프레임 내 if(true) 스택 프레임이 생성됩니다.
- if문이 true였기 때문에, else 구문은 스택 메모리에 등작해 볼 기회조차 갖지 못합니다.
- if(true) 절의 명령어를 하나하나 뜯어보면, 먼저 int m = k + 5;라는 명령어를 마주하게 됩니다. 이 명령어는 두 가지 명령어를 합친 것으로 m이라는 이름의 4바이트 정수 변수를 선언(1)하고, 해당 변수에 k + 5라는 값을 넣어 초기화(2)해주는 것입니다.
- 이 다음 명령어로 k = m;을 실행해서 if(true) 스택 프레임 밖에 있는 main() 스택 프레임 내 변수인 k의 값을 변경해주며, 이후 과정은 이전 예시와 동일합니다.
위 예시에서 알 수 있는 점은 아래와 같습니다.
외부 스택 프레임에서 내부 스택 프레임의 변수에 접근하는 것은 불가능하나 그 역은 가능합니다.
main() 메소드를 통해 알아보는 T 메모리 구조(4)
앞선 메소드들은 모두 반환형이 void로 반환값이 존재하지 않았습니다. 이번 예시에서는 반환값이 존재하는 메소드가 스택 프레임에 생성될 때 어떻게 생성되는지 알아보도록 하겠습니다.
public class Start4 {
public static void main(String[] args) {
int k = 5;
int m;
m = square(k);
}
public static int square(int k) {
int result;
k = 25;
result = k;
return result;
}
}
- 먼저 위 코드를 실행시키면, JRE에 의해 JVM이 기동되고 JVM은 전처리 작업을 통해 java.lang 패키지 및 Start4 관련 패키지를 T 메모리의 스태틱 영역에 올리게 됩니다.
- 이후, main() 메소드를 수행하기 위해 main() 메소드용 스택 프레임을 스택 영역에 생성하게 되고, 4번째 줄까지 실행함으로써 main() 스택 프레임 내 k 변수를 초기화하고 m 변수 공간을 생성합니다.
- 6번째 줄에서는 m = square(k); 명령어를 실행하여 square() 메소드를 스택 영역에 올리게 됩니다.
- square() 메소드의 경우 파라미터도 존재하고, 반환값도 존재합니다.
- 이 때, 반환값을 저장할 변수 공간 맨 아래에 두고 파라미터를 저장할 변수 공간을 둔 뒤 마지막으로 메소드의 지역 변수용 공간이 생깁니다.
- square() 메소드를 스택 영역에 올린 후 12번째 줄을 실행하면 square() 스택 프레임 속 k 변수(파라미터)의 값이 25로 변경됩니다. 이 때, main() 스택 프레임의 k 변수는 값의 변화가 없음을 알 수 있습니다.
- 이는, 이전에 main() 메소드 내에서 if 블록을 실행했을 때와 달리 square()와 main()의 스택 프레임이 각각 다른 영역에 생겨 각 스택 프레임 내 k 변수가 별도의 변수 공간이기 때문입니다.
- 13~14번째 줄을 통해 square() 스택 프레임의 k 값이 result 변수에 저장되어 반환되어 main() 스택 프레임의 변수 m이 초기화 됩니다.
- 이후 과정은 이전 예제와 동일하므로 생략하겠습니다.
T 메모리 구조를 활용해 멀티 쓰레드와 멀티 프로세서 이해하기
멀티 쓰레드(Multi Thread)란, T 메모리 모델의 스택 영역을 쓰레드 개수만큼 분할해서 사용하는 것입니다.
- 하나의 T 메모리만 사용(스태틱 영역과 힙 영역은 공유함)하면서, 스택 영역만 분할해서 사용합니다.
멀티 프로세스(Multi Process)란, 다수의 데이터 저장 영역을 갖는 것으로 다수의 T 메모리를 갖는 구조를 뜻합니다.
알면 좋은 내용
왜 프로그램이 실행되는 시작점인 main() 메소드는 static으로 선언되어 있을까?
기본적으로 처음 프로그램이 실행되면, 앞서 알아봤던 것처럼 스태틱 영역에 클래스들이 모두 올라갑니다. 이때, 클래스 메소드(= 스태틱 메소드 = 정적 메소드)가 아닌 객체 메소드라면 객체가 힙 영역에 생성된 이후에 사용이 가능하기 때문입니다.
- 만약, main() 메소드를 static으로 선언하지 않으면 객체 메소드가 되어 main() 메소드가 존재하는 클래스의 객체가 생성된 후 실행이 가능합니다.
변수는 T 메모리 영역 중 어느 곳에 있을 수 있을까?
변수의 종류에 따라 다른 영역에 생성됩니다.
- 지역변수: 스택 영역의 스택 프레임 안에서 일생을 보내며, 스택 프레임이 사라지면 함께 사라집니다.
- 클래스 멤버 변수: 스태틱 영역에서 일생을 보내며, JVM이 종료될 때까지 고정된(static) 상태로 존재합니다.
- 객체 멤버 변수: 힙 영역에서 일생을 보내며, 객체와 함께 가비지 컬렉터(GC)에 의해 힙 메모리 회수기에 의해 일생을 마치게 됩니다.
예제 4의 main() 메소드에서 square() 메소드 내 지역변수를 접근할 수 있을까?
메소드를 블랙박스화합니다.
main() 메소드의 어딘가에서 square() 메소드 속에 존재하는 지역 변수에 직접 접근은 불가능합니다. 메소드를 블랙박스화한다는 말은 입력 값들과 반환 값에 의해서만 메소드 사이에서 값이 전달될 수 있으며 서로 내부의 지역 변수는 볼 수 없음을 의미합니다.
전역 변수는 왜 자제하라고 할까?
전역 변수는 아래 클래스에서 볼 수 있듯이 static 키워드가 붙은 변수를 의미하며, 클래스 변수 또는 정적 변수라고도 부릅니다.
public class Start5 {
static int share;
public static void main(String[] args) {
share = 55;
int k = fun(5, 7);
System.out.println(share);
}
public static int fun(int m, int p) {
share = m + p;
return m - p;
}
}
- 위 예시 코드를 실행해보면, 먼저 JVM의 전처리 과정에 의해 아래와 같이 스태틱 영역이 구성됩니다.
- Start5 클래스의 변수인 share 앞에는 static이 붙어있어 T 메모리 영역 중 스태틱 영역에 공간이 할당됩니다.
- 5번째 줄에 의해 share에 55가 할당되어 main() 메소드는 8번째 줄에서 55를 출력하길 원했으나, fun() 메소드에 의해 share 값이 변경되어 8번째 줄에서 55가 아닌 12가 출력됨을 확인할 수 있습니다.
전역 변수의 경우, 코드 어느 곳에서나 접근할 수 있기 때문에 프로젝트 규모가 커짐에 따라 전역 변수를 변경할 수 있는 영역이 늘어나면 전역 변수에 어떤 값이 들어가있는지 파악하기 어렵습니다. 따라서, 전역 변수는 최대한 사용하지 않는 것이 좋습니다.
특히, 멀티 쓰레드 환경에서 쓰기 가능한 전역 변수를 사용하면 A 쓰레드에서 전역 변수 A에 20을 넣고 사용하는데, B 쓰레드에서 전역 변수 B에 10을 넣어버릴 수 있어 쓰레드 안정성(Thread-Safe)이 깨질 수 있습니다.
프로그램이 시작될 때 모든 패키지와 모든 클래스가 T 메모리의 스태틱 영역에 로딩될까?
자바 프로그램 구동 시 JVM에서 전처리 작업 수행 시, java.lang 패키지를 스태틱 영역에 올린 후 개발자가 만든 클래스 및 임포트된 패키지들을 모두 스태틱 영역에 올린다고 하였습니다. 사실, 이는 설명을 쉽게 하기 위함이었고 실제로는 해당 패키지 또는 클래스가 처음으로 사용될 때 로딩됩니다.
아래 예시를 통해 실제로 클래스가 처음으로 로딩될 때 스태틱 영역에 로딩됨을 확인해보겠습니다.
package staticBlock;
public class Driver03 {
public static void main(String[] args) {
System.out.println("main 메소드 시작");
동물 뽀로로 = new 동물();
동물 피카츄 = new 동물();
}
}
public class Animal {
static { // 클래스 생성 시의 실행 블록(static 블록)
System.out.println("동물 클래스 스태틱 영역에 배치 완료");
}
}
- 위 사진 중 왼쪽 예시를 보면, Driver04 클래스가 스태틱 영역에 먼저 로딩된 후, main() 메소드가 실행됨에 따라 동물 클래스가 스태틱 영역에 로딩되어 동물 클래스의 static 블록이 실행됨을 확인할 수 있습니다.
- 반면 오른쪽 예시를 보면, Driver04 클래스가 스태틱 영역에 로딩된 후 main() 메소드가 실행될 때 동물 클래스가 사용되지 않아 프로그램 종료시까지 동물 클래스가 스태틱 영역에 로딩되지 않음을 확인할 수 있습니다.
해당 패키지 또는 클래스가 처음 사용될 때 스태틱 영역에 로딩됩니다.
참고 자료
'PROGRAMMING LANGUAGE > JAVA' 카테고리의 다른 글
[JAVA] 자바가 확장한 객체 지향 (0) | 2022.06.04 |
---|---|
[JAVA] 객체 지향 4대 특성(캡! 상추다: 캡슐화/상속/추상화/다형성) (1) | 2022.06.03 |
[JAVA 개념] ArrayList 초기화 (0) | 2022.02.20 |
[JAVA 개념] 길이 관련 메소드 사용법(length, length(), size()) (0) | 2022.02.20 |
[JAVA 1.8↑] LocalDateTime, LocalDate, LocalTime (0) | 2020.12.11 |