자바 프로그램의 개발과 구동
자바의 가상 세계는 현실 세계를 그대로 모방하고 있다.
현실 세계에서 소프트웨어, 즉 프로그램은 개발자가 개발 도구를 이용해 개발하고 운영체제를 통해 하드웨어 상에서 구동된다.
자바 개발 도구인 JDK를 이용해 개발된 프로그램은 JRE에 의해 가상의 컴퓨터인 JVM 상에서 구동된다.
JDK: Java Development Kit / 자바 개발 도구
JRE: Java Runtime Environment / 자바 실행 환경
JVM: Java Virtual Machine / 자바 가상 기계
위 그림에서 보면 JDK는 자바 소스 컴파일러인 javac.exe를 포함하고 JRE는 자바 프로그램 실행기인 java.exe를 포함하고 있다.
자바가 이런 구조를 택한 이유는 한 개의 소스 파일로 각 플랫폼에서 프로그램을 구동하는 데 아무 문제가 없게끔 만들어주기 위해서이다.
자바에 존재하는 절차적/구조적 프로그래밍의 유산
절차적 프로그래밍을 한 마디로 표현하자면 goto를 쓰지 말라는 것이다.
goto를 사용하게 되면 프로그램의 실행 순서를 이리저리 이동할 수 있게 되는데, 이동이 잦아지면 소스를 따라가면서 프로그램을 이해하기 쉽지 않기 때문이다.
구조적 프로그래밍은 함수를 쓰라는 것이다.
함수를 쓰면 좋은 이유는 중복 코드를 한 곳에서 관리할 수 있고, 논리를 함수 단위로 분리해서 이해하기 쉬운 코드를 작성할 수 있기 때문이다.
그럼 자바에서 이러한 절차적/구조적 프로그래밍의 유산은 어디에 남아 있을까?
답은 메서드 안에 있다.
여기서 한 가지, 함수(Function)와 메서드(Method)는 무엇이 다를까?
전혀 다르지 않다. 절차적/구조적 프로그래밍에서 함수라 불렀는데 객체 지향에서는 다르게 불러야 하지 않을까? 그래서 메서드라고 불렀다고 한다.
굳이 차이점을 찾는다면 함수는 클래스나 객체와 아무 관계가 없지만 메서드는 반드시 클래스 정의 안에 존재해야 한다는 것이다.
main() 메서드: 메서드 스택 프레임
public class Start {
public static void main(String[] args) {
System.out.println("Hello OOP!!!");
}
}
위 코드에서 main() 메서드가 실행될 때, 메모리 특히 T 메모리에는 어떤 일이 일어날까?
쉽게 T 메모리 구조라고 부른다.
- JRE는 프로그램 안에 main() 메서드가 있는지 확인한다.
- main() 메서드의 존재가 확인되면 JVM은 목적 파일을 받아 전처리 작업들을 수행한다.
- java.lang 패키지를 T 메모리의 스태틱 영역에 배치한다.
- import된 패키지를 T 메모리의 스태틱 영역에 배치한다.
- 프로그램 상의 모든 클래스를 T 메모리의 스태틱 영역에 배치한다.
이 과정이 끝나고 T 메모리 영역의 상태이다.
여기서 main() 메서드의 첫 줄인 System.out.println("Hello OOP!!!"); 가 실행될까?
아직 아니다. main() 메서드가 놀기 위해 스택 프레임이 스택 영역에 할당된다.
더 정확히 말하자면 클래스 정의를 시작하는 여는 중괄호를 제외하고 여는 중괄호를 만날 때마다 스택 프레임이 하나씩 생긴다.
그리고 메서드의 인자 args를 저장할 변수 공간을 스택 프레임의 맨 밑에 확보해야 한다.
즉, 메서드 인자의 변수 공간을 할당하는 것이다.
이렇게 T 메모리를 구성하고 나서야 main() 메서드 안의 첫 명령문을 실행하게 된다.
System.out.println("Hello OOP!!!"); 구문이 실행되면 T 메모리는 어떻게 될까?
T 메모리에는 변화가 없다. GPU에 화면 출력을 요청할 뿐이다.
4번째 줄에서 main() 메서드의 끝을 나타내는 닫는 중괄호를 만나면 스택 프레임이 소멸된다.
main() 메서드가 끝나면 JRE는 JVM을 종료하고 JRE 자체도 운영체제 상의 메모리에서 사라진다.
변수와 메모리: 변수! 너 어디 있니?
public class Start2 {
public static void main(String[] args) {
int i;
i = 10;
double d = 20.0;
}
}
이 코드에서 3번째 줄을 실행한 후 T 메모리 영역은 어떨까?
main() 메서드 내에 있으니까 당연히 main() 메서드 스택 프레임 안에 밑에서부터 차곡차곡 변수 공간을 마련한다.
현재 변수 i에 저장된 값은 ?로 되어있다.
알 수 없는 값이 들어가 있다는 말이다. 이전에 해당 공간의 메모리를 사용했던 다른 프로그램이 청소하지 않고 간 값을 그대로 가지고 있게 된다.
그래서 변수를 선언만 하고 초기화하지 않은 상태에서 변수를 사용하는 코드를 만나면 javac는 “The local variable i may not have been initialized” 경고를 출력한다.
블록 구문과 메모리: 블록 스택 프레임
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;
}
}
if 블록 안의 int m = k + 5; 를 실행한 이후 T 메모리 영역의 모습이다.
이 때 if 블록 중 참일 때의 블록을 종료하는 닫는 중괄호를 만나면 if 블록 스택 프레임은 스택 영역에서 사라진다.
이렇게 if 블록 안에서 선언된 변수는 블록의 소멸과 함께 사라지기 때문에 14번째 줄 k = m + p 의 주석을 풀면 컴파일 오류가 발생한다.
지역 변수와 메모리: 스택 프레임에 갇혔어요!
T 메모리는 세 개의 영역이 있는데 변수는 어디에 있는 걸까?
정답은 ‘세 군데에 모두’다.
세 군데 각각에 있는 변수는 각기 다른 목적을 가지며 지역 변수, 클래스 멤버 변수, 객체 멤버 변수로 이름도 다르다.
지역 변수는 스택 영역의 스택 프레임 안에 위치한다. 따라서 스택 프레임의 소멸과 함께 사라진다.
클래스 멤버 변수는 스태틱 영역에 위치한다. JVM이 종료될 때까지 남아있는다.
객체 멤버 변수는 힙에 존재하다가 가비지 컬렉터에 의해 사라진다.
💡 외부 스택 프레임에서 내부 스택 프레임의 변수에 접근하는 것은 불가능하나 그 역은 가능하다.
메서드 호출과 메모리: 메서드 스택 프레임 2
public class Start4 {
public static void main(String[] args) {
int k = 5;
int m;
m = square(k);
}
private static int square(int k) {
int result;
k = 25;
result = k;
return result;
}
}
main() 메서드의 어디에선가 square() 메서드 내의 지역 변수 result에 직접 접근할 수 있을까?
또는 반대로 square() 메서드의 어디에선가 main() 메서드의 지역 변수 m에 직접 접근할 수 있을까?
절대 접근할 수 없다.
6번째 줄 m = square(k); 이 실행되고 있는 동안, 즉 square() 메서드 내의 실행 명령문에서는 main() 메서드의 지역 변수를 참조할 수 있을 것 같지만 그건 자바 스펙을 만드신 분들이 금지시켜 뒀다.
왜 금지했는지 짐작하자면 아래와 같다.
- 그것이 이치에 맞기 때문이다. 메서드는 서로의 고유 공간이다.
- 포인터 문제 때문이다. square() 메서드에서 main() 메서드 내부의 지역변수 m에 접근하기 위해서는 m 변수의 메모리 주소 값을 알아야 한다.
- 실전에서 사용되는 메서드는 다양한 곳으로부터 호출되며 호출하는 메서드 내부의 지역 변수를 호출 당하는 쪽에서 제어할 수 있게 코드를 만들려면 결국 포인터를 주고 받아야 한다.
전역 변수와 메모리: 전역 변수 쓰지 말라니까요!
두 메서드 사이에 값을 전달하는 방법은 메서드를 호출할 때 메서드의 인자를 이용하는 방법과 메서드를 종료할 때 반환값을 넘겨주는 방법이 있다.
그런데 이 두 가지 말고 한 가지 방법이 더 있다.
전역 변수를 사용하는 것이다.
전역 변수는 코드 어느 곳에서나 접근할 수 있다고 해서 전역 변수라고 하며, 여러 메서드들이 공유해서 사용한다고 해서 공유 변수라고도 한다.
그런데 왜 전역 변수를 쓰지 말라고들 하는걸까?
프로젝트 규모에 따라 코드가 커지면서 여러 메서드에서 전역 변수의 값을 변경하기 시작하면 값을 파악하기 쉽지 않기 때문이다.
전역 변수는 피할 수 있다면 즐기지 말고 피해야 할 존재다.
다만 읽기 전용으로 값을 공유해서 전역 상수로 쓰는 것은 적극 추천한다.
멀티 스레드 / 멀티 프로세스의 이해
멀티 스레드의 T 메모리 모델은 스택 영역을 스레드 개수만큼 분할해서 쓰는 것이다.
멀티 프로세스는 다수의 T 메모리를 갖는 구조이다.
멀티 프로세스는 하나의 프로세스가 다른 프로세스의 T 메모리 영역을 절대 침범할 수 없는 안전한 구조이지만 메모리 사용량은 그만큼 크다.
멀티 스레드는 하나의 T 메모리 안에서 스택 영역만 분할한 것이기 때문에 하나의 스레드에서 다른 스레드의 스택 영역에는 접근할 수 없지만 스태틱 영역과 힙 영역은 공유해서 사용하는 구조이기 때문에 멀티 프로세스 대비 메모리를 적게 사용한다.
멀티 스레드에서 전역 변수 사용의 문제점
두 개의 스레드로 구성된 프로그램이 있다고 해보자. 스레드 1이 공유 영역에 있는 전역 변수 A에 10을 할당한 후 CPU 사용권이 스레드 2로 넘어가고 스레드 2에서 A에 20을 할당했다.
다시 CPU 사용권이 스레드 1로 넘어가서 A의 값을 출력하면 어떻게 될까?
스레드1 | 스레드2 | |
전역 변수 A에 10 할당 | A = 10 | |
전역 변수 A에 20 할당 | A = 20 | |
전역 변수 A의 값 출력 | 20이 출력된다. |
스레드 1의 입장에서는 갑자기 20이라는 값이 출력되는 문제가 발생한다.
이처럼 쓰기 가능한 전역 변수를 사용하게 되면 스레드 안전성이 깨진다고 표현한다.
이를 보완하는 방법으로 락(lock)을 거는 방법이 있지만 락을 거는 순간 멀티 스레드의 장점은 버린 것과 같다.
정리 - 객체 지향은 절차적/구조적 프로그래밍의 어깨를 딛고
- 객체 지향 프로그래밍은 절차적/구조적 프로그래밍의 유산을 간직하고 있다.
연산자, 제어문, 메모리 관리 체계 등등 많은 부분을 차용하고 있는 것이다.
그래서 프로그래머는 절차적/구조적 프로그래밍 기법도 잘 알고 있어야 한다. - 메서드 작성에 대한 지혜를 구조적 프로그래밍에서 배워와야 한다.
메서드를 만들 때는 순서도 또는 의사 코드를 작성하는 것이 좋다.
UML 액티비티 다이어그램을 그리는 것도 좋긴 하지만 메서드의 로직을 표현할 때는 순서도가 더 직관적인 것 같다.
'Reading > 스프링 입문을 위한 자바 객체지향의 원리와 이해' 카테고리의 다른 글
7. 스프링 삼각형과 설정 정보_IoC/DI (0) | 2023.04.01 |
---|---|
5. 객체 지향 설계 5원칙 - SOLID (0) | 2023.03.29 |
4. 자바가 확장한 객체 지향 (0) | 2023.03.23 |
3. 자바와 객체 지향 (0) | 2023.03.23 |
1. 사람을 사랑한 기술 (0) | 2023.03.21 |