우리가 코딩한 프로그램을 실행하면 일반적인 경우 아래 그림과 같이 H/W(컴퓨터 본체) 위에 OS(Windows,MAC 등의 운영체제)가 실행되고, 그 운영체제 위에 코딩한 프로그램이 컴파일러를 통해 .exe
형태로 변환된 프로그램이 실행되는 구조로 설계된다.
그러나, java로 코딩한 프로그램을 실행할 경우 아래 그림과 같이 H/W(컴퓨터 본체) 위에 OS(Windows,MAC 등의 운영체제)가 실행되고, 그 운영체제 위에 우리가 코딩한 프로그램이 컴파일러를 통해 java byte code인 .class
형태로 변환이 된 후, JVM
이라는 프로그램이 실행이 되고, 그 위에 컴파일된 .class
형태의 프로그램이 실행이 된다.
이때, JVM
이란 Java Virtual Machine
의 약자로,
컴파일된 java 파일을 실행시켜주는 가상머신이다.
쉽게 말해서 java로 코딩한 프로그램을 실행시켜 주는 프로그램인 셈이다.
이렇게 코드를 OS 위에 직접적으로 실행시키지 않고, JVM
이라는 프로그램을 거쳐서 실행할 경우에는 OS 위에서 바로 실행하는 프로그램보다 느릴 수 밖에 없다.
그럼에도 불구하고 같은 프로그램을 제작할때 실행 속도가 더 느린 JVM을 사용하는 이유는 바로 H/W dependency
때문이다.
예를 들어보도록 하자.
필자가 c언어를 이용해 채팅 프로그램을 만들었다고 가정하자. 이때, 필자는 윈도우에서 개발하고, 테스트를 했기 때문에 win32 API
라는 것을 이용해 유저간의 통신을 해야 한다.
그러다 필자가 만든 채팅 프로그램이 많은 사람들에게 알려지면서 스마트폰과 맥북에서도 필자의 채팅 프로그램을 사용할 수 있도록 하기 위해서는 코드를 처음부터 다시 작성해야 한다.
필자가 유저간 통신을 할 수 있도록 작성한 win32 API
는 윈도우 운영체제에서만 작동한다.
즉, 맥북에서 동작하게 하려면 win32 API
가 아닌, POSIX library
를 사용해야 하며, 안드로이드에서 동작하게 하기 위해서는 kotlin
이라는 새로운 언어로 작성을 해야 한다.
뿐만 아니라 아이폰에서 동작시키기 위해서는 swift
라는 언어를 사용해야만 한다.
백번 양보해서 기존의 c언어로 작성을 할 수 있다고 하더라도 win32 API
대신 다른 통신 라이브러리를 사용해서 작성해야 한다.
즉, 사용하는 기기가 달라짐에 따라서 코드를 계속 수정하거나 처음부터 다시 작성해야 하는 경우가 발생할 수 있다.
이러한 현상을 H/W dependency
라고 한다.
이러한 문제를 해결하는 것이 바로 JVM
이다.
만약, 안드로이드 스마트폰에서 JVM
이 동작한다면 JVM
이 JAVA가 실행할 수 있는 가상의 환경을 만들어서 추가적인 코드의 수정 없이 채팅 프로그램이 동작하게 된다.
아이폰, 맥북, 그 외의 각종 OS가 탑재된 기기 역시 예외 없이 만약 JVM
이 동작한다면 JAVA로 작성한 코드의 추가적인 수정 없이 필자가 윈도우에서 실행했던 것처럼 동일하게 채팅 프로그램이 동작하게 된다.
이렇게 H/W dependency
를 줄임으로서 플랫폼에 구애받지 않고 동일한 성능으로 프로그램을 동작시킬 수 있다는 있는 것이 바로 JVM
이다.
현대에 와서는 플랫폼이 점점 다양화 되고 있다. 주위만 보더라도 안드로이드, IOS, MAC, windows등 다양한 플랫폼이 존재하며, 더 나아가 자동차에 들어가는 운영체제 뿐 아니라 냉장고 등에도 각종 운영체제가 들어가며, 이들은 각자의 용도에 맞게 운영체제가 customizing 된 경우도 상당히 많이 존재한다.
이러한 수많은 플랫폼에서 동일하게 동작하는 프로그램을 제작하고 연동하는 것 역시 중요해 지고 있는 가운데 앞으로 JVM
과 같은 가상머신의 중요성 역시 점점 증가하고 있다.
JMV
은 아래 그림과 같이 Class Loader
, Executation Engine
, Runtime Data Area
로 나뉜다.
이때의 JVM
의 각 영역의 동작은 다음과 같다.
먼저, JAVA로 작성한 코드가 컴파일러를 거치면 .class
형태로 변환이 된다. 이때, 이 .class
파일을 읽는 것이 바로 class loader
이다.
이 class loader
는 말 그대로 class를 로딩하는 역할을 한다.
java에서는 모든 기능들이 class
단위로 나뉜다. java에서 class 없이 독립적으로 동작하는 코드는 존재하지 않는다. 심지어 메인함수 마저도 class 내부에서 동작한다.
이때, class loader
에서는 각각의 class를 필요한 시기에 dynamic loading
한다.
예를 들어보도록 하자.
만약 우리가 java로 게임을 만들었다 가정하자.
이때, 우리는 캐릭터가 움직이는 기능을 하는 class, 캐릭터가 스킬을 사용하는 class, 다음 스테이지로 넘어가는 class를 만들었다고 가정해보자.
만약 캐릭터가 몬스터를 잡기 위해 움직이고 있는 상태라고 한다면 굳이 지금 메모리에 캐릭터가 스킬을 사용하는 기능을 하는 class와 다음 스테이지로 넘어가는 기능을 하는 class가 필요한가?
지금은 캐릭터가 움직이는 기능을 하는 class만 있어도 게임에는 전혀 지장이 없을 것이다.
이러한 상황에서 캐릭터가 움직이는 class만 불러와서 메모리에 올리는 것이 바로 dynamic loading
이며, 이러한 역할을 하는것이 바로 class loader
이다.
이렇게 class loader
에서 필요한 class를 메모리에 적재하면 JVM
의 Runtume Data Area 영역에 있는 각종 메모리에 적재가 된다.
이때, runtime data area의 구성은 아래 그림과 같이 heap area
, method area
, stack area
, PC register
, native method stack
로 구성된다.
heap area
heap area
일명 빵댕이 영역 는 각종 class의 instance들과 배열들이 저장되는 영역으로, JVM 내부에는 오직 하나의 heap area만 존재한다.
heap area
는 앞서 설명한 바와 같이 class의 instance가 적재되는 영역이므로, 메모리를 동적으로 사용하며, 다른 영역들고 메모리가 공유되는 특징을 갖고 있다. 또한, 동적으로 메모리가 사용되기에 Garbage Collector
라는 동적을 메모리를 관리하는 시스템에 의해 관리가 된다.
method area
method area
는 static 변수를 포함해 필드, 함수, 코드 등 클래스 수준에서의 모든 데이터 및 인터페이스가 저장되는 곳이며, JVM 내부에 오직 한개의 runtime area만이 존재하며, heap area
와 마찬가지로 다른 영역들가 공유된다.
stack area
stack area
는 frame
이라는 자료구조를 사용하며, 이 frame
을 저장하는 영역이다.
이 frame
은 함수가 호출될 때 마다 생성이 되며, 함수가 종료가 되면 사라진다. 또한, 각 frame
은 local variable array, operand stack, run time constant pool에 대한 참조값을 가진다.
PC register
PC register
란 JVM이 현재 실행중인 명령어의 주소를 저장하는 영역이다.
native method stack
native method stack
이란 JVM에서 유일하게 JAVA가 아닌 다른 native language(c, c++등..)으로 작성된 부분으로, native method 정보를 가지고 있다.
Excutation Engine
은 runtime area
에 할당된 코드를 실행하는 영역으로, bytecode를 한줄씩 읽고 실행시키는 방식으로 동작한다.
Excutation Engine
은 크게 interpreter
, JIT compiler
, Garbage Collector
로 나뉘어져 있다.
interpreter
interpreter
란 한줄씩 bytecode를 순차적으로 해석하는 방식으로, 해석이 빨라 초기에 프로그램이 빨리 실행된다는 장점을 갖고 있으나, 그 이후의 실행이 느리다는 단점이 존재한다. 뿐만 아니라 하나의 메소드를 여러번 호출하는 경우에는 늘 새로운 해석이 필요하다는 단점이 있다.
JIT compiler
JIT compiler
란 이러한 interpreter
의 단점을 보완해 주는 방식으로, 전체 byte code를 한번에 해석해서 native code로 변환하는 방식이다. 일반적으로 excutation engine
에서는 interpreter
가 bytecode를 변환하다 반복되는 구간에 한해서는 JIT compiler
가 변환을 하는 방식으로 동작한다.
Garbage Collector
Garbage Collector
란 동적할당된 값들중 사용 안하는 값을 "알아서 잘" 할당을 해제시키는 장치로, 각종 알고리즘을 통해 할당된 변수들의 사용 주기 및 사용 시간을 분석해 사용을 안하는 변수라고 판단되는 경우에 할당을 해제시키는 방식으로 동작한다.
이번 시간에는 java의 동작 원리에 대해서 살펴보았다.
그렇다면 오늘 배운 내용을 통해서 알게된 것은 무엇인가?
JVM
이라는 이름의 가상환경에서 동작한다. JVM
에서는 java에서 작성한class
라는 것을 단위로 메모리에 적재하며, 실행한다.JVM
이라는 가상환경이 동작하는 곳에서는 java 코드의 수정 없이 동일하게 동작한다.아마 크게 와 닿는것은 이 3가지 였을 것이다.
이를 통해 우리는 추후에 java로 코딩을 할때, class
설계의 중요성에 대해 대략이나마 알 수 있다.
예를 들어 우리가 게임을 만든다고 가정할때 게임의 모든 기능을 단 하나의 class로 작성할 경우, JVM
에서는 게임의 모든 기능을 한번에 전부 메모리에 적재하고, 실행하므로 메모리 낭비가 발생하며, 게임의 실행 시간 및 랙이 걸리는 현상이 발생하게 될 것이다.
그렇다고 해서 class를 너무 세분화 할 경우에는 메모리의 절약은 가능하나, 결국에는 JVM 내부에서도 class를 읽고, 적재하고, 실행하는데 시간이 소요되므로, 이 역시 게임을 하면서 랙이 걸리는 현상이 발생하게 될 것이다.
그렇다면 class를 어떻게 설계해야 하는가?
결국 시스템 전체 매커니즘을 고려해 가장 필요한 기능들 만을 묶어 최소 단위로 설계하는 것이 바람직한 class 설계일 것이다.
즉, 무조건 class를 적게 만드는 것도 아닌, class를 많이 만들어서 세분화 하는것도 아닌, "알아서 적당하게 잘" 만들어야 이상적인 class의 설계이며, 프로그램을 최적화 하는 것이다.
이를 위해서는 어떤 동작을 하는 시스템을 만들 것이며, 이 시스템은 어떻게 동작하는지 등과 같이 시스템에 대한 전체적인 이해와 더불어 어떤 기능이 어떻게 상호작용을 하는지등 시스템의 세부적인 기능과 상호작용에 대해 이해하고 있어야 한다.
한마디로 말하면 시스템에 대해서 매우 잘 알고 있어야 한다는 것이다.
물론, 실제로 개발하는 시스템 자체가 상당히 복잡하기에 이 시스템에 대해서 잘 알기는 쉽지 않다. 그러나, 지금부터라도 꾸준히 시스템을 설계하는 연습을 하고, 시스템을 보는 습관을 들인다면 시스템에 대해 잘 아는것이 불가능 한 것도 아니다.
지금부터라도 꾸준히 연습하자. 우리가 풀어야 할 문제가 주어지면 바쁘다는 핑계로, 빨리 끝내고 쉬고싶다는 핑계로 키보드부터 잡지 말자.
오히려 펜 부터 잡고 시스템을 차근차근 설계해보자. 지금 당장은 느리고, 미련해 보이더라도 나중에 이것이 우리의 경쟁력이 될 것이다. 그렇게 시간이 지나면 우리는 단순히 "땔감"으로 불리는 코더가 아닌 엔지니어가 되 있을 것이며, 우리가 펜을 먼저 잡았던 습관이 우리를 먹여살릴 기술이자 능력이 되 있을 것이다.