비동기 시스템은 A,B라는 2개의 Task가 작동한다고 가정했을 때 A의 일이 끝나던 말던 B의 일이 시작되는 시스템을 말한다.

이에 반해서 동기 시스템은 A라는 일이 끝난 후에 B의 일이 시작되는 시스템을 말한다.

그렇기에 MultiThread에 대해서 잘 알고 있어야 한다.

Thread에 대해서는 다음 글을 참조하자.

https://harmony-raccoon.tistory.com/81

 

[Java] MultiThread란?

MultiThread를 이해하기 위해서는 Process(프로세스)와 Thread(쓰레드)의 차이점 및 Concurrency(동시성)와 Parallelism(병렬성)의 차이점에 대해서 이해하고 있어야 한다.Process프로세스(Process)는 우리가 만든

harmony-raccoon.tistory.com

Java는 크게 Callback을 구현하는 방식Future를 사용하는 방식이 존재한다.

Callback

Callback이란 비동기 시스템 안에서 순서가 지켜져야 하는 코드 흐름의 순서를 보장하기 위한 디자인 패턴이다.

Callback을 구현하는 방식은 크게 CompletionHandler, 함수형 인터페이스 사용, Future 객체 사용하는 것 3가지이다.

함수형 인터페이스에 대해서는 다음 글을 참조하자.

https://harmony-raccoon.tistory.com/24

 

Java 람다식, 함수형 프로그래밍

함수형 프로그래밍(Functional Programming)이란?함수형 프로그래밍은 자료 처리를 수학적인 함수로 규정하고, 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임을 말합니다. 이 프로그래밍 스타

harmony-raccoon.tistory.com

CompletionHandler

CompletionHandler의 인터페이스는 다음과 같다.

public interface CompletionHandler<V,A> {

    /**
     * Invoked when an operation has completed.
     *
     * @param   result
     *          The result of the I/O operation.
     * @param   attachment
     *          The object attached to the I/O operation when it was initiated.
     */
    void completed(V result, A attachment);

    /**
     * Invoked when an operation fails.
     *
     * @param   exc
     *          The exception to indicate why the I/O operation failed
     * @param   attachment
     *          The object attached to the I/O operation when it was initiated.
     */
    void failed(Throwable exc, A attachment);
}

2개의 추상 메서드가 정의되어있다.

Completed 메서드에 내가 진행하고자 하는 로직을 작성하고, 만약 앞서 진행되어야 했던 로직이 실패했다면 그 에러를 처리할 로직을 failed 메서드에 작성하면 된다.

public class Callback implements CompletionHandler<String, Void> {
    @Override
    public void completed(String result, Void attachment) {
        print("Task 2 시작");
        try {
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        print("Task 2 종료");
    }

    @Override
    public void failed(Throwable exc, Void attachment) {
        print("Task 1 실패: " + exc.toString());
    }

    private void print(String content) {
        System.out.println(Thread.currentThread().getName() + " - " + content);
    }
}
public class CallbackEx1 {
    public static void main(String[] args) {
        Callback callback = new Callback();
        ExecutorService executorService = Executors.newCachedThreadPool();

        executorService.submit(() -> {
            print("Task 1 시작");
            try {
                Thread.sleep(1_000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            print("Task 1 종료");

            String result = "end";
            callback.completed(result, null); // Task1이 끝난 후, Task2의 순서를 보장함.
        });
        // Task 3가 먼저 실행될 지, Task 1이 먼저 실행될 지 모른다.
        print("Task 3 시작");
        try {
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        print("Task 3 종료");
        
        executorService.shutdown();
    }

    private static void print(String content) {
        System.out.println(Thread.currentThread().getName() + " - " + content);
    }
}

이렇게 작성한 후 로직을 실행하면 결관는 다음과 같다.

Callback with CompletionHandler

Functional Interface

Consumer 함수형 인터페이스를 활용해서 Callback 비동기 시스템을 구현하면 다음과 같다.

public class CallbackEx2 {
    public static ExecutorService executorService;
    public static void main(String[] args) {
        executorService = Executors.newCachedThreadPool();

        callback(param -> {
            print("Task 2 시작");
            try {
                Thread.sleep(1_000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            print("Task 2 종료");
        });
        // Task 3가 먼저 실행될 지, Task 1이 먼저 실행될 지 모른다.
        print("Task 3 시작");
        try {
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        print("Task 3 종료");
        
        executorService.shutdown();
    }

    public static void callback(Consumer<String> callback) {
        executorService.submit(() -> {
            print("Task 1 시작");
            try {
                Thread.sleep(1_000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            print("Task 1 종료");

            String result = "end";
            callback.accept(result); // Task1이 끝난 후, Task2의 순서를 보장함.
        });
    }

    private static void print(String content) {
        System.out.println(Thread.currentThread().getName() + " - " + content);
    }
}

이렇게 작성한 후 로직을 실행하면 결관는 다음과 같다.

Future

Future라는 객체가 비동기로 실행된 로직의 결과물을 저장해온다.

비동기로 로직을 실행할 때, 필수적으로 필요한 결과물이 존재할 수 있다.

이때, 비동기로 동작하므로 여기서 얻은 결과물이 null일지 실제 결과물인지 알 수 없다.

따라서 Future는 get()메서드를 호출할 때, 동기적으로 작동하여 실제 결과물을 얻을 때까지 잠시 기다린다.

public class CallbackEx3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        CallbackCallable cc = new CallbackCallable();

        // 작업1 Callable이 리턴한 값을 future에 담는다.
        Future<String> future = executorService.submit(cc);
        print("Task 3 시작");
        try {
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(future.get());
        print("Task 3 종료");
        
        executorService.shutdown();
    }

    private static void print(String content) {
        System.out.println(Thread.currentThread().getName() + " - " + content);
    }
}

이렇게 Task가 종료되는 시점은 Task1의 결과물이 도착한 이후로 설정하고, 코드를 제작하였다.

public class CallbackCallable implements Callable<String> {
    @Override
    public String call() {
        print("Task 1 시작");
        try {
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        print("Task 1 종료");

        return Thread.currentThread().getName() + " - end";
    }

    private void print(String content) {
        System.out.println(Thread.currentThread().getName() + " - " + content);
    }
}

위의 테스트 코드를 실행하면 결과는 다음과 같다.

Future을 활용한 비동기 시스템 구현

CompletableFuture

Future는 get()이라는 메서드를 호출한다면 메서드를 호출한 쓰레드는 Blocking(잠시 멈춤)상태로 들어가게 된다.

따라서 기약없는 기다림이(언제 자식 쓰레드의 로직이 끝나고 리턴값이 오는지 알 수 없음) 시작되어서 성능이 떨어진다.

이러한 문제를 해결하기 위해서 CompletableFuture라는 클래스가 Java8에서 도입되었다.

CompletableFuture에 대한 메서드는 다음과 같다.

비동기 작업 실행

  • runAsync() : 반환값 없는 작업을 실행할 때 사용한다. 함수형 인터페이스인 Runnable를 인자로 받는다.
  • supplyAsync() : 반환값이 있는 작업을 실행할 때 사용한다. 함수형 인터페이스인 Supplier를 인자로 받는다.

Callback(콜백) 등록

  • thenApply(): 반환값을 받아서 다른 반환값을 리턴한다. 함수형 인터페이스인 Function를 인자로 받는다.
  • thenAccept() : 반환값을 받아서 다른 반환값을 리턴하지 않는다. 함수형 인터페이스인 Comsumer를 인자로 받는다.
  • whenComplete() : 작업이 완료된 후의 결과(반환값 or 예외)를 처리한다. 함수형 인터페이스인 BiComsumer를 인자로 받는다.

작업 조합

  • thenCombine() : 2개의 비동기 작업 결과를 결합하여 데이터를 가공하고 리턴합니다. 
  • allOf() : 여러개의 비동기 작업이 모두 끝날 때까지 기다립니다.
  • anyOf() : 여러개의 비동기 작업 중 하나라도 끝나면 후속 작업을 바로 시작합니다. 

예외 처리

  • exceptionally() : 비동기 작업 내에서 발행한 예외를 처리합니다.
  • handle() : 비동기 작업 후 발생한 반환값 혹은 예외값을 처리할 수 있습니다.

외부에서 완료 가능

  • complete() : 외부에서 강제로 비동기 작업을 완료시킵니다.
public class CallbackEx4 {
    public static ExecutorService executorService;

    public static void main(String[] args) {
        executorService = Executors.newCachedThreadPool();

        // CompletableFuture 생성
        CompletableFuture<String> future = CompletableFuture.supplyAsync(CallbackEx4::task1, executorService);

        // 작업이 완료되면 결과를 처리하는 콜백 등록
        future.thenAccept(CallbackEx4::task2);

        task3();

        try {
            future.get(); // 결과를 기다림
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }

        executorService.shutdown();
    }

    private static void print(String content) {
        System.out.println(Thread.currentThread().getName() + " - " + content);
    }

    private static String task1() {
        print("Task 1 시작");
        try {
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        print("Task 1 종료");
        return "end";
    }

    private static void task2(String result) {
        print("Task 2 시작");
        try {
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        print("Task 2 종료");
    }

    private static void task3() {
        print("Task 3 시작");
        try {
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        print("Task 3 종료");
    }
}

위의 테스트 코드를 실행하면 결과는 다음과 같다.

CompletableFuture를 활용한 비동기 시스템

 

참고 자료

https://velog.io/@pllap/Java%EC%97%90%EC%84%9C%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D

 

Java에서의 비동기 프로그래밍

동기? 비동기? 글을 시작하기에 앞서, 동기와 비동기가 무엇인지 간단하게 설명해 보자면 다음과 같다. > 작업을 수행하는 두 주체 A, B가 있다고 가정하자. 동기 (sync) A가 작업을 끝내는 시간에

velog.io

https://velog.io/@amoeba25/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

 

자바에서 비동기 구현하기

해당 개념을 공부하게 된 이유는 내가 맡은 프로젝트에서 외부 API를 호출할 때 해당 서버의 처리와 관계없이 해당 요청을 안전하게 마무리하기 위함이다. 일전에 자바스크립트에서 비동기가 어

velog.io

https://mangkyu.tistory.com/263

 

[Java] CompletableFuture에 대한 이해 및 사용법

이번에는 자바8에 추가된 CompletableFuture에 대해 알아보도록 하겠습니다. 1. CompletableFuture에 대한 이해[ Future의 단점 및 한계 ]Java5에 Future가 추가되면서 비동기 작업에 대한 결과값을 반환 받을 수

mangkyu.tistory.com

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Java' 카테고리의 다른 글

[Java] MultiThread란?  (3) 2025.05.28
[Java] Runtime Data Area란?  (1) 2025.05.26
[Java] Garbage Collection란?  (1) 2025.05.23
[Java] Execution Engine이란?  (1) 2025.05.22
[Java] Class Loader란?  (2) 2025.05.21

MultiThread를 이해하기 위해서는 Process(프로세스)와 Thread(쓰레드)의 차이점 및 Concurrency(동시성)와 Parallelism(병렬성)의 차이점에 대해서 이해하고 있어야 한다.


Process

프로세스(Process)는 우리가 만든 프로그램이 실제로 작동하는 과정 가운데 있는 것을 의미한다.

즉, 개발자가 코드를 작성하고 그 코드가 CPU 위에서 작동하고 있는 상황을 말한다.

크게 Stack, Heap, Data, Code로 구성되어있다.

Thread

쓰레드(Thread)는 실제 Process에서 작업(Code를 실행시키는 것)을 수행하는 주체이다.

따라서 모든 Process는 최소 하나의 Thread를 가지고 있다.

Parallelism(병렬성) vs Concurrency(동시성)

Concurrency vs Parallelism

Concurrency(동시성)

Concurrency(동시성)이란 여러개의 Task가 동시 작업되는 것처럼 보이지만, 실제로는 여러개의 Task가 조금씩 작동하고 있는 것을 의미한다.

위 그림처럼, 실제로는 2개의 Task가 번갈아서 실행되면서 작동하게 된다. 우리가 보는 대부분의 프로세스는 OS가 이런 동시성을 활용해서 프로세스를 실행시키고 있다.

간단하게 예를 들어보자.

CPU의 Core는 하나당 하나의 프로그램을 담당한다.

이때, 내가 지금 사용하고 있는 CPU의 Core가 8개라면 최대 8개의 프로그램을 실행시킬 수 있다. 그러나 우린 실제로 사용할 때, 프로그램의 제약이 없이 사용하고 있다.

이것이 가능한 이유는 이 Concurrency 때문이다. 다양한 프로그램이 조금씩 코드를 실행해가지만, 그 속도가 매우 빠르기 때문에 우리는 동시에 실행되는 것처럼 느낀다.

Parallelism(병렬성)

병렬성은 말 그대로 2개 이상의 Task가 동시에 진행된다.

위의 예시를 그대로 생각해보자

2개 이상의 프로그램이 각각 다른 CPU의 Core에서 실행되고 있는 것을 말한다.

이렇게 프로그램을 실행한다면 동시에 여러가지의 일을 수행할 수 있다.

Thread 생성

Java에서 Thread를 생성하기 위해서는 Thread라는 클래스를 상속받거나, Runnable 인터페이스를 구현하는 방식으로 구분된다.

Thread

Thread라는 클래스를 상속받아 클래스를 생성하고, run 메서드를 구현하는 방식이다. 예시는 다음과 같다.

public class AThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread name : " + Thread.currentThread().getName());
    }
}

이후, 실제 사용하고자 하는 Main함수에 객체를 생성하고, 메서드를 호출하면 된다. 그 예시는 다음과 같다.

public class Main {
    public static void main(String[] args) {
        AThread a = new AThread();
        a.start();
    }
}

위 코드를 실행하면 다음과 같은 로그가 나오게 된다.

Thread 클래스 구현

start()라는 메서드를 호출하는 이유??

이때, 위 예시를 보면 start()라는 메서드를 호출한다. (우리가 위에서 구현한 메서드는 run()이다.)

이 이유를 알기 위해선 Thread라는 클래스의 코드를 봐야한다.

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }
    
    ...
    
    /* What will be run. */
    private Runnable target;

    ...

    /* The group of this thread */
    private ThreadGroup group;

    ...
    
    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
    
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

위 내용을 좀 뜯어보자

처음에 start()메서드가 호출되면 다음과 같은 코드를 거친다

if (threadStatus != 0)
    throw new IllegalThreadStateException();

위 코드는 threadStatus라는 것이 0이 아니면 바로 Exception을 호출하여 메서드를 종료한다.

threadStatus는 start()메서드가 호출되면 카운팅이 된다. 즉 start()라는 메서드가 각 쓰레드마다 한번씩만 호출될 수 있도록 하기 위한 코드인 것이다.

group.add(this);
boolean started = false;
try {
    start0();
    started = true;
} finally {
    try {
        if (!started) {
            group.threadStartFailed(this);
        }
    } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
    }
}

 

계속해서 코드를 분석하자.

group에 이 객체를 포함시키게 되는데 group은 쓰레드들이 모여있는 객체이다.

이렇게 새롭게 생성된 쓰레드 객체를 group에 포함시킨 후, start0()이라는 메서드를 호출한다.

start0()메서드는 native method 중 하나이고, OS에게 호출하여 새롭게 쓰레드를 생성하고, 그 쓰레드를 이 객체와 연동한다.

새롭게 생성된 쓰레드는 자동적으로 run() 메서드를 실행하게 된다.

따라서, 실제 멀티 쓰레드를 이용하기 위해서는 run()메서드가 아닌 start()메서드를 호출해야 한다.

Runnable

Runnable 인터페이스를 구현하고, Thread라는 객체를 생성하는 방식이다.

public class BThread implements Runnable{
    @Override
    public void run() {
        System.out.println("Thread name : " + Thread.currentThread().getName());
    }
}

이후, main 쓰레드에서 객체를 생성하고, start()메서드를 실행하면 된다.

public class Main {
    public static void main(String[] args) {
        BThread r = new BThread();
        Thread b = new Thread(r);
        b.start();
    }
}

위 코드처럼, Runnable 인터페이스를 구현했으므로, Thread를 Runnable 클래스를 이용해서 생성해야 한다.

이렇게 메서드를 실행하면 다음과 같은 로그가 출력된다.

Runnable Thread

Thread vs Runnable

그럼 두 방식의 차이점이 무엇일까?

Thread방식은 클래스를 상속받아야 한다. 따라서 단일 상속 원칙에 따라서 Thread는 다른 클래스를 상속받을 수 없다.

하지만 Thread를 상속받았기에 Runnable보다 많은 동시성 관련 작업을 섬세하게 진행할 수 있다. run() 메서드를 포함하여 다른 메서드들 상속받아서 개발자의 설계대로 개발이 가능하다.


반대로 Runnable은 Interface로 진행하므로 다른 클래스를 상속받을 수 있다.

Runnable에서 구현한 Thread 작동 방식을 다른 객체에서 재사용이 가능하다.

Runnable 관련 객체를 싱글톤으로 구현하고, 위의 방식처럼 여러 쓰레드에서 이 작동 방식을 사용한다면 메모리 사용량을 절약할 수 있다.


참고 자료

https://connie.tistory.com/12

 

[Java Study] - 멀티 쓰레드(쓰레드 생성과 실행)

자바의 멀티 쓰레드 프로그래밍을 알아보기 전에 프로세스와 쓰레드의 개념을 살펴보면 프로세스 실행 중인 프로그램 즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받

connie.tistory.com

https://software-engineering-corner.zuehlke.com/demystifying-concurrency

 

Demystifying Concurrency

Essential Clarifications

software-engineering-corner.zuehlke.com

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Java' 카테고리의 다른 글

[Java] 비동기 시스템 구현  (2) 2025.05.31
[Java] Runtime Data Area란?  (1) 2025.05.26
[Java] Garbage Collection란?  (1) 2025.05.23
[Java] Execution Engine이란?  (1) 2025.05.22
[Java] Class Loader란?  (2) 2025.05.21

Runtime Data Area는 JVM이 실행되기 위한 메모리를 의미한다.

물론 실제 메모리를 가리키는 것이 아니라 VM위의 메모리이므로, 실제 Hardware적인 설계가 아닌 Software적인 설계에 가깝다.

JVM와 Java라는 언어에 대해서 잘 이해하기 위해서는 이 메모리 구조에 대해서 정확하게 이해하고 있어야 한다.

Runtime Data Area의 구조

Runtime Data Area의 구조는 크게 PC Register, Stack, Native Method Stack,  Heap, Method Area로 구성되어있다.

Runtime Data Area

위 그림처럼 각 Thread별로 Stack, PC Register, Native Method Stack이 존재하고, 모든 Thread가 공유하는 Heap, Method Area가 존재한다.

모든 Thread가 공유하고 있는 Heap, Method Area에는 경쟁상태(Race Condition)이 발생할 수 있다.

PC Register

PC(Program Counter)라는 용어에 이해가 있어야 한다.

PC(Program Counter)는 프로그램이 코드를 수행할 때, 지금 수행하고 있는 코드의 위치를 가리키고 있는 메모리를 말한다.

간단한 예시를 보자.

public void log(int count) {
    System.out.print("현재 Count는 : ");
    System.out.println(count);
}

위와 같은 코드가 있다고 가정하자.

첫번째 줄인 System.out.print("현재 Count는 : ")이라는 코드를 Thread가 수행하다가 Context Switch가 일어났다고 가정하자.

Thread : 프로그램을 수행하는 단위를 의미한다.
Context Switch : Thread 1번이 프로그램을 수행하다가 Thread 2번이 또 다른 프로그램을 실행시키기 위해서 Thread 1번이 잠시 잠드는 경우를 말한다.
(간단하게 예시를 들면, 개발자 A가 C++ 언어를 기반으로 한 코드 CP를 작성하다가, Java 언어를 기반으로 한 코드 JP를 작성하기 위해서 CP 코드 작성을 잠시 쉬는 것을 의미한다.)

 

다시 log를 작성하는 Thread가 자원을 먹어서 코드를 실행하고자 할 때, 직전 실행했던 코드 위치를 정확하게 알고 있어야 다시 실행시킬 수 있다.

이렇게 각 Thread마다 자신이 직전에 실행했던 코드의 위치를 저장하는 메모리를 PC Register라고 한다.

Stack

Stack에서는 크게 Local Variable, Operand Stack, Frame Data 3가지의 데이터가 저장된다.

Java Stack

Local Variable

Local Variable(로컬 변수)는 각 객체가 가지는 지역 변수, 메서드가 가지는 매개 변수가 저장되어 있다.

배열(Array)로 저장되어 있다.

첫번째 인덱스는 this 즉, 현재 인스턴스의 참조값을 저장하고 있다.

두번째 인덱스부터 매개변수(Pramaeters), 지역변수(Local Variables)가 저장된다.

Operand Stack

Operand Stack(연산 스택)은 Stack의 자료구조로 연산자 및 연산 데이터들이 저장되어 있다.

Java는 다른 언어와 다르게 Register를 이용해서 연산을 진행하지 않고, Stack을 활용한다.

다른 어셈블리어와 달리 연산에 레지스터를 쓰지 않은 이유는 각 디바이스마다 레지스터의 수가 다르기 때문에 하드웨어의 편차를 최소화하기 위해 JVM에서는 연산과정이 복잡하더라도 Stack을 사용한다

Frame Data

Local Variable, Operand Stack 외의 자료구조들을 포함한 전체 자료구조를 의미한다.

대표적으로 Constant Pool Reference, Return Address, Exception Dispatch Information 등 존재한다.

  • Constant Pool Reference : 현재 메서드가 속한 클래스의 런타임 상수 풀을 참조하는 포인터를 저장한다. 메서드나 필드 참조, 상수값 등을 동적으로 해석할 때 사용한다.
  • Return Address : 메서드가 종료한 후, 어떤 위치로 돌아가야 하는지에 대한 정보를 저장한다.
  • Exception Dispatch Information : 예외가 발생했을 때 예외 처리를 위한 정보를 저장한다.

Native Method Area

Native Method Area는 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역입니다.

Native Method는 Java언어로 구현된 프로그램이 아니라 Java 이외의 언어로 구현된 프로그램을 말한다.

따라서 JNI(Java Native Interface)를 통해서 호출되는 경우가 일반적이다.

Method Area

JVM이 실행중인 프로그램의 메서드클래스 정보를 저장하고 있는 메모리 구조이다.

Method Area는 크게 상수 풀(Constant Pool), 필드 정보(Field Information), 메소드 정보(Method Information), 메소드 코드(Method Code), 예외 테이블(Exception Table)로 구성되어있다.

Method Area

  • 상수 풀(Constant Pool)
    • 클래스(Class)와 인터페이스(Interface)에서 사용하는 모든 리터럴 값을 저장하고 있다.
  • 필드 정보(Field Information)
    • 클래스(Class)나 인터페이스(Interface)에서 선언된 필드(Field, 멤버 변수)의 이름, 타입, 접근 제어자등을 저장하고 있다.
    • 실제 필드 값은 Heap이나 static영역에 저장되며, 필드 정보에는 구조만 저장한다.
  • 메소드 정보(Method Information)
    • 메서드 이름, 반환 타입, 파라미터 타입, 접근 제어자, static 여부, abstract 여부 등을 저장한다.
    • 메서드가 Overriding, Overloading을 하고 있는지 여부를 저장한다.
  • 메소드 코드(Method Code)
    • 메서드의 실제 바이트 코드(Byte Code)를 저장하고 있다.
  • 예외 테이블(Exception Table)
    • 메서드 내에서 try-catch 등 예외 처리 구간에 대한 정보를 저장하고 있다.

Java 8이후로 다음처럼 Metaspace Area로 변경되었다.

Metaspace Area

MetaSpace Area로 오면서 변화된 점

  • JVM에서 규정된 메모리가 아닌 OS에서 제공하는 실제 메모리를 사용하기 시작하였다. (따라서, OS가 허락하는 한 최대의 메모리를 사용할 수 있게 되었고, 메모리 부족 문제에서 어느정도 자유로워졌다.)
  • 단순히 메서드 코드와 내부 정보만 저장하는 것에서 더 확장되어 클래스 구조, 메서드 시그니처 등 더 많은 메타데이터를 저장하게 되었다.
  • 또한 static 변수들이 모두 Heap으로 이동하게 되면서 MetaSpace Area가 GC의 대상이 되었다.

Heap Area

Heap 메모리 구조에 대해서는 GC에서 자세하게 설명해놓았다.

자세한 내용은 다음 자료를 참고하자

https://harmony-raccoon.tistory.com/79

 

[Java] Garbage Collection란?

우리가 코드를 작성하고 프로그램을 실행시키다보면 메모리를 사용해야 한다. 하지만 사용하다보면 많은 메모리들은 할당을 해제시켜야 한다.간단한 예시를 들어보자public GC GCEx() { GC gc = new GC(

harmony-raccoon.tistory.com

참고 자료

https://kkang-joo.tistory.com/18

 

[JAVA] JVM 메모리 구조, 데이터 영역 ( Runtime Data Areas)

JVM의 구성요소 중, 데이터 영역에 대한 설명으로 아래의 내용들에 대해서 다루어 보도록 하겠다 Runtime Data Areas란 Runtime Data Areas의 구성 요소 스레드 별로 존재 PC Register JVM Stack Native Method Stack 스

kkang-joo.tistory.com

https://velog.io/@impala/JAVA-JVM-Runtime-Data-Area

 

[JAVA] JVM - Runtime Data Area

JVM의 메모리영역인 Runtime Data Area에 대해 알아보고 각 영역별 특징과 역할을 이해한다

velog.io

https://ssdragon.tistory.com/121

 

JVM(자바가상머신)이란? - Part4, Runtime Data Area

22/09/29 - 게시글 등록 22/10/13 - PermGen에 관한 Heap 영역 수정 및 추가 JVM(자바가상머신)이란? - Part 1, 소개 자바를 쓰는 개발자라면 누구나 들어봤을 JVM(Java Virtual Machine)을 알아보려고 한다. 자바 바

ssdragon.tistory.com

https://adjh54.tistory.com/280#1.%20%EB%A9%94%EC%84%9C%EB%93%9C%20%EC%98%81%EC%97%AD(Method%20Area)%20%3A%20~%20java%207-1-3

 

[Java] JVM(Java Virtual Machine) 이해하기 -2 : 메모리 영역(Runtime Data Area)

해당 글에서는 JVM의 구성요소 중 하나인 Runtime Data Area에 대해 상세하게 알아보기 위해 작성한 글입니다. 💡 JVM의 동작과정에 대해 궁금하시면 아래의 글을 참고하시면 크게 도움이 됩니다. [Java

adjh54.tistory.com

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Java' 카테고리의 다른 글

[Java] 비동기 시스템 구현  (2) 2025.05.31
[Java] MultiThread란?  (3) 2025.05.28
[Java] Garbage Collection란?  (1) 2025.05.23
[Java] Execution Engine이란?  (1) 2025.05.22
[Java] Class Loader란?  (2) 2025.05.21

우리가 코드를 작성하고 프로그램을 실행시키다보면 메모리를 사용해야 한다. 하지만 사용하다보면 많은 메모리들은 할당을 해제시켜야 한다.

간단한 예시를 들어보자

public GC GCEx() {
	
    GC gc = new GC("Major");
    
    gc = new GC("Minor");
    return gc;
}

위의 코드를 살펴보자

간단하게 gc가 새로운 객체로 선언되었다. 이후, gc는 또 다른 객체를 가리키면서 전의 객체를 가리키는 변수가 사라졌다.

이 상황을 그림으로 살펴보면 다음과 같다.

메모리 할당 해제가 필요한 상황

이렇게 전에 생성된 "Major"라는 이름을 가진 객체는 어떠한 변수도 가리키지 않고, 프로그램에서 다시 사용할 수 없다.

이런 메모리가 계속 쌓이게 된다면 Overflow 에러가 나올 것이므로, 이 메모리를 할당 해제를 시켜줘야 한다.

이때, C언어같은 경우 개발자가 직접 메모리를 할당 해제하여 메모리 관리를 한다.

하지만 Java나 Kotlin, Rust같은 언어들은 개발자가 직접 메모리 관리를 하지 않고, Garbage Collection이라는 객체가 대신 메모리 관리를 한다.

Garbage Collection의 구조

Garbage Collection이 메모리를 관리하기 위해 내부 코드가 동작되기 시작한다면 STW(Stop the World)의 상태에 돌입하게 된다.

STW(Stop The World)란, GC가 동작되기 위해서 모든 코드들이 잠시 멈춰있는 상태를 말한다. 따라서 STW의 시간이 길어지면 길어질수록 성능 이슈가 생긴다.

즉, STW를 최소화하면서 코드를 동작시켜야 한다.

STW

이를 위해서 JVM은 Garbage Collection을 크게 Major, Minor로 구분하여 효율적으로 메모리를 관리하고 있다.

이 둘의 차이를 이해하기 위해서는 JVM의 Heap 메모리 구조에 대해서 이해하고 있어야 한다.

Heap 메모리 구조

Garbage Collection을 구현할 때 다음과 같은 2가지 전제 조건'weak generational hypothesis'을 두고 만들어졌다.

  1. 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
  2. 오래된 객체에서 젋은 객체로의 참조는 아주 적게 존재한다.

위 조건을 간단하게 이해하면, '대부분의 객체는 일회성으로 사용되며, 일회성으로 사용되지 않는 오래된 객체들은 대부분 새로운 객체를 참조하지 않는다.'라는 것이다.

따라서 JVM는 위 가설을 최대한 활용하기 위하여 Heap 메모리 공간을 Young, Old라는 2가지 공간으로 분류하였다.

Young vs Old

  • Young 영역(Young Generation)
    • 새롭게 생성된 객체가 메모리 할당(Allocation)되는 영역
    • 대부분 객체가 금방 Unreachable(한번만 사용되고 사용되지 않는 객체) 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라진다.
    • Young 영역에 대한 Garbage Collection은 Minor GC이다.
  • Old 영역(Old Generation)
    • Young 영역에서 Reachable(한번을 넘어 여러번 사용되는 객체) 상태를 유지하여 살아남은 객체가 복사되는 영역
    • Young 영역보다 크게 할당되며, 영역의 크기가 큰 만큼 Garbage Collection이 적게 호출된다.
    • Old 영역에 대한 Garbage Collection은 Major GC이다.

Major GC

Major GC는 Old Generation의 메모리가 차서, 정리하는 경우에 호출된다.

이때, Major GC는 Old Generation의 메모리를 처리하기에 Minor GC의 STW 소요 시간보다 대략 10배 길다.

즉, Major GC는 최대한 적게 호출되어야 성능 이슈가 적다는 뜻이다.

Minor GC

Minor GC는 Young Generation의 메모리가 차서, 정리하는 경우에 호출된다.

Young Generation의 영역은 Old Generation보다 적기 때문에 STW 소요 시간이 적다.

따라서, Minor GC가 자주 호출되어도 성능 이슈에 크게 영향을 안 미친다.

Garbage Collection 동작 방식

Garbage Collection은 Major GC 혹은 Minor GC에 따라서 세부적인 동작 방식은 다르다.

그러나 GC를 처리하는 방향성은 Mark and Sweep이라는 것을 따라간다.

Mark and Sweep

  • Mark : 사용되고 있는 데이터와 아닌 데이터를 구별하는 과정
  • Sweep : 사용되지 않는 데이터를 메모리 할당 해제하는 작업

Minor GC 동작 방식

Minor GC를 정확하게 이해하기 위해서는 위에서 보았던 Heap 메모리 구조에 대해서 정확하게 이해하고 있어야 한다.

그 중, Young 메모리는 다음 3개로 구분된다.

  • Eden 영역
  • Survivor 영역 (2개)

각 영역의 처리 순서를 나열하면 다음과 같다.

1. 새로 생성한 대부분의 객체는 Eden 영역에 생성된다.
2. Eden 영역의 메모리가 가득 차면 Minor GC가 실행된다.
     2 - 1. Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동된다.
     2 - 2. Eden 영역에서 GC가 발생하면 이미 살아남은 객체가 존재하는 Survivor 영역으로 객체가 계속 쌓인다.
3. Minor GC를 진행하던 도중, 하나의 Survivor 영역이 가득 차게 되면 그 중에서 살아남은 객체를 다른 Survivor 영역으로 이동한다.
5. 그리고 가득 찬 Survivor 영역은 아무 데이터도 없는 상태로 된다.
6. 이 과정을 반복하다가 계속해서 살아남아 있는 객체(일정 GC 횟수를 버틴 객체)는 Old 영역으로 이동하게 된다.

이때, 일정한 GC 횟수를 세어야 하는데 그 값을 age라 부른다.

그리고 이 age값은 Object Header에 기록되어 있다.

Minor GC가 실행될 때, age값을 보고 일정한 값 이상이라면 이 객체를 Old Generation으로 이동시킨다.

Minor GC 전과 후

Major GC 동작 방식

Major GC는 이렇게 Minor GC가 동작하면서 Old Generation 영역에 데이터가 쌓이게 되는데, Old Generation 메모리가 가득 차게 되었다면 실행된다.

Minor GC보다 Major GC가 탐색하고 확인해야할 데이터가 훨씬 더 많으므로 STW 시간도 대략 10배 더 걸린다.

Young Generation과 Old Generation 모두 메모리 관리를 하는 GC를 Full GC라고 부른다.

참고자료

https://mangkyu.tistory.com/118

 

[Java] Garbage Collection(가비지 컬렉션)의 개념 및 동작 원리 (1/2)

1. Garbage Collection(가비지 컬렉션)이란? [ Garbage Collection(가비지 컬렉션)이란? ] 프로그램을 개발 하다 보면 유효하지 않은 메모리인 가바지(Garbage)가 발생하게 된다. C언어를 이용하면 free()라는 함

mangkyu.tistory.com

https://www.perfmatrix.com/type-of-garbage-collector/

 

Type of Garbage Collector - Serial | Parallel | CMS GC | G1 GC |

Serial garbage collector, Parallel garbage collector, CMS garbage collector and G1 garbage collector are the main type of garbage collector.

www.perfmatrix.com

 

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC

 

☕ 가비지 컬렉션 동작 원리 & GC 종류 💯 총정리

Garbage Collection(GC) 이란? 가비지 컬렉션(Garbage Collection, 이하 GC)은 자바의 메모리 관리 방법 중의 하나로 JVM(자바 가상 머신)의 Heap 영역에서 동적으로 할당했던 메모리 중 필요 없게 된 메모리 객

inpa.tistory.com

https://d2.naver.com/helloworld/1329

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Java' 카테고리의 다른 글

[Java] MultiThread란?  (3) 2025.05.28
[Java] Runtime Data Area란?  (1) 2025.05.26
[Java] Execution Engine이란?  (1) 2025.05.22
[Java] Class Loader란?  (2) 2025.05.21
[Java] JVM이란?  (3) 2025.05.20

Execution Engine(실행 엔진)이란 메모리(Runtime Area Data in Method Area)에 올라가있는 .class파일을 읽어서 코드를 실행시키는 것을 말한다.

Execution Engine은 크게 JIT, Java Interpreter로 구성되어있다.

Interpreter란?

Interpreter란 소스 코드를 한줄씩 읽음과 동시에 번역을 하여 코드를 실행시키는 것을 의미한다.

즉, 미리 번역을 하지 않고 동시 통역을 한다고 생각하면 편할 듯 싶다.

실행 시간에는 소스코드가 크던 작던 큰 차이가 없지만, 실행 속도 자체는 Complier를 지원하는 언어보다 느리다.

대표적인 언어는 python, JavaScript가 존재한다.

Complier란?

Complier란 모든 소스 코드를 한번에 번역을 해 놓은 후, 실행시키는 것을 의미한다.

따라서 미리 번역을 통째로 진행해야 하므로 컴파일 시간이 존재하며, 소스 코드가 크다면 그 시간이 오래 걸리게 된다.

하지만 한번 번역을 끝낸다면, 번역 시간이 필요하지 않으므로 Interpreter 언어보다 실행 속도가 빠르다.

대표적인 언어로는 C, C++, Go가 존재한다.


Java는 위 언어의 특징들을 동시에 사용하는 언어이다. 즉, Complier와 Interpreter를 둘다 사용하고 있다. Complier는 크게 Javac(Java Complier), JIT(Just In Time Complier)가 존재하며, Interpreter는 Java Interpreter가 존재한다.

Javac는 Execution Engine에 포함되어 있지 않으므로, JVM에서 다루게 된다.

Java Interpreter

Java Interpreter는 Runtime Data Area Method 영역에 존재하는 .class파일을 읽어서 Machine Code로 번역해준다.

이렇게 번역된 코드를 저장하는 것이 아니기에 매번 다시 그 메서드를 호출하여도 다시 Interpreter가 번역해야 한다.

JIT

JIT(Just In Time Complier)는 동적으로 .class파일을 Native Code로 번역하여 JVM의 Cache 부분에 저장한다.

일정부분 같은 메서드나 함수가 호출된다면 JIT가 그 함수를 번역하여 저장한다면 Interpreter가 다시 와서 번역하지 않고 JVM이 바로 그 코드를 실행시킬 수 있게 되는 것이다.


즉, Java는 Interpreter와 JIT를 활용하여 Interpreter의 성능 이슈를 해결한 언어이다.

Java Execution Engine이 동작하는 방식을 그림으로 표현하면 다음과 같다.

Execution Engine

참고자료

https://velog.io/@itonse/Java-%EC%8B%A4%ED%96%89-%EC%97%94%EC%A7%84Execution-Engine

 

Java - 실행 엔진(Execution Engine)

Execution Engine이란? 클래스로더에 의해 JVM으로 로드된 클래스 파일(바이트 코드로 된 .class파일)들은 런타임 데이터 영역의 메서드 영역에 배치되는데, JVM은 메서드 영역의 바이트 코드를 Execution E

velog.io

https://junhyunny.github.io/information/java/jvm-execution-engine/

 

JVM 실행 엔진(Execution Engine)

<br /><br />

junhyunny.github.io

https://ssdragon.tistory.com/28

 

JVM(자바가상머신)이란? - Part 2, Execution Engine

JVM(자바가상머신)이란? - Part 1, 소개 자바를 쓰는 개발자라면 누구나 들어봤을 JVM(Java Virtual Machine)을 알아보려고 한다. 자바 바이트코드가 JRE에서 동작을 하는데, 이 JRE에서 가장 중요한 요소는

ssdragon.tistory.com

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Java' 카테고리의 다른 글

[Java] Runtime Data Area란?  (1) 2025.05.26
[Java] Garbage Collection란?  (1) 2025.05.23
[Java] Class Loader란?  (2) 2025.05.21
[Java] JVM이란?  (3) 2025.05.20
Java 람다식, 함수형 프로그래밍  (1) 2024.12.31

JVM의 구성 요소 중 하나인 Class Loader에 대해서 알아보자

Class Loader란?

Class Loader란 JVM이 실행된 후 동적으로 class 파일을 읽어와서 메모리에 로딩하는 객체를 의미합니다.

Class Loader의 구조

Class Loader의 구조

Bootstrap Class Loader(부트 스트랩 클래스 로더)

클래스 로더 중 최상위 클래스 로더로서 java 언어로 구현되어 있지 않고, 네이티브 언어로 구현되어 있다.(네이티브 언어란, CPU와 OS가 직접 실행할 수 있는 코드를 의미한다.)

부트 스트랩 로더는 자바 클래스를 로드할 수 있는 자바 자체의 클래스 로더와 자바 API같은 최소한의 자바 클래스를 로드한다.

Extension Class Loader(확장 클래스 로더)

Java에서 제공하는 기본 기능 API를 제외한 라이브러리를 로드하는 클래스 로더이다. java.ext.dirs에 존재하는 클래스 파일을 로드하고, 이 값이 없다면 ${JAVA_HOME}/jre/lib/ext에 있는 클래스 파일을 로드한다.

System Class Loader(시스템 클래스 로더)

개발자가 직접 만든 Java 파일을 로드하는 클래스 로더이다.

Class Loader의 동작 방식

1. JVM의 메소드 영역에 클래스가 로드되어 있는지 확인한다.
2. 만일 로드되어 있는 경우 해당 클래스를 사용한다.
3. 메소드 영역에 클래스가 로드되어 있지 않을 경우, 시스템 클래스 로더에 클래스 로드를 요청한다.
4. 시스템 클래스 로더는 확장 클래스 로더에 요청을 위임한다.
5. 확장 클래스 로더는 부트스트랩 클래스 로더에 요청을 위임한다.
6. 부트스트랩 클래스 로더는 부트스트랩 Classpath에 해당 클래스가 있는지 확인한다.
7. 클래스가 존재하지 않는 경우 확장 클래스 로더에게 요청을 넘긴다.
8. 확장 클래스 로더는 확장 Classpath에 해당 클래스가 있는지 확인한다.
9. 클래스가 존재하지 않을 경우 시스템 클래스 로더에게 요청을 넘긴다.
10.시스템 클래스 로더는 시스템 Classpath에 해당 클래스가 있는지 확인한다. 클래스가 존재하지 않는 경우 ClassNotFoundException을 발생시킨다.

Class Loader의 원칙

  1. 위임 원칙
    1. 클래스 로더는 클래스 및 리소스를 찾기 위해 요청을 받았을 때, 상위 클래스 로더에게 책임을 위임한다
  2. 가시 범위 원칙
    1. 하위 클래스 로더는 상위 클래스 로더가 로드한 클래스를 볼 수 있지만, 상위 클래스 로더는 하위 클래스 로더가 로드한 클래스를 알 수 없다.
  3. 유일성의 원칙
    1. 하위 클래스 로더는 상위 클래스 로더가 로드한 클래스를 다시 로드하지 않아야 한다.

Class Loading 과정

Class Loader 과정

Loading

로딩 과정은 말 그대로 클래스 로더가 .class 파일을 읽어온 후, Runtime Data Area의 Method 영역에 저장한다.

그 후 Heap 영역에 Class Object를 생성하여 저장한다.

Linking

링크 작업은 크게 3가지로 구분되어 진행된다

  1. Verifying
    .class 파일의 형식이 제대로 있는지 확인한다. 자바 명세서에 있는 대로 class 파일이 저장되었는지 확인한다.
  2. Preparing
    클래스가 필요로 하는 메모리를 할당한다. 이때, 필요한 메모리는 클래스 내 변수, 메서드 등을 의미한다.
  3. Resolving
    심볼릭 레퍼런스를 메모리 영역에 존재하는 다이렉트 레퍼런스로 교체한다.
    심볼릭 레퍼런스 : 이름으로만 구성한 참조 관계
    다이렉트 레퍼런스 : 이름과 실제 데이터가 존재하는 메모리 주소로 구성한 참조 관계

Initializing

클래스 내 변수들을 초기화한다.

동적 클래스 로딩

Java는 JVM을 실행시키면서 동시에 Class를 메모리에 로드시키지 않고, 실제 코드가 돌아갈 때 Class를 로드한다.

이때, 로드 방식을 크게 Load Time Dynamic LoadingRun Time Dynamic Loading으로 구분된다.

Load Time Dynamic Loading(로트 타임 동적 로딩)

  1. JVM이 시작되고 부트 스트랩 클래스 로더가 생성된 후, 모든 클래스가 상속받고 있는 Object 클래스를 읽어온다.
  2. 클래스 로더는 명령행에서 지정한 클래스(개발자가 개발한 클래스거나 외부 라이브러리 등)를 로딩하기 위해 해당 클래스 파일을 읽어온다.
  3. 해당 클래스를 로딩하는 과정에서 필요한 클래스들을 추가적으로 로딩한다.

즉, 클래스를 새로 로딩하면서 또 다른 클래스를 로드하는 것을 로드 타임 동적 로딩이라고 한다.

Run Time Dynamic Loading(런타임 동적 로딩)

Class.forName이라는 메서드를 통해서 해당하는 클래스를 찾아서 로딩한 후, 객체를 로딩한다.

즉, 메서드 단위에서 클래스를 로드하는 것을 런타임 동적 로딩이라고 한다.

참고 자료

https://steady-coding.tistory.com/593

 

[Java] JVM의 클래스 로더란?

java-study에서 스터디를 진행하고 있습니다. 클래스 로더란? 자바는 동적 로드, 즉 컴파일 타임이 아니라 런타임(바이트 코드를 실행할 때)에 클래스 로드하고 링크하는 특징이 있다. 이 동적 로드

steady-coding.tistory.com

https://wpioneer.tistory.com/255

 

[Java] ClassLoader 구조 및 동작 원리

이전에 JVM에 대해서 자세히 설명한적이 있다 거기서 ClassLoader라는 개념이 나오는데 이번에는 ClassLoader에 대한 자세한 설명을 하려고 한다. Class Loader Class Loader는 ByteCode를 읽어서 Class 객체를 생

wpioneer.tistory.com

https://goodgid.github.io/Java-Class-Loader/

 

클래스 로더(Class Loader)

 

goodgid.github.io

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Java' 카테고리의 다른 글

[Java] Runtime Data Area란?  (1) 2025.05.26
[Java] Garbage Collection란?  (1) 2025.05.23
[Java] Execution Engine이란?  (1) 2025.05.22
[Java] JVM이란?  (3) 2025.05.20
Java 람다식, 함수형 프로그래밍  (1) 2024.12.31

Java라는 언어를 이해하기 위해서는 JVM이라는 가상 컴퓨터를 이해해야 한다.

JVM이란?

JVM(Java Virtual Machine)은 자바라는 언어를 구동하기 위한 가상 컴퓨터를 말한다. 그럼 가상 컴퓨터가 무엇이고, 왜 이런 과정이 필요할까??

VM이란?

VM(Virtual Machine)은 간단하게 말하자면 SW로 구현한 컴퓨터이다.

 

Machine 구조

위의 구조는 우리가 실제 사용하는 컴퓨터 구조이다. 매우 간소화시켰지만, 필요한 부분은 OS(Linux, Windows, MacOs 등등) 위에서 프로그램이 작동한다는 것이다. 이때, OS에 따라서 프로그램의 코드가 달라져야 한다. 프로그램 언어(C++, Java, Kotlin 등등)가 OS위에서 작동하게 되는데, OS마다 호출해야 하는 코드가 달라지게 된다. 따라서 작동시킬 컴퓨터마다 환경을 모두 다르게 설정해야 한다.(정확히는 OS 마다)

VM 구조

이러한 문제점을 보완하고자 나온 개념이 VM이다. OS위에 가상의 OS를 다시 실행시켜 어떠한 컴퓨터에도 같은 설정으로 작동할 수 있게 하는 시스템을 말한다.

Java도 이 VM을 작동시켜 모든 OS위에 프로그램이 안정적으로 작동할 수 있게 하는 시스템을 말한다.

JVM의 구조

JVM의 구조는 크게 Class Loader, Execution Engine, Garbage Collector, Runtime Data Area로 구성되어 있다.

Class Loader

Class Loader는 Javac(Java Complier)가 변환한 .class 파일을 동적으로 읽어서 RunTime Data Area에 할당해준다.

Javac란?

Javac는 .java 파일을 .class파일로 변환해주는 컴파일러를 의미한다.

Javac는 JDK 안에 포함되어 있으며, OS에 상관없이 독립적으로 동작한다.

자세한 내용은 다음 자료를 참고하자

https://harmony-raccoon.tistory.com/77

 

[Java] Class Loader란?

JVM의 구성 요소 중 하나인 Class Loader에 대해서 알아보자Class Loader란?Class Loader란 JVM이 실행된 후 동적으로 class 파일을 읽어와서 메모리에 로딩하는 객체를 의미합니다.Class Loader의 구조Bootstrap Clas

harmony-raccoon.tistory.com

Execution Engine

우리가 작성한 .java 파일을 .class 파일로 변환하거나, .class 파일을 읽어서 실행한다.

Execution Engine에는 JIT, Java Interpreter가 존재한다.

자세한 내용은 다음 자료를 참고하자

https://harmony-raccoon.tistory.com/78

 

[Java] Execution Engine이란?

Execution Engine(실행 엔진)이란 메모리(Runtime Area Data in Method Area)에 올라가있는 .class파일을 읽어서 코드를 실행시키는 것을 말한다.Execution Engine은 크게 JIT, Java Interpreter로 구성되어있다.Interpreter란?

harmony-raccoon.tistory.com

GC(Garbage Collector)

Garbage Collector는 할당된 Object가 더이상 어떠한 명령에도 사용되지 않다고 판단되면, 메모리 할당을 자동으로 회수한다. 만약 C같은 언어라면 메모리 회수를 개발자가 직접 코드로 명령해야 하지만, 자바는 GC가 알아서 관리해준다.

자세한 내용은 다음 자료를 참고하자

https://harmony-raccoon.tistory.com/79

 

[Java] Garbage Collection란?

우리가 코드를 작성하고 프로그램을 실행시키다보면 메모리를 사용해야 한다. 하지만 사용하다보면 많은 메모리들은 할당을 해제시켜야 한다.간단한 예시를 들어보자public GC GCEx() { GC gc = new GC(

harmony-raccoon.tistory.com

Runtime Data Area

프로그램을 실행할 때 필요한 데이터를 모아놓은 공간이다.

이 공간은 크게 Method Area, Heap Area, Stack Area, Pc Register, Native Method Stack으로 구분할 수 있다.

자세한 내용은 다음 자료를 참고하자

https://harmony-raccoon.tistory.com/80

 

[Java] Runtime Data Area란?

Runtime Data Area는 JVM이 실행되기 위한 메모리를 의미한다.물론 실제 메모리를 가리키는 것이 아니라 VM위의 메모리이므로, 실제 Hardware적인 설계가 아닌 Software적인 설계에 가깝다.JVM와 Java라는 언

harmony-raccoon.tistory.com

참고 자료

https://inpa.tistory.com/entry/JAVA-%E2%98%95-JVM-%EB%82%B4%EB%B6%80-%EA%B5%AC%EC%A1%B0-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%98%81%EC%97%AD-%EC%8B%AC%ED%99%94%ED%8E%B8#%ED%81%B4%EB%9E%98%EC%8A%A4_%EB%A1%9C%EB%8D%94_class_loader

 

☕ JVM 내부 구조 & 메모리 영역 💯 총정리

저번 포스팅에서는 JRE / JDK / JVM에 대해서 간략하게 알아보는 시간을 가졌다면, 이번 포스팅에서는 JVM의 내부 구조에 대해 좀 더 자세하게 알아보도록 할 예정이다. JVM(자바 가상 머신)은 자바 언

inpa.tistory.com

https://backendcode.tistory.com/161

 

[IT 기술 면접] JVM (자바 가상 머신) 이란?

이번에는 JVM에 대해 정리할 것이다. ▶ JVM 이란? JVM이란 Java Virtual Machine, 자바 가상 머신의 약자를 따서 줄여 부르는 용어이다. (가상 머신이란 프로그램을 실행하기 위해 물리적 머신과 유사한

backendcode.tistory.com

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

'Java' 카테고리의 다른 글

[Java] Runtime Data Area란?  (1) 2025.05.26
[Java] Garbage Collection란?  (1) 2025.05.23
[Java] Execution Engine이란?  (1) 2025.05.22
[Java] Class Loader란?  (2) 2025.05.21
Java 람다식, 함수형 프로그래밍  (1) 2024.12.31

함수형 프로그래밍(Functional Programming)이란?

함수형 프로그래밍은 자료 처리를 수학적인 함수로 규정하고, 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임을 말합니다. 이 프로그래밍 스타일은 외부 변화에 영향을 받지 않고, 내부 변화에만 의존합니다. 따라서 메서드로 인한 부작용(Side Effect)이 발생하지 않도록 설계됩니다.

정의만 읽으면 감이 잘 오지 않을 수 있습니다. 함수형 프로그래밍을 도입하기 위해 Java 8에서 추가된 함수형 인터페이스를 살펴보겠습니다.


Java 8 함수형 인터페이스 구조

아래는 java.util.function 패키지에 포함된 Consumer 함수형 인터페이스의 구조입니다:

더보기
더보기
package java.util.function;

import java.util.Objects;

/**
 * Represents an operation that accepts a single input argument and returns no
 * result. Unlike most other functional interfaces, {@code Consumer} is expected
 * to operate via side-effects.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #accept(Object)}.
 *
 * @param <T> the type of the input to the operation
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

이 함수형 인터페이스는 내부에 정의된 상태 변수가 없고, 추상 메서드 accept 하나와 기본(default) 메서드 andThen을 가집니다. 이를 통해 함수형 프로그래밍 스타일을 Java에서 구현할 수 있습니다.


람다 표현식(Lambda Expression)

람다 표현식은 함수형 프로그래밍을 Java에서 실현하기 위해 도입된 문법입니다. 람다 표현식은 메서드 호출을 간결하게 만들어주며, 선언과 동시에 데이터를 처리할 수 있습니다.

예를 들어:

Arrays.asList(1,2,3).stream()
	.map(i -> i*i)
	.forEach(System.out::println);

위 코드는 다음과 같은 동작을 수행합니다:

  1. 리스트의 각 요소를 제곱으로 변환 (map).
  2. 변환된 결과를 출력 (forEach).

이처럼 람다 표현식을 사용하면 데이터를 선언적으로 처리할 수 있습니다.


함수형 프로그래밍의 활용 예시

기존의 객체지향 프로그래밍(OOP) 방식에서는 코드 리팩토링이 어려운 경우가 있습니다. 예를 들어, 다음 코드를 보겠습니다:

private void 시도_함수_1() {
	try {
    	실제 코드들....
    } catch (Exception e) {
    	throw e;
    }
}

private void 시도_함수_2() {
	try {
    	실제 코드들....
    } catch (Exception e) {
    	throw e;
    }
}

이 방식은 함수가 많아질수록 try-catch 블록이 중복되고, 유지보수가 어려워집니다. 이를 함수형 프로그래밍으로 리팩토링하면:

 

private void 시도_함수(Runnable logic) {
	try {
    	logic.run();
    } catch(Exception e) {
    	throw e;
    }
}

호출 시:

시도_함수(() -> {
    // 실제 코드 1...
});

시도_함수(() -> {
    // 실제 코드 2...
});

이렇게 하면 try-catch 블록이 중복되지 않고, 새로운 로직을 추가할 때도 호출만 변경하면 되므로 유지보수가 쉬워집니다.


함수형 인터페이스가 되기 위한 조건

  1. 내부에 상태나 변수를 가지면 안 됩니다.
  2. 추상 메서드가 오직 한 개만 있어야 합니다.

Java 8부터는 이러한 조건을 만족하는 함수형 인터페이스를 기본 제공하므로, 개발자는 이를 활용해 함수형 프로그래밍을 구현할 수 있습니다.


Java에서 제공하는 함수형 인터페이스

1. Predicate

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
  • 용도: 하나의 파라미터를 받아 boolean 값을 반환할 때 사용.

 

2. Consumer

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

 

  • 용도: 하나의 파라미터를 받아 결과를 반환하지 않을 때 사용.

3. Supplier

@FunctionalInterface
public interface Supplier<T> {
    T get();
}
  • 용도: 파라미터 없이 객체를 반환할 때 사용.

 

4. Function

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
  • 용도: 하나의 파라미터를 받아 변환된 결과를 반환할 때 사용.

 

5. Runnable

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
  • 용도: 파라미터도 없고 반환값도 없는 작업을 실행할 때 사용.

 


마무리

함수형 프로그래밍은 객체지향 프로그래밍의 한계를 보완하고, 코드를 더욱 간결하고 유지보수하기 쉽게 만들어줍니다. 이를 통해 Java 개발자는 선언적이고 모듈화된 코드를 작성할 수 있습니다.

 

혹시라도 틀린 내용이 있다면 댓글로 알려주시면 감사하겠습니다!!

참고 문헌

참고 문헌1

참고 문헌2

참고 문헌3

참고 문헌4

참고 문헌5

 

 

 

'Java' 카테고리의 다른 글

[Java] Runtime Data Area란?  (1) 2025.05.26
[Java] Garbage Collection란?  (1) 2025.05.23
[Java] Execution Engine이란?  (1) 2025.05.22
[Java] Class Loader란?  (2) 2025.05.21
[Java] JVM이란?  (3) 2025.05.20

+ Recent posts