2. Java Study

[JAVA] JVM 메모리 구조란? (자바 가상 머신에 대해서 AraBoJa)

Dorothy. 2024. 7. 21. 11:20

 

 

 안녕하세요? 오늘도 돌아온 Dorothy입니다.

 

 오늘은 JVM 메모리 구조에 대해서 알아보도록하겠습니다.

Java Virtual Machine, 줄여서 JVM 구조라고 하는데요, JVM은 JAVA structure에서 중요한 내용이므로 반드시 정확하게 그 개념과 구조에 대해서 숙지해야 하겠습니다.

 

 자바 가상 머신(JVM)의 메모리 구조는 크게 다음과 같은 영역으로 나뉩니다:

 

  1. 메서드 영역 (Method Area)
  2. 힙 영역 (Heap)
  3. 자바 스택 (Java Stacks)
  4. PC 레지스터 (PC Registers)
  5. 네이티브 메서드 스택 (Native Method Stacks)

이와 같이 5개의 영역으로 이루어져 있는데요, 이를 알아보기 쉽게 도식화하면 다음과 같습니다. 

 

1. 메소드 영역

메소드 영역은 JVM의 메모리 구조 중 하나로, JVM이 시작될 때 생성됩니다.

메소드 영역은 클래스의 메타데이터 즉, 클래스 레벨의 정보들을 저장하는 공간입니다. 

클래스 레벨의 정보는 다음과 같습니다.

  • 클래스 이름
  • 수퍼클래스 이름
  • 인터페이스 코드
  • 메소드의 바이트 코드
  • 런타임 상수 풀
  • 필드와 메서드 데이터
  • 생성자
  • 메소드 정보

 

 이러한 메타데이터 중에서 메소드 정보 부분은 해당 클래스에 정의된 모든 메소드에 대한 메타데이터를 포함하며, 이는 vtable을 구성하는 데 사용됩니다.

 클래스가 로드되고 링크 과정을 거치면서 생성되는 이런 정보들은 JVM의 메서드 영역에 할당됩니다. 이 메서드 영역은 JVM의 가비지 컬렉터의 관리 대상이기도 합니다.


메서드 영역은 실제 물리적 위치는 JVM 구현체나 실행되는 시스템의 구조에 따라 다릅니다. 일반적으로, JVM은 운영체제에서 실행되며, 메서드 영역은 운영체제로부터 할당받은 메모리 영역에 위치합니다.

 

Q) 자바의 객체 메소드는 메소드 코드를 참조하고 있나요?

A) 그렇습니다. 자바에서 객체의 메소드는 실제로 메소드 코드를 가리키는 메소드 영역에 저장됩니다.


자바에서 클래스는 메소드를 정의하고, 객체는 클래스의 인스턴스입니다. 클래스의 메소드는 해당 클래스의 행위나 동작을 정의하며, 메소드는 클래스의 멤버로 선언됩니다. 객체는 이러한 메소드를 호출하여 원하는 작업을 수행할 수 있습니다. 메소드 코드는 클래스의 정의 부분에서 작성되며, 해당 코드는 메소드 영역에 저장됩니다. 


실제로 객체를 생성하고 메소드를 호출할 때, 메소드의 코드가 메모리의 힙 영역에 위치한 객체의 인스턴스와 연결되어 실행됩니다. 객체의 인스턴스는 힙 영역에 할당되며, 메소드의 코드는 이 인스턴스와 관련된 데이터에 접근할 수 있습니다. 메소드는 인스턴스의 상태를 변경하거나, 인스턴스의 데이터를 읽어서 원하는 동작을 수행합니다.


따라서 자바에서 객체의 메소드는 실제로 메소드 코드를 가리키는 것으로 이해할 수 있습니다. 이를 통해 객체의 메소드가 실행되면, 해당 메소드의 코드가 실제로 실행되어 원하는 작업을 수행하게 됩니다.

 

※ 자바에서 객체의 메소드가 메소드 영역의 메소드 코드를 가리키는 방식은 내부적으로는 메소드 테이블(vtable:virtual method table)을 사용하여 구현됩니다.


메소드 테이블은 클래스에 정의된 모든 메소드의 주소(메소드 코드를 가리키는 포인터)를 저장하는 테이블입니다. 이 테이블은 상속 관계에 있는 클래스들 간에 공유되며, 각 클래스의 메소드에 대한 정보를 담고 있습니다.


객체의 인스턴스는 해당 클래스의 메소드 테이블을 가리키는 포인터를 가지고 있습니다. 이 포인터는 객체가 어떤 클래스의 인스턴스인지를 가리키고, 해당 클래스의 메소드 테이블을 참조할 수 있게 합니다. 따라서 객체가 메소드를 호출할 때, 해당 메소드의 코드를 직접 가리키는 것이 아니라, 메소드 테이블을 통해 해당 메소드의 주소를 찾아서 실행합니다.


이 방식은 다형성(polymorphism)을 지원하기 위한 메커니즘입니다. 상위 클래스 타입의 변수로 하위 클래스의 객체를 참조할 수 있는데, 이 경우 변수의 타입에 따라 호출되는 메소드가 달라지게 됩니다. 메소드 테이블은 이러한 동적 디스패치(dynamic dispatch)를 가능하게 하여 실행 시에 적절한 메소드를 호출할 수 있도록 합니다.


요약하자면, 자바에서 객체의 메소드는 메소드 테이블을 통해 메소드 영역의 메소드 코드를 가리키게 됩니다. 이를 통해 객체의 타입에 따라 적절한 메소드가 실행되며, 다형성과 동적 디스패치를 지원합니다.

 

2. 힙 영역

 JVM의 힙 영역은 OS가 프로세스에 할당한 메모리 공간에서 생성됩니다. 
JVM의 힙 영역은 자바 애플리케이션에서 사용하는 모든 객체와 배열이 할당되는 공간입니다. JVM이 시작될 때 생성되며, 가비지 컬렉션에 의해 자동으로 관리됩니다. 이 힙 영역의 크기는 시작 시에 정의되지만, 프로그램 실행 중에 필요에 따라 늘어나거나 줄어들 수 있습니다. 


운영 체제는 JVM이 시작될 때 일정량의 메모리를 할당하며, 이 중 일부는 JVM의 힙 영역에 할당됩니다. 힙 영역의 메모리는 더 이상 참조되지 않는 객체를 가비지 컬렉터가 정리함으로써 재사용할 수 있습니다. 


그러나 JVM의 힙 영역이 모두 사용되고, 더 이상 가비지 컬렉션을 통해 충분한 메모리를 회수할 수 없는 경우, OutOfMemoryError가 발생하여 애플리케이션 실행이 중단될 수 있습니다. 이는 JVM이 사용할 수 있는 메모리가 OS의 힙 메모리를 모두 소진한 것을 의미합니다.

 

3. 자바 스택

JVM의 자바 스택 영역은 특정 스레드가 메서드를 실행하는 동안 사용되는 데이터를 저장하는 메모리 영역입니다. 각 스레드는 자신만의 독립적인 자바 스택을 가지며, 이 영역은 스레드가 생성될 때 함께 생성됩니다. 


스택은 LIFO(Last-In-First-Out, 후입선출) 방식으로 데이터를 저장합니다. 스레드가 메서드를 호출할 때마다 그 메서드를 위한 새로운 스택 프레임이 스택의 상단에 푸시(push)되고, 해당 메서드의 실행이 완료되면 그 스택 프레임은 팝(pop)되어 제거됩니다.


각 스택 프레임은 다음과 같은 세부 정보를 포함합니다:

  1. 로컬 변수 배열: 메서드 내에서 선언된 모든 변수가 이 배열에 저장됩니다. 각 변수는 인덱스를 통해 접근할 수 있으며, 메서드의 실행이 종료되면 해당 메서드의 로컬 변수도 함께 사라집니다.
  2. 연산 스택: 이는 메서드가 실행되는 동안 일시적인 계산 결과를 보관하는 데 사용됩니다. 예를 들어, 두 개의 변수를 더하려면 두 변수를 스택에 푸시하고, 두 값을 팝하여 더한 다음 결과를 다시 스택에 푸시합니다.
  3. 프레임 데이터: 이는 메서드가 반환될 때 제어가 전달되는 위치(return address)나 "동적 링크(dynamic link)"와 같은 추가 정보가 포함됩니다. 동적 링크는 이전 스택 프레임에 대한 참조입니다.

자바 스택은 메서드 호출과 메서드 실행에 필요한 메모리를 관리하는데 사용되며, 각 스택 프레임의 생명주기는 해당 메서드의 생명주기와 일치합니다. 메서드가 종료되면 해당 스택 프레임도 스택에서 제거되므로, 스택에서 메모리를 명시적으로 해제할 필요가 없습니다. 이는 가비지 컬렉션과는 별개의 자동 메모리 관리 메커니즘입니다.


자바 스택에서 가장 흔히 발생하는 오류는 StackOverflowError로, 재귀호출과 같이 메서드 호출이 너무 많이 중첩되어 스택 메모리를 초과할 때 발생합니다.

 

4. PC 레지스터

PC 레지스터(Program Counter Register)는 JVM의 메모리 구조 중 하나로, 각 스레드가 생성될 때마다 생성되는 스레드 당 프라이빗 메모리 영역입니다.


PC 레지스터는 현재 실행 중인 JVM 명령의 주소를 저장하며, 스레드가 어떤 명령을 실행해야 하는지를 결정하는 데 중요한 역할을 합니다. 즉, 이 레지스터는 현재 실행되는 명령어의 위치를 가리킵니다.


프로그램 카운터의 값은 명령어가 순차적으로 실행되는 동안 증가하며, 제어 흐름 명령어(예: 조건부 브랜치, 루프, 메서드 호출 등)가 실행될 때마다 업데이트됩니다. 


자바에서는 스레드 간에 문맥 교환(context switching)이 발생할 때, PC 레지스터가 중요한 역할을 합니다. 문맥 교환은 실행 중인 스레드가 다른 스레드로 변경되는 과정을 의미하는데, 이때 PC 레지스터의 값이 각 스레드의 실행 상태를 저장하고 복원하는데 사용됩니다. 즉, 스레드가 재개될 때 정확히 어디에서 실행을 계속해야 하는지를 알 수 있습니다.


PC 레지스터는 메서드가 네이티브 메서드인 경우에는 undefined 값을 가질 수 있습니다. 네이티브 메서드는 JVM 외부에서 실행되므로, 이 경우 JVM은 메서드의 다음 명령을 가리킬 필요가 없기 때문입니다.

 

※JVM의 PC 레지스터(Program Counter Register)는 JVM이 관리하는 가상 메모리 영역이며, 실제 하드웨어 CPU 레지스터와는 별개입니다. JVM은 Java 코드를 바이트코드로 컴파일하고, 이 바이트코드를 자체적인 가상 머신 환경에서 실행합니다.


JVM의 PC 레지스터는 현재 스레드가 실행 중인 바이트코드 명령의 주소를 저장합니다. CPU의 PC 레지스터와 비슷한 역할을 하지만, 이는 JVM 내에서 독립적으로 관리되는 것입니다.


실제 인텔 CPU에서는 "instruction pointer" 또는 "program counter"라는 이름의 레지스터가 있습니다. 이 레지스터는 CPU가 현재 실행 중인 명령어의 주소를 가리킵니다. 그러나 이것은 실제 하드웨어 레벨에서 동작하는 것으로, JVM의 PC 레지스터와는 직접적인 연관이 없습니다.


따라서, JVM이 인텔 CPU에서 실행되더라도, JVM의 PC 레지스터는 JVM이 직접 관리하는 가상 메모리 영역에 위치합니다. 이는 JVM이 자바 코드를 실행하는 동안 각 스레드의 실행 상태를 추적하고 관리하는 데 필요한 정보를 제공합니다.

 

5. 네이티브 메소드 스택

네이티브 메소드 스택은 JVM의 메모리 구조 중 하나로, 자바 외부에서 작성된 네이티브 메소드를 실행하는 데 필요한 메모리를 제공합니다. 이 메모리 영역은 각 스레드에 대해 별도로 생성되며, 각 스레드는 자신만의 독립적인 네이티브 메소드 스택을 가집니다.


네이티브 메소드란, 주로 C나 C++ 등의 다른 프로그래밍 언어로 작성된 메소드를 의미합니다. 이런 메소드는 자바 네이티브 인터페이스(Java Native Interface, JNI)를 통해 자바 애플리케이션에서 호출될 수 있습니다. 네이티브 메소드는 종종 운영 체제 API에 직접 접근하거나, 하드웨어를 직접 제어하는 등의 작업을 수행하는 데 사용됩니다.


네이티브 메소드 스택은, 네이티브 메소드가 실행되는 동안 필요한 변수나 중간 결과 등을 저장하는 데 사용됩니다. 이 스택은 각 스레드가 네이티브 메소드를 호출할 때마다 생성되며, 메소드의 실행이 완료되면 스택은 제거됩니다.


네이티브 메소드 스택의 크기는 JVM 구현체와 운영 체제에 따라 다릅니다. 일부 JVM은 이 영역의 크기를 사용자가 조정할 수 있게 해주지만, 다른 JVM에서는 그렇지 않을 수도 있습니다. 또한, 네이티브 메소드 스택의 사용 방법은 사용하는 네이티브 프로그래밍 언어와 그 언어의 런타임에 따라 달라질 수 있습니다.


자바 스택과 마찬가지로, 네이티브 메소드 스택에서도 너무 많은 메소드 호출로 인해 스택 오버플로우가 발생할 수 있습니다. 이 경우, JVM은 `StackOverflowError`를 발생시킵니다. 또한, 충분한 메모리가 없어 스택을 생성할 수 없는 경우에는 `OutOfMemoryError`가 발생할 수 있습니다.

 

6. 자바 네이티브 인터페이스(Java Native Interface, JNI)

 JNI(Java Native Interface)은 자바 언어와 다른 네이티브 언어(예: C, C++) 간의 상호 운용성을 제공하기 위한 인터페이스입니다. JNI를 사용하면 자바 언어로 작성된 애플리케이션에서 네이티브 코드를 호출하거나 네이티브 코드에서 자바 언어의 기능을 사용할 수 있습니다.

 JNI를 통해 자바와 네이티브 코드 간의 상호 작용이 가능해집니다. 이는 자바 언어로 작성된 애플리케이션이 특정 네이티브 라이브러리를 사용하거나, 네이티브 성능이 필요한 부분을 C나 C++로 구현하여 호출하는 등의 상황에서 유용합니다.

  JNI를 사용하여 자바와 네이티브 코드 간의 통신을 수행하려면 몇 가지 단계를 거쳐야 합니다. 먼저, 네이티브 코드를 작성하고 컴파일하여 공유 라이브러리(예: DLL 파일, so 파일) 형태로 만들어야 합니다. 그런 다음, 자바에서 JNI를 사용하여 네이티브 코드를 호출하거나 네이티브 코드에서 자바 메소드를 호출할 수 있습니다. 이를 위해 JNI는 네이티브 메소드의 선언과 정의를 자바 코드에서 작성하고, 네이티브 메소드를 로드하고 호출하는 기능을 제공합니다.

 JNI는 플랫폼에 종속적인 기술이므로, JNI를 사용하는 경우에는 해당 플랫폼에 맞는 네이티브 코드를 작성해야 합니다. 또한, JNI는 성능 저하를 가져올 수 있으므로 네이티브 코드를 사용해야 하는 경우에만 사용하는 것이 권장됩니다.

 JNI는 자바 플랫폼의 확장성과 유연성을 높여주는 중요한 도구로 사용됩니다. 주로 시스템 레벨의 작업이 필요한 경우나 네이티브 라이브러리를 활용해야 하는 경우에 활용됩니다.

 

7. 네이티브 메소드(Native Method)

네이티브 메소드(Native Method)란, 자바 언어에서 자바 가상 머신(JVM) 외부에서 구현된 메소드(일반적으로 C/C++ 함수)를 말합니다. 즉, 자바 언어로 작성된 프로그램이 네이티브 코드(일반적으로 C, C++ 등의 언어로 작성된 코드)를 호출하는데 사용되는 메소드입니다.

네이티브 메소드는 자바 언어로는 구현하기 어려운 시스템 수준의 작업이나 특정 하드웨어와의 상호작용, 기존 C/C++ 라이브러리의 활용 등을 목적으로 사용됩니다. 네이티브 메소드는 자바 언어로 작성된 프로그램이 네이티브 코드를 호출하거나 네이티브 코드에서 자바 언어의 기능을 사용할 수 있도록 해줍니다.

네이티브 메소드는 자바 소스 코드에서 선언되며, 메소드의 정의는 네이티브 코드로 작성됩니다. 네이티브 메소드는 "native" 키워드를 사용하여 선언되고, 중괄호({})로 메소드의 본문이 비어 있음을 나타냅니다. 예를 들면 다음과 같은 형태입니다:

public native void myNativeMethod();

네이티브 메소드를 사용하기 위해서는 해당 메소드를 정의한 클래스에서 JNI(Java Native Interface)를 활용하여 네이티브 코드와 자바 코드 간의 통신을 설정해야 합니다. 네이티브 메소드는 네이티브 코드가 컴파일되어 공유 라이브러리 형태로 제공되어야 하며, 자바에서는 해당 라이브러리를 로드하여 사용할 수 있습니다.

네이티브 메소드는 주로 시스템 레벨의 작업이 필요한 경우나 성능 향상을 위해 네이티브 코드를 사용해야 하는 경우에 활용됩니다. 그러나 네이티브 메소드는 자바의 가독성, 이식성, 안전성 등을 저해할 수 있으므로 신중하게 사용해야 합니다.

 

1) 네이티브 메소드(Native Method) : OS GUI Applicaiton Programming Interface

자바 메소드

자바에서는 AWT(Abstract Window Toolkit)와 Swing, 또는 JavaFX와 같은 라이브러리를 사용하여 GUI 애플리케이션을 만들 수 있습니다. 이들 라이브러리는 내부적으로 네이티브 메소드를 사용하여 운영 체제의 GUI 기능에 접근합니다. 이 네이티브 메소드들은 JNI(Java Native Interface)를 통해 자바 코드와 통신하며, 실제로는 C/C++로 작성된 코드를 실행합니다.

이렇게 구현된 네이티브 메소드는 창을 생성하거나, 그림을 그리거나, 사용자의 입력을 받아들이는 등의 작업을 수행합니다. 이들은 운영 체제의 GUI API를 호출하여, 사용자의 행동에 반응하거나 화면에 요소를 그리는 등의 작업을 수행합니다.

 

다음은 Java에서 GUI를 작성하기 위한 간단한 예제 코드입니다. 이 코드는 JFrame 클래스를 사용하여 윈도우를 생성하고, JButton 클래스를 사용하여 버튼을 추가하는 예제입니다.

import javax.swing.JButton;
import javax.swing.JFrame;

public class GUIExample {
    public static void main(String[] args) {
        // 윈도우 생성
        JFrame frame = new JFrame("Sample Window");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(300, 200);

        // 버튼 생성
        JButton button = new JButton("Click Me!");
        button.setBounds(100, 70, 80, 30);

        // 버튼 이벤트 처리
        button.addActionListener(e -> {
            System.out.println("Button clicked!");
        });

        // 버튼을 윈도우에 추가
        frame.add(button);

        // 윈도우 표시
        frame.setLayout(null);
        frame.setVisible(true);
    }
}

위의 예제 코드는 JFrame 클래스를 사용하여 윈도우를 생성하고, JButton 클래스를 사용하여 버튼을 추가합니다. 버튼 클릭 이벤트를 처리하기 위해 addActionListener 메소드를 사용하고, 람다식을 이용하여 버튼이 클릭되었을 때 실행될 코드를 작성합니다.

이 코드를 실행하면 윈도우가 표시되고 "Click Me!"라는 버튼이 생성됩니다. 버튼을 클릭하면 "Button clicked!"이라는 메시지가 콘솔에 출력됩니다.

 

리눅스 : X Window System

X Window System(또는 일반적으로 "X11" 또는 "X"로 불리움)은 리눅스와 유닉스 계열의 운영 체제에서 사용되는 그래픽 사용자 인터페이스(GUI) 표준입니다. 이는 비트맵 디스플레이에 그래픽을 렌더링하는데 사용되는 프로토콜 및 관련 라이브러리 집합으로, 클라이언트/서버 모델을 사용하여 동작합니다.

X Window System은 실질적으로 API를 제공하긴 하지만, 일반적으로 운영 체제의 핵심 API(예: 리눅스 시스템 호출)와는 별도로 취급됩니다. 이는 X Window System이 그 자체로 복잡한 시스템이며, 특히 그래픽 하드웨어와 상호 작용하는 방법에 대한 자체적인 모델을 가지고 있기 때문입니다.

그러나, X Window System은 그래픽 사용자 인터페이스를 만드는데 필요한 다양한 함수와 프로토콜을 제공합니다. 이런 의미에서, X Window System은 리눅스와 같은 유닉스 계열 운영 체제에서 GUI 애플리케이션을 만드는 데 필요한 중요한 API로 볼 수 있습니다.

더불어 X Window System은 하드웨어에 직접적으로 접근하는 것이 아니라, 대부분의 경우에는 리눅스 커널이 제공하는 시스템 호출을 통해 그래픽 장치에 접근합니다. 따라서 X Window System은 리눅스 커널의 API 위에서 작동하는 또 다른 레이어로 볼 수 있습니다.

따라서, 리눅스에서 GUI와 관련된 네이티브 메소드는 X Window System이나 Wayland를 사용하여 창을 생성하거나 그래픽을 그리는 등의 작업을 수행하는 것이라고 말할 수 있습니다. 이러한 네이티브 메소드의 실행에 필요한 메모리는 JVM의 네이티브 메소드 스택에 할당됩니다.

 

다음은 X Window System API를 사용하여 단순한 창을 생성하고 텍스트를 표시하는 예제 코드입니다. 이 코드는 C 언어로 작성되었습니다.

#include <X11/Xlib.h>

int main() {
    Display *display;
    Window window;
    XEvent event;
    const char *message = "Hello, X Window System!";
    int screen;

    /* X 서버와 연결하기 */
    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "Cannot connect to X server.\n");
        return 1;
    }

    screen = DefaultScreen(display);

    /* 윈도우 생성 */
    window = XCreateSimpleWindow(display, RootWindow(display, screen),
                                 10, 10, 200, 100, 1,
                                 BlackPixel(display, screen),
                                 WhitePixel(display, screen));

    /* 윈도우 맵핑 */
    XMapWindow(display, window);

    /* 이벤트 루프 */
    while (1) {
        XNextEvent(display, &event);
        if (event.type == Expose) {
            /* 텍스트를 윈도우에 표시 */
            XDrawString(display, window, DefaultGC(display, screen),
                        50, 50, message, strlen(message));
        }
        else if (event.type == KeyPress)
            break;
    }

    /* 연결 해제 */
    XCloseDisplay(display);

    return 0;
}

이 코드는 Xlib 라이브러리를 사용하여 X 서버와 연결하고, 윈도우를 생성하며, 이벤트를 처리하는 간단한 기능을 제공합니다. 윈도우가 생성되면 "Hello, X Window System!" 메시지가 윈도우에 표시됩니다. 이 코드는 키보드 이벤트가 발생하면 프로그램이 종료됩니다.

 

Windows : GUI32.DLL

gui32.dll은 Microsoft Windows 운영 체제에서 사용되는 DLL(Dynamic Link Library) 파일입니다. 이 파일은 Windows API를 기반으로 한 그래픽 사용자 인터페이스(GUI) 기능을 제공하는 라이브러리입니다. gui32.dll은 창 생성, 버튼, 메뉴, 대화 상자 등과 같은 UI 요소를 생성하고 관리하는 데 사용됩니다.

gui32.dll은 C/C++ 또는 다른 언어로 작성된 Windows 애플리케이션에서 사용될 수 있으며, 해당 DLL 파일을 로드하여 그래픽 인터페이스를 구현할 수 있습니다. 주로 Windows API 함수들과 함께 사용되며, 개발자가 그래픽 요소를 만들고 조작할 수 있도록 도와줍니다.

다음은 gui32.dll을 사용하여 간단한 윈도우와 버튼을 생성하는 예제 코드입니다. 이 코드는 C 언어로 작성되었습니다.

#include <stdio.h>
#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    const char className[] = "Sample Window";
    const char windowTitle[] = "Sample Window with Button";
    
    WNDCLASS wc = { 0 };
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszClassName = className;
    
    if (!RegisterClass(&wc)) {
        MessageBox(NULL, "Window registration failed.", "Error", MB_ICONERROR);
        return 1;
    }
    
    HWND hwnd = CreateWindow(className, windowTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 300, 200, NULL, NULL, hInstance, NULL);
    if (hwnd == NULL) {
        MessageBox(NULL, "Window creation failed.", "Error", MB_ICONERROR);
        return 1;
    }
    
    HWND button = CreateWindow("BUTTON", "Click Me!", WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON, 100, 70, 80, 30, hwnd, (HMENU)1, hInstance, NULL);
    
    ShowWindow(hwnd, nCmdShow);
    
    MSG msg = { 0 };
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    
    return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_COMMAND:
            if (LOWORD(wParam) == 1) {
                MessageBox(hwnd, "Button clicked!", "Information", MB_OK);
            }
            break;
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
    }
    
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}


위의 예제 코드는 Windows API 함수와 gui32.dll을 사용하여 윈도우와 버튼을 생성합니다. WinMain 함수에서는 윈도우 클래스를 등록하고, 윈도우와 버튼을 생성한 후 메시지 루프를 실행합니다. WindowProc 함수는 윈도우 프로시저로서 메시지를 처리하고, 버튼이 클릭되었을 때 메시지 상자를 표시합니다.

이 코드를 컴파일하고 실행하면 윈도우와 "Click Me!"라는 버튼이 있는 간단한 애플리케이션을 볼 수 있습니다. 버튼을 클릭하면 메시지 상자가 표시됩니다.

 

 

2) 네이티브 메소드(Native Method) : File Input/Output

자바의 파일 입출력(File I/O) 메소드는 내부적으로 네이티브 메소드를 사용하여 구현되어 있습니다. 이러한 네이티브 메소드들은 자바의 파일 및 디렉터리 작업을 운영 체제 수준에서 처리하기 위해 사용됩니다.
자바의 파일 입출력을 지원하기 위해 사용되는 일부 네이티브 메소드들은 다음과 같습니다:
1. FileInputStream과 FileOutputStream 클래스의 네이티브 메소드:

  • open(): 파일을 엽니다.
  • read(): 파일에서 데이터를 읽습니다.
  • write(): 파일에 데이터를 씁니다.
  • close(): 파일을 닫습니다.

2. RandomAccessFile 클래스의 네이티브 메소드:

  • read(): 파일에서 데이터를 읽습니다.
  • write(): 파일에 데이터를 씁니다.
  • seek(): 파일 내에서 지정된 위치로 이동합니다.

3. FileDescriptor 클래스의 네이티브 메소드:

  • sync(): 파일의 변경 내용을 디스크에 동기화합니다.
  • valid(): 파일 디스크립터의 유효성을 확인합니다.

다음은 Java에서 파일 입출력을 수행하는 예제 코드입니다. 이 코드는 `FileOutputStream` 클래스를 사용하여 파일을 생성하고 데이터를 쓰는 예제입니다.

import java.io.FileOutputStream;
import java.io.IOException;

public class FileIOExample {
    public static void main(String[] args) {
        String filename = "sample.txt";
        String data = "Hello, File!";
        
        try (FileOutputStream fos = new FileOutputStream(filename)) {
            byte[] bytes = data.getBytes();
            fos.write(bytes);
            
            System.out.println("Data written to file: " + bytes.length + " bytes");
        } catch (IOException e) {
            System.out.println("Failed to write to file: " + e.getMessage());
        }
    }
}


위의 예제 코드는 `FileOutputStream` 클래스를 사용하여 파일을 생성하고 데이터를 씁니다. `FileOutputStream` 객체를 사용하여 파일을 열고, `write` 메소드를 호출하여 데이터를 파일에 씁니다. `getBytes` 메소드를 사용하여 문자열을 바이트 배열로 변환한 후 파일에 씁니다.

이 코드를 실행하면 "sample.txt"라는 이름의 파일이 생성되고, 파일에 "Hello, File!" 문자열이 기록됩니다. 파일 쓰기 도중에 예외가 발생하면 예외 메시지가 출력됩니다.

 

리눅스

리눅스에서 C 언어를 사용하여 파일 입출력을 수행하는 예제를 제공해 드리겠습니다. 아래의 코드는 파일을 읽고 쓰는 기본적인 파일 입출력을 수행하는 C 프로그램입니다.

#include <stdio.h>

int main() {
    FILE *file;
    char line[100];

    // 파일 읽기
    file = fopen("/path/to/file.txt", "r");
    if (file == NULL) {
        perror("Error opening file");
        return -1;
    }

    while (fgets(line, sizeof(line), file) != NULL) {
        printf("%s", line);
    }

    fclose(file);

    // 파일 쓰기
    file = fopen("/path/to/file.txt", "a");
    if (file == NULL) {
        perror("Error opening file");
        return -1;
    }

    fputs("This is a new line.\n", file);

    fclose(file);

    return 0;
}

위의 코드에서 `/path/to/file.txt` 부분은 실제 파일 경로로 변경해야 합니다. 파일을 읽을 때는 `fopen` 함수를 "r" 모드로 호출하고, 파일에 쓸 때는 "a" 모드로 호출합니다. `fgets` 함수를 사용하여 파일에서 한 줄씩 읽어오고, `fputs` 함수를 사용하여 파일에 새로운 줄을 씁니다. 파일을 올바르게 열었는지 확인하기 위해 `fopen` 함수의 반환값을 검사합니다. 파일을 사용한 후에는 `fclose` 함수를 사용하여 파일을 닫아야 합니다.

이 코드는 파일을 읽고 쓰는 기본적인 예제이며, 실제로는 파일을 열기 전에 존재 여부를 확인하거나 오류 처리를 추가하는 등의 작업을 할 수 있습니다. 필요에 따라 코드를 확장하거나 수정하여 원하는 파일 입출력 동작을 수행할 수 있습니다.

 

Windows

윈도우즈 운영 체제에서 파일 입출력을 처리하는 네이티브 메소드는 core32.dll이나 kernel32.dll과 같은 윈도우즈 시스템 라이브러리에 정의되어 있습니다.


윈도우즈 파일 입출력과 관련된 일부 네이티브 메소드는 다음과 같습니다:
1. CreateFile(): 파일을 생성하거나 열기 위한 기능을 제공합니다.
2. ReadFile(), WriteFile(): 파일에서 데이터를 읽거나 쓰는 기능을 제공합니다.
3. CloseHandle(): 파일 핸들을 닫습니다.
4. DeleteFile(): 파일을 삭제합니다.
5. MoveFile(): 파일을 이동하거나 이름을 변경합니다.
6. GetFileSize(): 파일의 크기를 가져옵니다.

위의 메소드들은 kernel32.dll에 정의되어 있으며, 자바에서는 이러한 네이티브 메소드를 호출하여 윈도우즈 파일 입출력을 수행합니다. 이를 통해 자바 프로그램은 윈도우즈 운영 체제의 파일 시스템과 상호 작용할 수 있습니다.

 

다음은 kernel32.dll의 API를 사용하여 파일 입출력을 수행하는 예제 코드입니다. 이 코드는 C 언어로 작성되었습니다.

#include <stdio.h>
#include <windows.h>

int main() {
    HANDLE hFile;
    DWORD dwBytesWritten;
    char buffer[] = "Hello, File!";

    // 파일 열기
    hFile = CreateFile("sample.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("Failed to create file.\n");
        return 1;
    }

    // 파일에 데이터 쓰기
    if (!WriteFile(hFile, buffer, sizeof(buffer) - 1, &dwBytesWritten, NULL)) {
        printf("Failed to write to file.\n");
        CloseHandle(hFile);
        return 1;
    }

    printf("Data written to file: %d bytes\n", dwBytesWritten);

    // 파일 닫기
    CloseHandle(hFile);

    return 0;
}

위의 코드는 CreateFile 함수로 파일을 생성하고, WriteFile 함수로 데이터를 파일에 씁니다. CreateFile 함수는 파일 핸들을 반환하며, WriteFile 함수는 파일에 데이터를 씁니다. 이 예제에서는 "Hello, File!" 문자열을 파일에 씁니다.

컴파일하여 실행하면 "sample.txt"라는 이름의 파일이 생성되고, 파일에 "Hello, File!" 문자열이 기록됩니다.

 

이러한 네이티브 메소드들은 자바의 파일 입출력 클래스들에서 사용되며, 실제로는 운영 체제의 파일 시스템과 상호 작용하여 파일과 데이터를 읽고 쓰는 작업을 수행합니다. 이를 통해 자바 프로그램은 운영 체제와 상관없이 파일 입출력 작업을 수행할 수 있습니다.

 

3) 네이티브 메소드(Native Method) : Network 통신

자바에서 네트워크 통신을 위해 사용되는 메소드들은 `java.net` 패키지에 포함되어 있습니다. 이 패키지는 네트워크 관련 기능을 제공하는 클래스와 인터페이스를 포함하고 있습니다. 주요한 네트워크 메소드들은 다음과 같습니다:

1. Socket 및 ServerSocket 클래스의 메소드:

  • Socket(): 소켓을 생성합니다.
  • bind(): 소켓을 특정 포트에 바인딩합니다.
  • connect(): 소켓을 원격 호스트에 연결합니다.
  • getInputStream(): 소켓으로부터 입력 스트림을 가져옵니다.
  • getOutputStream(): 소켓으로부터 출력 스트림을 가져옵니다.
  • close(): 소켓을 닫습니다.

2. URL 및 HttpURLConnection 클래스의 메소드:

  • openConnection(): URL에 대한 연결을 엽니다.
  • getInputStream(): 연결된 URL로부터 입력 스트림을 가져옵니다.
  • getOutputStream(): 연결된 URL로부터 출력 스트림을 가져옵니다.
  • connect(): URL 연결을 수행합니다.
  • disconnect(): URL 연결을 닫습니다.

3. DatagramSocket 및 DatagramPacket 클래스의 메소드:

  • DatagramSocket(): Datagram 소켓을 생성합니다.
  • send(): Datagram을 송신합니다.
  • receive(): Datagram을 수신합니다.
  • close(): Datagram 소켓을 닫습니다.

4. InetAddress 클래스의 메소드:

  • getByName(): 호스트 이름에 대한 IP 주소를 가져옵니다.

위의 메소드들은 자바에서 네트워크 통신을 수행하기 위해 사용됩니다. 이를 통해 소켓 통신, HTTP 요청, UDP 통신 등 다양한 네트워크 작업을 수행할 수 있습니다. 이러한 메소드들은 자바 언어 수준에서 네트워크 기능을 제공하며, 플랫폼 독립적인 방식으로 동작합니다.

 

다음은 Java에서 TCP 통신을 수행하는 예제 코드입니다. 이 코드는 클라이언트와 서버 간의 간단한 텍스트 기반 메시지 교환을 보여줍니다.

클라이언트:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

public class TCPClient {
    public static void main(String[] args) {
        String serverIP = "localhost";
        int serverPort = 12345;

        try (Socket socket = new Socket(serverIP, serverPort);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
             BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in))) {

            System.out.println("Connected to the server.");

            String message;
            while ((message = userInput.readLine()) != null) {
                out.println(message);

                if (message.equalsIgnoreCase("exit")) {
                    break;
                }

                String response = in.readLine();
                System.out.println("Server response: " + response);
            }
        } catch (IOException e) {
            System.out.println("Client Error: " + e.getMessage());
        }
    }
}

서버:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) {
        int serverPort = 12345;

        try (ServerSocket serverSocket = new ServerSocket(serverPort);
             Socket clientSocket = serverSocket.accept();
             PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {

            System.out.println("Client connected.");

            String message;
            while ((message = in.readLine()) != null) {
                System.out.println("Received message from client: " + message);

                if (message.equalsIgnoreCase("exit")) {
                    break;
                }

                String response = "Server: " + message;
                out.println(response);
            }
        } catch (IOException e) {
            System.out.println("Server Error: " + e.getMessage());
        }
    }
}


위의 예제 코드는 클라이언트와 서버 간에 TCP 소켓을 통해 텍스트 기반 메시지를 주고받습니다. 클라이언트는 서버에 연결한 후 사용자의 입력을 읽어 서버로 전송하고, 서버는 클라이언트로부터 메시지를 수신하고 다시 응답을 보냅니다. "exit"라는 메시지를 보내면 통신이 종료됩니다.

클라이언트는 `Socket`을 사용하여 서버에 연결하고, `PrintWriter`와 `BufferedReader`를 사용하여 데이터를 전송하고 수신합니다. 서버는 `ServerSocket`을 사용하여 클라이언트의 연결을 수락하고, 클라이언트와의 통신을 위해 `Socket`, `PrintWriter`, `BufferedReader`를 사용합니다.

클라이언트와 서버는 동일한 IP 주소와 포트 번호를 사용하여 통신합니다. 예제 코드에서는 `localhost`를 사용하여 동일한 컴퓨터에서 테스트할 수 있습니다

 

Socket

소켓(Socket)은 운영 체제(OS)에서 제공하는 네트워크 통신 인터페이스입니다. 소켓은 애플리케이션 간의 네트워크 통신을 가능하게 해주는 추상화된 개념입니다. 소켓을 사용하여 애플리케이션은 네트워크를 통해 데이터를 송수신하고, 다른 컴퓨터나 서버와 통신할 수 있습니다. 즉 소켓 인터페이스는 네트워크 통신을 추상화하고 데이터의 송수신을 담당하는 중요한 개념입니다.

일반적으로 소켓은 IP(Internet Protocol) 주소와 포트 번호로 식별됩니다. IP 주소는 네트워크 상의 장치(컴퓨터, 서버 등)를 고유하게 식별하는 번호이며, 포트 번호는 해당 장치 내에서 특정 프로세스를 식별하는 번호입니다.

소켓은 네트워크 프로토콜과 함께 사용됩니다. TCP(Transmission Control Protocol)와 UDP(User Datagram Protocol)는 가장 일반적으로 사용되는 프로토콜 중 두 가지입니다. TCP 소켓은 신뢰성 있는 연결 지향 통신을 제공하고, UDP 소켓은 비연결성 및 신뢰성이 낮은 데이터 그램 통신을 제공합니다.

애플리케이션은 소켓을 생성하여 네트워크 통신을 시작할 수 있습니다. 소켓을 생성하면 운영 체제는 해당 소켓을 식별하고, 애플리케이션이 소켓을 통해 데이터를 송수신할 수 있는 인터페이스를 제공합니다. 소켓은 주로 송신용 소켓과 수신용 소켓 두 가지로 구분됩니다.

  • 송신용 소켓: 데이터를 보내는 쪽에서 사용하는 소켓입니다. 애플리케이션은 송신용 소켓을 통해 데이터를 보내고, 네트워크를 통해 수신측에 도달합니다.
  • 수신용 소켓: 데이터를 받는 쪽에서 사용하는 소켓입니다. 애플리케이션은 수신용 소켓을 통해 데이터를 수신하고, 네트워크로부터 송신측으로부터 도착한 데이터를 받습니다.

애플리케이션은 소켓을 통해 데이터를 송수신하기 위해 소켓에 다양한 작업을 수행할 수 있습니다. 주요 작업은 다음과 같습니다.

  1. 소켓 바인딩(Binding): 소켓을 특정 IP 주소와 포트 번호에 바인딩합니다. 이를 통해 소켓은 해당 IP 주소와 포트 번호로 식별됩니다.
  2. 연결 요청(Listen): TCP 소켓에서 사용되며, 다른 소켓으로의 연결 요청을 수신할 준비를 합니다.
  3. 연결 수락(Accept): TCP 소켓에서 사용되며, 연결 요청을 수락하고 신규 연결을 생성합니다.
  4. 연결 요청(Connect): TCP 소켓에서 사용되며, 다른 소켓과의 연결을 시도합니다.
  5. 데이터 송수신(Read/Write): 소켓을 통해 데이터를 읽거나 쓸 수 있습니다. 읽기 작업은 소켓으로부터 데이터를 수신하고, 쓰기 작업은 소켓을 통해 데이터를 전송합니다.
  6. 소켓 닫기(Close): 소켓 연결을 종료하고, 소켓 자원을 해제합니다.

소켓은 네트워크 프로그래밍에서 중요한 개념이며, 다양한 프로그래밍 언어와 라이브러리에서 소켓을 지원하고 있습니다. 소켓을 통해 애플리케이션은 네트워크를 통해 데이터를 안전하고 신속하게 전송할 수 있습니다.

 

server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define SERVER_PORT 12345

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_address, client_address;
    char buffer[1024];

    // 서버 소켓 생성
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("Failed to create socket");
        exit(EXIT_FAILURE);
    }

    // 서버 주소 설정
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(SERVER_PORT);
    server_address.sin_addr.s_addr = INADDR_ANY;

    // 서버 소켓을 주소에 바인딩
    if (bind(server_socket, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
        perror("Binding failed");
        exit(EXIT_FAILURE);
    }

    // 클라이언트로부터의 연결 요청을 대기
    listen(server_socket, 1);
    printf("Server listening on port %d\n", SERVER_PORT);

    // 클라이언트와의 연결 수락
    socklen_t client_address_size = sizeof(client_address);
    client_socket = accept(server_socket, (struct sockaddr*)&client_address, &client_address_size);
    if (client_socket < 0) {
        perror("Acceptance failed");
        exit(EXIT_FAILURE);
    }
    printf("Connected to client: %s\n", inet_ntoa(client_address.sin_addr));

    // 클라이언트와의 통신
    while (1) {
        // 클라이언트로부터 데이터 수신
        memset(buffer, 0, sizeof(buffer));
        if (recv(client_socket, buffer, sizeof(buffer), 0) < 0) {
            perror("Receiving data failed");
            exit(EXIT_FAILURE);
        }

        printf("Received message: %s\n", buffer);

        // 클라이언트로 데이터 송신
        if (send(client_socket, buffer, strlen(buffer), 0) < 0) {
            perror("Sending data failed");
            exit(EXIT_FAILURE);
        }
    }

    // 소켓 종료
    close(client_socket);
    close(server_socket);

    return 0;
}

 

client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 12345

int main() {
    int client_socket;
    struct sockaddr_in server_address;
    char buffer[1024];

    // 클라이언트 소켓 생성
    client_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (client_socket == -1) {
        perror("Failed to create socket");
        exit(EXIT_FAILURE);
    }

    // 서버 주소 설정
    server_address.sin_family = AF_INET;
    server_address.sin_port = htons(SERVER_PORT);
    if (inet_pton(AF_INET, SERVER_IP, &(server_address.sin_addr)) <= 0) {
        perror("Invalid address or address not supported");
        exit(EXIT_FAILURE);
    }

    // 서버에 연결
    if (connect(client_socket, (struct sockaddr*)&server_address, sizeof(server_address)) < 0) {
        perror("Connection failed");
        exit(EXIT_FAILURE);
    }
    printf("Connected to server\n");

    // 클라이언트와의 통신
    while (1) {
        printf("Enter message: ");
        fgets(buffer, sizeof(buffer), stdin);

        // 메시지 전송
        if (send(client_socket, buffer, strlen(buffer), 0) < 0) {
            perror("Sending data failed");
            exit(EXIT_FAILURE);
        }

        // 서버로부터 데이터 수신
        memset(buffer, 0, sizeof(buffer));
        if (recv(client_socket, buffer, sizeof(buffer), 0) < 0) {
            perror("Receiving data failed");
            exit(EXIT_FAILURE);
        }

        printf("Received message: %s\n", buffer);
    }

    // 소켓 종료
    close(client_socket);

    return 0;
}

 

 위 코드는 C 언어를 사용하여 TCP 소켓 통신을 위한 간단한 예제 코드입니다. 이 코드는 클라이언트와 서버 간의 간단한 텍스트 메시지 교환을 수행합니다.

 

 

 

이상입니다!!

 

 

 Inspired from. ( aka.노마드개발자)