인프런 널널한 개발자 강사님의 기초 탄탄! 독하게 시작하는 Java - Part2 : OOP와 JVM의 강의 중 JVM내용 정리


 

Class Loader는 우리가 직접 만들수도 있다고 합니다. C/C++로 구현하지 않고 JAVA로도 직접 구현해서 사용할 수 있다고 하네요.

Class Loader에 대해 이해하기 위해 .class 파일과 바이트 코드에 대해 짚고 넘어갑니다.

 

.class 파일은 .java파일을 컴파일 했을때 생성되는 자바 바이트 코드입니다.

 이것을 Class Loader가 Loading하는 것입니다. 윈도우로 치면 .exe, .dll 같은 확장자를 가진 실행파일(PE)이 JVM에서는 .class파일인 겁니다.

class 파일의 구조는 아래와 같다고 합니다.

ClassFile {
    u4 				magic;
    u2 				minor_version;
    u2 				major_version;
    u2 				constant_pool_count;
    cp_info 			constant_pool[constant_pool_count-1];
    u2 				access_flags;
    u2 				this_class;
    u2 				super_class;
    u2 				interfaces_count;
    u2 				interfaces[interfaces_count];
    u2 				fields_count;
    field_info 			fields[fields_count];
    u2 				methods_count;
    method_info 		methods[methods_count];
    u2 				attributes_count;
    attribute_info 		attributes[attributes_count];
}

 

위에서 부터

u4는 unsigned 캐릭터 아마 4바이트

밑으로 minor_version, major_version 정보가 나와있고

그 밑에 중요한 constant_pool_count 얘기가 나옵니다. 아무튼 이것저것 this class, super class, field, method, 이런정보가 담겨져 있구나 라는걸 알 수 있씁니다.

 

 

 

사진은 대표적인 Hello World! 코드를 컴파일한 .class 을 16진수 편집기로 열어본 것입니다.

 

맨 앞의 CA FE BA BE 이 4바이트가 위에서 u4에 해당하는 거고, 그 뒤로 major/minor 버전, constant pool 등등 정보가 담겨져 있는 것입니다. 근데 뭐 저거만 보고 리버스 엔지니어링을 하는 것은 힘들기 때문에 보통 도구를 이용해서

.class 파일을 분석하기도 한다고 합니다.

 

IntelliJ에서도 바이트코드를 볼 수가 있는데

바이트 코드 보는법은 생략하고 저런식으로 더 쉬운 형태로 변환하여 .class 파일을 분석할 수 잇다고 합니다.

저런 거로 디컴파일도 할 수 있다고 하네요.

 

아무튼

 

Hello World를 출력하는 .java 파일이 java byte code로 바뀌고 그걸 JVM이라는 프로세스가 실행시켜주는데,

근데 실질적으로 실행은 CPU가 하는 것입니다.

자바로 프로그램을 개발하게 되면 결국은 자바 소스코드 말고,

.class 파일에 들어있는 형태인 자바 바이트 코드의 스트림 형태로 변환이 되어서 로딩되고 적재된 다음에 어쩌구저쩌구 쭈욱 가는겁니다.

 

 

 

자바 바이트코드의 실행의 근거가 되어주는 것들은 JVM에서의 명령들입니다. 근데 JVM 자체가 가상 머신이지 진짜 머신(CPU)는 아닙니다. 그래서 결과적으로 그것들(바이트코드들)을 CPU가 인식할 수 있는 Native코드로 바꾸어 주어야 합니다. 그래서 그 과정에서 사용되는 컴파일러가 등장합니다. 그래서 컴파일러가 두 번 등장하는데

첫 번째로, .java파일을 .class 파일로 바꾸는 컴파일러.

두 번째로, .class파일을 CPU가 이해할 수 잇는 Native 코드로 바꾸는 컴파일러

네이티브로 바꿔주는 컴파일러는 두 놈이 있습니다.

 

출처: https://medium.com/webeveloper/jvm-java-virtual-machine-architecture-94b914e93d86

JVM의 3가지 영역 중 Execution Engine에 해당되는 부분에서 Interpreter와 JIT Compiler 입니다.

 

JIT컴파일러(Just-In-Time) 는 JVM이 바이트코드 레벨에서 분석을 해서 예를들어 반복문을 돈다고 하면 반복문 안의 코드는 특정 횟수 만큼 반복이 될 것입니다 이런 반복되는 부분에대해 JIT가 미리 컴파일을 해놓습니다.

평상시에는 Interpreter가  바이트코드를 일정 단위 뜯어와서 CPU에 번역해서 연산시키고 합니다. 근데 이걸 매번 변환하는 과정을 거친다는 것은 성능을 떨어뜨리는 원인이 됩니다.

그래서? JIT가 자주 사용되는 부분을 미리 번역해놓고 재사용하기 때문에 JIT로 인해 JVM의 성능이 극단적으로 상승했다고 합니다.

 


 

Class Loader

클래스로더의 역할을 생각해보면 하는 일도 많고 중요하기도 하지만 생각보다 이걸 건드리거나 할 일은 많이 없다고 합니다. 따라서 클래스로더가 무슨일을 하는지 이론적으로만 알아도 된다고 합니다.

 

우리가 작성한 JAVA파일을 class 파일로 컴파일이 되고, class파일 안에 있는 자바 바이트코드를 JVM이 뜯어와서 실행을 하게 됩니다.

중요한 점은 바이트 코드의 본질은 결국에는 Method라는 것입니다.

 

클래스 로더가 하는 일은 아래와 같습니다.

 

1. 이름을 알고 있는 특정 클래스에 대한 정의(Byte Stream)를 가져오는 역할을 수행

  -> 가져온다라는 의미를 생각해 보면 어디 다른곳에서 가져올 수 있다는 의미입니다. 네트워크를 통해 퍼온다거나 할 수 있습니다.

2. 부트스트랩 클래스 로더

  - JVM에서 라이브러리로 취급(rt.jar, tools.jar)되는 것들을 로드 (핫스팟에서는 C++로 구현)

3. 플랫폼 클래스 로더 (기존 확장 클래스 로더)

  - 클래스 라이브러리 로드

4. 애플리케이션 클래스 로더

  - sun.misc.Launcher$AppClassLoader를 의미

 

 

클래스가 로딩되는 과정

 

개발자에게는 3가지 시점이 있습니다.

1. 컴파일타임

2. 링크타임

3. 런타임

그런데 자바에서는 이렇게 구분하는 개념이 없습니다. build를 하면 컴파일타임을 말하고 실행을 하면 링크 + 런타임을 묶는다고 합니다.

 

출처 : 강의자료 일부

그래서 클래스를 로딩하겠다 하면 JVM이 인식할 수 있게 하는 자바 바이트코드 스트림 덩어리로 바꾸는 과정을 로딩 단계라고 합니다. 위 그림에서는 Loading -> Linking 까지 입니다.

그리고 사용단계는 그 아래쪽인데 사용단계 수준에서 인스턴스화를 진행합니다. 즉, new 연산이 등장하는거죠 그래서 Using 박스 안에 Initializaion(초기화) 가 있는 것입니다.

그리고 Unloading은 GC를 통해 메모리가 해제되는 것입니다.

 

Java 클래스 로딩

- 클래스 로딩 및 링킹 과정이 모두 런타임에 이루어짐

- 실행 성능이 일부 저하될 수 있으나 높은 확장성과 유연성을 제공하는 근간

  - 인터페이스만 맞으면 Runtime에 구현 클래스를 결정하지 않을 수 잇음

  - 클래스 로더는 실행할 프로그램 코드를 네트워크로 수신하는 것도 가능

- 해석(Resolution) 단계는 동적 바인등(혹은 늦은 바인딩)을 지원할 목적으로 초기화 후로 지연될 수 있음

 

 

로딩 단계에서 가장 먼저 하는일, 검증.(Verification)

- JVM 명세가 정하는 규칙과 제약을 만족하는지 확인합니다.

  - 파일 형식(.class)

  - 메타데이터

  - 바이트코드

  - 심벌 참조

- 보안위협에 대한 검증 포함

  - 바이트코드 검증 시 함께 확인

 

준비 및 해석단계(Preparation, Resolution)

- java.lang.Class 인스턴스(메타 데이터)가 힙 영역에 생성되고 클래스 변수(정적 멤버) 메모리를 0으로 초기화

  -> 로드되는 클래스의 인스턴스는 Using 단계에서 힙 사용

  -> final 선언된 변수는 코드에서 정의한 초깃값으로 정의(0이 아닐수 있다)

- 생성자 호출 전 상태(new 연산 전 단계)

  -> 필드(인스턴스 변수) 초깃값은 생성자 호출 시 정의됨

  -> 정적 필드에 초깃값 할당

- 해석은 상수 풀의 심벌 참조를 직접 참조로 대체하는 과정

 

 

사용단계

Heap 영역에 객체 생성 (new 연산 실행) 

- JVM은 객체 저장을 위한 메모리 공간을 확보 후 0으로 초기화 (단, 객체 헤더 제외)

- 객체 초기화를 위한 구성설정 실시

  -> 클래스 이름 및 메타 정보 확인 방법

  -> 객체에 대한 해시코드

  -> GC 세대 나이

- 생성자 호출

 

'CS > JAVA' 카테고리의 다른 글

JVM 이해하기 (1) - 도입  (3) 2025.01.28
옵저버 패턴 정리  (0) 2024.07.21

인프런 널널한 개발자 강사님의 기초 탄탄! 독하게 시작하는 Java - Part2 : OOP와 JVM의 강의 중 JVM내용 정리


 

 

개발을 하고 서비스를 하면서 장애가 났을때에 다양한 원인이 있겠지만, 해결을 하려할 때 JVM에 대한 깊은 이해가 없을경우 해결을 못하는 경우가 있습니다. 

 

JVM에 대한 이해를 쌓고자 강의를 들으면서 알게 된 것들과 중요한 내용을 지금부터 정리해보겠습니다


 

 

JVM은 결과적으로 보면 프로세스이다.  java.exe 실행 파일을 실행시켜서 그 위에서 내가 만든 어플리케이션을 돌리는 것이다.

JVM은 기본적으로 Virtual Machine이다.

여담

널널한 개발자님 강의를 보다 보면 설명을 하시면서 물리적/논리적 개념으로 나누는데 

물리적인 부분 == H/W
논리적인 부분 == S/W
으로 봐도 된다.

또한 논리적이다 라는 말은 가상이라는 개념으로 봐도 된다고 종종 말씀하신다.
logical 을 IT쪽에서는 Virtual이라고 한다.
그리고 컴퓨터에서 CPU를 machine이라고도 한다고 하는데 그렇다면
Vircual Machine이다 라고 하면 CPU를 논리적으로 구현한것이다 라고 해석할 수 있다.

 

 

 

강사님께서 정말 많이 자주 그리시는 컴퓨터의 형태이다.

 

하드웨어 영역과 소프트웨어 영역으로 나누어져 있고, 소프트웨어영역은 Application영역과 System영역이 있다. System 영역의 가장 대표적인게 운영체제인데, 운영체제의 핵심적인 알맹이를 커널이라고 한다. 

커널은 그림에서 Kernel 영역에서 돌아가는 것이고

User Mode Application 영역이 있는데, 이것은 이제 위 그림에서 User 영역에서 돌아가는것이다.

 

우리가 소위 Native라고 부르는 영역은 H/W부터 Kernel까지 포함하는 구간이다.

그래서 그 부분을 개발하는것은 C/C++과 같이 Native언어로 개발을 하는데, C/C++코드가 CPU가 이해할 수 있는 언어로 직접 번역이 되어 실행합니다.

그렇기떄문에 H/W나 OS에 의존성이 있고, 의존성이 있다는 말은 Window에서 개발한 C/C++코드가 있으면 그 코드는 리눅스에서 제대로 동작하지 않을 수 있다는 것입니다.

 

처음에 말했듯 VM이라는 것은 컴퓨터를 소프트웨어적으로 구현한 것이다. 머신은 머신인데 이 머신이 인식할 수 있는 코드가 자바 바이트 코드라면 그걸 JVM 이라고 한다는 것이다.

 

결과적으로 JVM이라는것을 말하면 소프트웨어로 CPU도 구현돼있고, RAM도 구현돼있고, SSD도 구현돼있는 것이다. 따라서 JVM은 User Mode Applicatio 프로세스 임과 동시에 가상 메모리 공간을 RAM이나 보조기억장치처럼 활용 하면서 동시에 머신으로 연산도 해주는기능을 모두 포함하고 추가로 운영체제가 제공하는 기능 일부까지 다 들어있습니다.

 

따라서 JVM은 CS론에서 "OS/컴퓨터구조 를 전부 섞어서 C나 C++로 구현" 한게 JVM이 된다는 것이다.

그래서 위에서 잠깐 설명한 네이티브영역을 개발하는 언어들과 다르게 JVM만 있다면 내가 만든 자바코드는 JVM이 돌려주기 때문에 OS에 영향이 없이 JVM만 있다면 돌릴 수 있는 것이다.

 

C++ 과 Java의 메모리 관리 차이

C++ Java
- 객체에 대한 모든 관리 책임은 개발자에 있다.(소유권, 메모리 할당 및 해제)

- 객체의 생명주기에 모두 개입하는 구조
- 객체 메모리 해제는 전적으로 JVM몫이다.(개발자에게는 소유권도 책임도 없음)

- 문제 발생 시 구조를 알아야 대응이 가능함

 

C++은 자유도가 극상에 가깝다. 근데 자유도가 극상인 만큼 책임도 많이 져야한다.

자바는 GC가 메모리관리를 해준다. 

 


 

JVM 구성요소

출처 : https://medium.com/webeveloper/jvm-java-virtual-machine-architecture-94b914e93d86

 

1. Class Loader,  2. Runtime Data Area, 3.  Execution Engine으로 크게 3가지 영역으로 나뉜다.

아무 영역에 속하지 않은 Native Method이쪽은 User Mode가 아닌 Kernel모드 수준에서의 API를 호출하게 해주는걸 가능하게 해준다.

나는 3가지 영역에 대해서만 알아볼거고, 2번 Runtime Data Area영역이 가장 중요하다고 한다.

 

 

'CS > JAVA' 카테고리의 다른 글

JVM 이해하기 (2) - Class Loader  (2) 2025.01.29
옵저버 패턴 정리  (0) 2024.07.21

옵저버 패턴

정의

옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자 객체들이 관찰하는 객체에 변화가 있을 때마다 변경 정보를 받고 갱신되도록 하는 디자인 패턴입니다.

옵저버 패턴은 1:1혹은 1:N 의존성을 가지는데, 주로 분산 이벤트 처리 시스템에서 많이 사용됩니다.

발행/구독 모델로 알려져 있기도 합니다.

객체 사이의 의존성을 줄이고 유연하게 상호작용하도록

즉, Coupling을 느슨하게 유지하는 것을 도와주는 패턴입니다.

이해하기 가장 쉬운 예시는 유튜브 혹은 인스타그램을 생각하면 됩니다.

유튜브에서 내가 구독한 채널이 새로운 영상을 올리면 새 영상 업로드 알림을 받습니다.

나 뿐만아니라 같은 채널을 구독한 다른 모든 구독자들도 알림을 받을것입니다.

옵저버 패턴에서 유튜브 채널이 Subject가 되고, 구독자들이 Observer가 됩니다

 

관찰자(Observer)라고 해서 관찰자 객체들이 Subject를 스스로 관찰하는 것처럼 느껴지지만
직접 관찰한다는 느낌 보다는 Subject의 변화를 통한 정보 갱신을 하기 위해 변경사항을 전달받길 기다리는 수동적인 상태에 더 가깝습니다.

 

옵저버 패턴의 클래스 다이어그램을 그리면 아래와 같습니다.

Subject와 Observer는 인터페이스 이며 이것을 구현하는 클래스가 필요합니다.

Subject에서 observerCollection을 갖는데 타입으로 L등이 될 수 있습니다.

Subject를 구현한 클래스를 ConcreteSubjet라고 하겠습니다.

ConcreteSubjets는 Observer들을 List, Set, Map 등의 Collection에 모아 갖고 있습니다.

또한 Observer들을 내부에서 등록 및 삭제하는 구조이며 Subject의 상태가 변경되면 Observer들에게 알림을 발행합니다.

 

 

옵저버 패턴 흐름

1. 한개의 주체(Subject)와 여러개의 관찰자(Observer)로 구성되어습니다.

2. Observer 패턴에서는 Subject의 상태가 바뀌면 변경사항을 Obsever들에게 전달합니다.

3. Subject로부터 받은 정보로 Observer의 정보를 바꿀 수 있습니다.

4. Observer들은 Subject의 Collection에서 삭제/추가 될 수 있습니다.

유튜브 채널이 있고, 영상을 게시했을때 구독자에게 알림을 보내는 상황을 가정하여 코드를 작성해봤습니다.

 

Observer.java

public interface Observer {
    void update(String title);
}

관찰자/구독자 역할을 하게될 Observer 인터페이스입니다.

ObserverImpl.java

public class ObserverImpl implements Observer{
    String name;
    public ObserverImpl(String name){
        this.name = name;
    }
    @Override
    public void update(String title) {
        System.out.println(this.name + "님, [" + title + "] 영상이 게시되었습니다.");
    }
}

Observer를 구현한 클래스입니다.

 

Subject.java

public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObserver(String title);
}

 

SubjectImpl.java

import java.util.ArrayList;
import java.util.List;

public class SubjectImpl implements Subject{

    private final List<Observer> observerList;
    public SubjectImpl(){
        observerList = new ArrayList<>();
    }

    // Observer 추가
    @Override
    public void registerObserver(Observer o) {
        observerList.add(o);
    }

    // Observer 삭제
    @Override
    public void removeObserver(Observer o) {
        observerList.remove(o);
    }

    // Observer들에게 이벤트 알림
    @Override
    public void notifyObserver(String title) {
        for (Observer o : observerList)
            o.update(title);
    }
}

 

Subject를 구현한 클래스입니다.

Observer들을 List를 이용해 관리합니다.

옵저버를 등록하고 알림을 보내는 것을 간단하게 구현했습니다.

public class youtube {
    public static void main(String[] args) {
        // 유튜브 채널 생성
        Subject channel = new SubjectImpl();
        
        // 구독자 생성
        Observer subscriber1 = new ObserverImpl("철수");
        Observer subscriber2 = new ObserverImpl("영희");
        
        // 구독자 등록
        channel.registerObserver(subscriber1);
        channel.registerObserver(subscriber2);
        
        // 영상 업로드 알림
        channel.notifyObserver("웃긴 짤");
        channel.notifyObserver("무서운 짤");
        
        // 영희가 구독 취소
        channel.removeObserver(subscriber2);
        
        // 영상 업로드 알림
        channel.notifyObserver("슬픈 짤");
    }
}

 

 

실행 결과는 아래와 같습니다.

영희가 구독을 취소한 이후로는 영희에게 알림이 가지 않습니다.

 

 

 

장점

1. 옵저버 패턴을 사용하면 Subject의 상태 변경을 매번 일일히 확인하지 않고도 자동으로 감지가 가능합니다.

2. Subject의 코드를 변경하지 않아도 새 구독자 클래스를 도입이 가능해 OCP를 준수합니다.

3. 런타임 시점에서 Subject와 Observer가 구독 알림 관계를 맺을 수 있습니다.

4. Subject와 Observer의 관계를 느슨하게 유지할 수 있습니다.

단점

1. 알림의 순서를 제어할 수 없으며, 무작위 순서로 알림을 받게됩니다.

2. 무분별한 사용은 코드의 복잡도가 증가합니다.

3. 옵저버 객체 등록 이후 해지하지 않으면 메모리 누수가 발생할 수 있습니다.

 


MVC 패턴

정의

MVC 패턴은 사용자 인터페이스를 설계하는 데 사용되는 디자인 패턴으로, 애플리케이션을 세 가지 주요 구성 요소로 나눕니다.

1. Model: 애플리케이션의 데이터와 비즈니스 로직을 처리합니다.

2. View: 사용자 인터페이스 요소를 처리하고 모델의 데이터를 사용자에게 표시합니다.

3. Controller: 사용자 입력을 처리하고 모델과 뷰를 업데이트합니다.

옵저버 패턴은 MVC 패턴의 모델과 뷰 간의 관계를 구현하는 데 자주 사용됩니다.

모델이 옵저버 패턴의 Subject이고, 뷰가 Observer가 됩니다.

모델의 상태가 변경되면, 모델은 뷰에게 변경 사항을 알리고, 뷰는 이를 반영하여 화면을 업데이트 합니다.

이것을 통해 MVC 패턴에서 모델과 뷰 간의 느슨한 결합을 유지할 수 있습니다.


옵저버 패턴을 직접 구현하지 않아도 자바의 내장 Observer 객체가 있어 클래스를 상속하기만 하면 쉽게 이용할 수 있습니다. (java.util.Observer, java.util.Observable)

하지만 Observable는 인터페이스가 아닌 클래스입니다. Observable클래스를 상속해야 하는데, 자바에서는 단일 상속만 지원하기 때문에

Subject역할을 하는 클래스가 다른 클래스를 상속하고 있는 상태라면 내장 객체를 이용할 수 없습니다.

메서드 위임을 해주면 되지 않나 싶지만, Observable의 메서드가 protected로 선언되어 있기 때문에 결국 자식 클래스에서 호출할 수 밖에 없습니다.

따라서 내장 객체를 이용할 수 없는 상황이라면 직접 구현해줘야만 합니다.

 

 

 

 

출처 및 참고

https://shan0325.tistory.com/33

https://gsbang.tistory.com/entry/Design-Pattern-Observer-Pattern%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4

https://velog.io/@te-ing/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%98%B5%EC%A0%80%EB%B2%84Observer-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

https://xzio.tistory.com/289

https://refactoring.guru/design-patterns/observer

https://ssdragon.tistory.com/144

 

'CS > JAVA' 카테고리의 다른 글

JVM 이해하기 (2) - Class Loader  (2) 2025.01.29
JVM 이해하기 (1) - 도입  (3) 2025.01.28

+ Recent posts