Java의 동작 원리 및 버전별 특징 (Java vs Kotlin)

최민길(Gale)·2023년 1월 8일
1

Spring Boot 적용기

목록 보기
2/46

안녕하세요! 오늘은 저번 시간에 이어서 Spring boot를 사용하여 API 서버를 개발할 때 사용할 언어에 대해 알아보는 시간을 갖도록 하겠습니다.

우선 Spring boot에서는 대표적으로 자바와 코틀린을 제공합니다. 코틀린의 경우 자바와 100% 호환이 가능하며 자바보다 더 간결하고 실용적, 경제적이라는 장점이 있습니다. 하지만 Gradle을 통한 컴파일 시 속도가 느리며 자바8의 향상된 기능을 제공하지 않아 성능 측면에서 자바에 비해 부족합니다. 또한 Spring boot의 자료들 역시 자바의 자료가 더 많이 존재하기 때문에 이번 API 서버에서는 자바를 활용하여 서버를 만들어보려고 합니다.


출처 : https://steady-snail.tistory.com/67

우선 자바의 동작 원리에 대해 알아보겠습니다
1. .java 파일을 자바 컴파일러(javac)가 바이트 코드로 컴파일한다.
2. 컴파일된 바이트 코드를 JVM의 클래스 로더에 전달한다.
3. 클래스 로더는 동적 로딩을 통해 필요한 클래스(바이트 코드)들을 JVM의 메모리인 런타임 데이터 영역으로 올린다.
4. 실행 엔진에서 JVM 메모리에 올라온 바이트 코드들을 실행한다.

보이시는 것처럼 컴파일러가 바이트 코드로 컴파일한 후 JVM에서 코드가 실행되는데, JVM의 경우 클래스 로더, 런타임 데이터 영역, 그리고 실행 엔진으로 이루어져 있습니다.


출처 : https://steady-snail.tistory.com/67

런타임 데이터 영역(=메모리)의 경우 JVM이 OS 위에서 실행되면서 할당받는 메모리 영역으로, 다음과 같이 이루어져 있습니다.
1. PC(Program Counter) 레지스터 : 현재 수행 중인 명령의 주소를 가지고 있고 스레드 시작 시 생성되며 각 스레드마다 하나씩 존재
2. JVM 스택 : 스택 프레임이라는 구조체를 저장하는 스택
3. 네이티브 메서드 스택 : 자바 외의 언어로 작성된 코드를 위한 스택
4. 힙 : 인스턴스 또는 객체를 저장하는 공간으로 동적으로 할당된 메모리 영역. 모든 Object 타입의 데이터가 할당되고 Object를 가리키는 참조 변수가 스택 영역에 저장
5. 메서드 영역 : 모든 스레드가 공유하는 영역. JVM 생성될 때 생성되며 클래스, 런타임 상수 풀 등을 보관
5-1. 런타임 상수 풀 : 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조하기 위해 사용되는 상수 풀. JVM 동작에서 핵심적인 역할 수행

여기서 PC 레지스터와 JVM 스택, 그리고 네이티브 메서드 스택의 경우 각각 스레드가 차지하고 있어, 런타임 데이터 영역은 크게 스레드가 차지하는 영역, 객체를 생성 및 보관하는 힙 영역, 그리고 클래스 정보의 메서드 영역으로 나뉘게 됩니다.


출처 : https://steady-snail.tistory.com/67

클래스 로드의 경우 런타임 중에 JVM의 런타임 데이터 영역 내에 존재하는 메소드 영역(클래스 영역)에 동적으로 자바 클래스를 로드하는 역할을 하며 위의 그림처럼 여러 종류가 계층 형태로 존재합니다. 가장 위에 있는 부트스트랩 클래스 로더의 경우 JVM 시작 시 가장 최초로 실행되어 메모리에 올라가는 클래스 로더입니다. Object 클래스를 비롯하여 자바의 API들을 로드합니다. 익스텐션 클래스 로더의 경우 기본 자바 API를 제외한 확장 클래스들을 로드하며, 시스템 클래스 로더의 경우 사용자가 지정한 경로 내의 어플리케이션의 클래스들, 즉 사용가 생성한 클래스를 로드합니다.

클래스 로더는 다음과 같이 동작합니다.
1. JVM의 메소드 영역에 클래스가 로드되어 있는지 확인 (만약 로드되어 있다면 해당 클래스 사용)
2. 클래스가 로드되어 있지 않을 경우 시스템 클래스 로더에 클래스 로드 요청
3. 시스템 클래스 로더는 익스텐션 클래스 로더에 요청 위임하고, 익스텐션 클래스 로더는 부트스트랩 클래스 로더로 요청을 위임
4. 부트스트랩 클래스 로더에서 클래스 존재 여부를 확인하여 존재하지 않을 경우 익스텐션 클래스 로더로 요청을 넘기고, 익스텐션 크래스 로더에도 존재하지 않을 경우 시스템 클래스 로더로 요청을 넘김
5. 시스템 클래스 로더에서 CLASSPATH에 클래스가 있는지 확인하고, 존재하지 않을 경우 ClassNotFountException 발생

즉 요약하자면, 가장 밑에 있는 클래스 로더에서 가장 상위의 부트스트랩 클래스 로더까지 반드시 요청이 올라가게 되며, 부트스트랩 클래스 로더에서 아래로 내려가면서 클래스 존재 여부를 확인하는 특징을 띄고 있습니다. 따라서 만약 시스템 클래서 로더에 존재하는 클래스이더라도 시스템 클래스 로더까지만 올라가는 것이 아니라 부트스트랩 클래스 로더까지 올라간 이후 내려오는 과정에서 클래서가 로드되는 방식입니다.

이 때 하위 클래스 로더는 상위 클래스 로더가 로드한 클래스를 볼 수 있지만, 상위 클래스 로드는 하위 클래스 로더가 로드한 클래스를 알 수 없습니다. (가시성 제한) 이를 통해 상위 클래스 로더에서 로드한 클래스도 하위 클래서 로더에서 사용할 수 있게 됩니다. 또한 하위 클래스 로더가 상위 클래서 로더에게 로드한 클래스를 다시 로드하지 않아야 합니다. (유일성의 원칙) 이를 통해 각 클래스 로더 별 고유한 클래스를 보장할 수 있습니다.


출처 : https://steady-snail.tistory.com/67

그렇다면 클래스 로더는 어떤 식으로 클래스를 로딩할까요? 위의 그림처럼 5단계로 나타납니다.
1. 로딩 : 클래스 파일을 가져와서 JVM의 메소드 영역에 로드
2. 검증 : 클래스가 자바 언어 명세 및 JVM 명세대로 구성되어 있는지 검사
3. 준비 : 클래스가 필요로 하는 메모리 할당
4. 분석 : 클래스 내의 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경
5. 초기화 : 클래스 변수들을 적절한 값으로 초기화(정적 블록 -> 정적 변수 -> 생성자)

이 때 1번 과정, 즉 로딩을 하는 과정에서 클래스 참조 시점에 JVM에 코드가 링크되고 실제 런타임 시점에 로딩되는 동적 로딩을 거칩니다. 즉 JVM이 미리 모든 클래스에 대한 정보를 메소드 영역에 로딩하지 않습니다. 이 때 JVM에 클래스가 로딩되고 초기화 시 순차적으로 동작하며, 여러 스레드가 동시에 클래스를 로딩하려고 하면 오직 한 개의 클래스만 로딩됩니다. 동적 로딩의 방법은 로드 타임 동적 로딩 방식과 런타임 동적 로딩 방식 크게 2가지의 방식이 있습니다.

로드 타임 동적 로딩은 하나의 클래스를 로딩하는 과정에서 동적으로 다른 클래스를 로딩하는 방법이며 다음과 같이 동작합니다.
1. JVM 시작 후 부트스트랩 클래스 로더가 생성된 후 모든 클래스가 상속받고 있는 Object 클래스 읽어옴
2. 읽어올 클래스 파일을 읽고, 이 과정에서 필요한 클래스를 별도로 로딩

런타임 동적 로딩은 클래스를 로딩할 때가 아닌 코드를 실행하는 순간에 클래스를 로딩하는 것으로, 다음과 같이 동작합니다.
1. 클래스 로딩 시 어떤 클래스도 읽어오지 않음
2. 클래스 내부의 메소드가 실행될 때 그 메소드와 관련딘 클래스를 로딩


출처 : https://steady-snail.tistory.com/67

실행 엔진은 클래스 로더를 통해 메소드 영역에 올라온 바이트 코드를 명령어 단위로 읽어서 기계가 실행할 수 있는 네이티브 코드 형태로 변경합니다. 바이트 코드는 1바이트 크기의 OpCode와 추가 피연산자로 이루어져 있는데, 실행 엔진에서 하나의 OpCode를 가져와 피연산자와 작업을 수행 후, 다음 OpCode 연산을 수행하는 방식으로 동작합니다. 실행 엔진의 동작 방식은 다음의 두 가지 방법입니다.

  1. 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석. 하나하나의 해석은 빠르지만 전체적인 실행 속도는 느리다.(JVM 기본 방식)
  2. JIT 컴파일러 : 바이트 코드 전체를 컴파일한 후 네이티브 코드로 변경하여 더 이상 인터프리팅 하지 않고 네이티브 코드를 직접 실행하는 방식.

이 때 JIT 컴파일러의 경우 한번에 컴파일하기 때문에 인터프리팅 방법에 비해 변환 과정이 오래 걸리나 한번 변환하면 캐시에 보관된 네이티브 코드를 바로 실행하기 때문에 빠르게 수행됩니다. 따라서 JVM 내부적으로 메서드가 얼마나 자주 호출되는지에 따라 JIT 컴파일러를 도입하는 것이 성능 향상에 더 효과적입니다.

또한 JVM에는 가비지 컬렉터가 존재합니다. 가비지 컬렉터(gc)란 동적으로 할당된 메모리 영역(=힙 영역) 중 사용하지 않는 영역을 해제하는 역할을 수행합니다. 가비지 컬렉터는 힙 내의 객체에 대해 유효한 참조가 있는지 없는지를 판단하여 유효한 참조가 없다면 삭제하는 방식으로 수행합니다. 스택 영역의 모든 변수를 스캔하면서 어떤 객체를 참조하고 있는지를 찾아서 마킹하고, 유효 객체들이 참조하고 있는 객체들도 마킹한 후 마킹되지 않은 객체를 힙 영역에서 제거하는 방식으로 성능을 향상시킵니다.

이상으로 JVM에 대해 알아보았습니다. 그렇다면 자바의 어떤 버전을 사용해야 할 지에 대해서 살펴보기 위해 자바 8부터 19까지 간단하게 알아보도록 하겠습니다

  • Java8 : Lambda, stream 등이 추가
  • Java9 : 컬렉션, 스트림 등의 추가 기능과 모듈시스템이 추가
  • Java10 : var 키워드 도입, 병렬 처리 가비지 컬랙션이 도입되어 성능이 향상, JVM 힙 영역을 시스템 메모리 외의 다른 종류의 메모리에도 할당 가능
  • Java11 : Oracle JDK와 OpenJDK 통합, 람다 지역변수 사용법 변경 등
  • Java12 : 유니코드 11지원 등
  • Java13 : 유니코드 12.1 지원, 새로운 스위치 표현식 제공(preview)
  • Java14 : 스위치 표현식 표준화
  • Java15 : Text-Blocks / Multiline Strings 등 프로덕션 출시
  • Java16 : Unix 도메인 소켓에 연결 가능
  • Java17 : 보안 관리자 deprecated, Sealed Classed 프로덕션 출시 등
  • Java18 : UTF-8을 Java Standard API의 기본 charset으로 설정, finalization for removal deprecated
  • Java19 : 구조적 동시성, 레코드 패턴(preview) 등 추가

아직까진 버전에 대해 자세히는 모르겠지만 Java17의 경우 Java11 이후 장기 지원 릴리스 버전이라 17 버전으로 서비스를 구축해보려고 합니다. 이상으로 긴 글 읽어주셔서 감사합니다!

내용 출처
https://steady-snail.tistory.com/67
https://steady-coding.tistory.com/593
https://velog.io/@ljo_0920/java-%EB%B2%84%EC%A0%84%EB%B3%84-%EC%B0%A8%EC%9D%B4-%ED%8A%B9%EC%A7%95

profile
저는 상황에 맞는 최적의 솔루션을 깊고 정확한 개념의 이해를 통한 다양한 방식으로 해결해오면서 지난 3년 동안 신규 서비스를 20만 회원 서비스로 성장시킨 Software Developer 최민길입니다.

0개의 댓글