728x90
반응형

본 글은 모던자바인액션 책을 참고하여 작성한 글입니다.

동작 파라미터화란?

👉🏻Java8 등장배경 및 새로운 기능 이전글에서 언급했던 것처럼
동작 파라미터화는 Java8에서 새롭게 등장한 기능입니다.

동작 파라미터화는 말그대로 특정 동작을 파라미터로 입력할 수 있는 기능인데, 이 기능을 활용하면 특정 동작을 정의한 코드를 다른 메서드의 인수로써 넘겨줄 수 있습니다.

why? 왜 사용해야 하는가?

이전글에서 해당 동작 파라미터화 기능이 Java8의 핵심기능인 스트림 API의 토대가 되었다고 언급했었습니다.
스트림 API를 활용하기 위해 사용한다고도 말할 수 있지만, 좀 더 근본적인 사용이유는
"자주 바뀌는 요구사항에 효과적으로 대응"할 수 있기 때문입니다.

자세한건 예시를 보면서 알아보겠습니다.

예시

요구사항 1. 녹색사과 필터링

농장 재고목록 Application에서 활용되는 사과 리스트중 녹색사과만 필터링하는 기능을 추가한다는 요구사항이 있다고 가정해보겠습니다.
해당 요구사항의 기능은 아래 코드로 구현할 수 있습니다.

public static List<Apple> filterGreenApples(List<Apple> inventory) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ( GREEN.equals(apple.getColor()) ) {
            result.add(apple);
        }
    }
    return result;
}



요구사항 2. 녹색/빨간사과 필터링

농부가 변심하여 빨간 사과도 필터링하고 싶다고 요구사항을 변경하였습니다.
위 코드를 Copy/Paste하여 if문의 조건만 RED로 바꿔 메서드를 추가할 수 있겠지만,
추후에 다양한 색으로 요구사항이 또 들어올 경우에 유연한 대처가 불가능해질 수 있습니다.
또한, 동일한 기능의 코드가 반복될 수 있죠.

이를 보완하기 위해 아래 코드처럼 색이라는 변수를 받을 수 있도록 메서드에 색 파라미터를 추가해줍니다.

public static List<Apple> filterAppleByColor(List<Apple> inventory, Color color) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ( apple.getColor().equals(color) ) {
            result.add(apple);
        }
    }
    return result;
}



그리고 아래 코드를 호출하여 요구사항을 만족시킬 수 있습니다.

List<Apple> greenApples = filterAppleByColor(inventory, GREEN);
List<Apple> redApples = filterAppleByColor(inventory, RED);



요구사항 3. 색 이외에 무게 기준으로 필터링 가능하도록 기능 추가

농부가 또 변심하여 색 이외에도 가벼운 사과와 무거운 사과로 구분할 수 있도록 해달라고 할 수 있습니다.

public static List<Apple> filterAppleByWeight(List<Apple> inventory, int weight) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ( apple.getWeight() > weight ) {
            result.add(apple);
        }
    }
    return result;
}



위 코드로도 무게 기준으로 필터링할 수 있도록 기능을 구현할 수 있지만,
해당 무게 필터링 코드는 색 필터링 코드와 대부분 중복됩니다.
이는 소프트웨어 공학의 DRY (Don't Repeat Yourself), 같은 것을 반복하지 말자는 원칙을 어기는 것입니다.

public static List<Apple> filterApple(List<Apple> inventory, Color color, int weight, boolean flag) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ((flag && apple.getColor().equals(color))
            || (!flag && apple.getWeight() > weight)) {
            result.add(apple);   
        }
    }
    return result;
}



위 코드로 flag에 따라 필터링 대상을 결정하고,
필터링 대상에 맞게 필터링을 수행할 수 있지만, 요구사항 추가에 유연하게 대응할 수 없고,
flag를 다양화해서 어찌어찌 대응한다해도 코드의 복잡성이 굉장히 크게 증가할 것입니다.

이를 대체하는 방안으로 동작 파라미터화기능이 사용될 수 있습니다.

동작 파라미터화 기능의 활용

예시 보완

위 예시에서 동작 파라미터화 기능을 사용하기 위해 아래와 같은 사전준비 작업이 필요합니다.

  1. 필터링 메서드의 참/거짓을 반환하는 인터페이스(프레디케이트) 정의
  2. 참/거짓을 반환하는 인터페이스(프레디케이트)의 구현체 정의 (ex. 150이상 무게 사과 판단, 초록사과 판단 등)
  3. 필터링 메서드의 동작 파라미터화


아래는 프레디케이트 정의 코드입니다.

public interface ApplePredicate {
    boolean test (Apple apple);
}



아래는 프레디케이트의 구현체 코드입니다.

// 150이상 무게 사과 판단
public class AppleHeavyWeightPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return apple.getWeight() > 150;
    }
}

// 초록사과 판단
public class AppleGreenColorPredicate implements ApplePredicate {
    public boolean test(Apple apple) {
        return GREEN.equals(apple.getColor());
    }
}



위 처럼 프레디케이트 인터페이스를 정의하고 구현체를 작성하는 패턴을 전략 디자인 패턴이라고 합니다.
전략 디자인 패턴은 전략(알고리즘)을 캡슐화하는 알고리즘 패밀리(프레디케이트 인터페이스)를 정의해둔 다음에 런타임에 상황에 따라 알고리즘을 선택하는 기법입니다. (ApplePredicate : 알고리즘 패밀리, AppleHeavyWeightPredicate/AppleGreenColorPredicate : 전략)

해당 패턴을 활용한다면 filter 메서드에서 파라미터로 받은 동작에 맞게 필터링을 수행할 수 있습니다.
아래 코드는 전략 디자인 패턴 기반의 동작을 파라미터로 받을 수 있도록 수정한 filter 메서드입니다.

public static List<Apple> filterApple(List<Apple> inventory, ApplePredicate p) {
    List<Apple> result = new ArrayList<>()
    for (Apple apple : inventory) {
        if ( p.test(apple) ) {
            result.add(apple);
        }
    }
    return result;
}



위 코드처럼 프레디케이트(인터페이스)를 파라미터로 지정한 다음에 다양한 구현체를 파라미터로 받아 다양하게 필터링을 수행할 수 있도록 하였습니다.
즉, 우리가 전달한 ApplePredicate 객체에 의해 filterApples 메서드의 동작이 결정됩니다.

익명클래스

위와 같은 방법도 충분히 좋지만, 동작을 파라미터화하기 위해 인터페이스를 정의하고 여러 클래스를 정의하는 과정이 상당히 번거롭게 느껴집니다.
이를 간소화하기 위해 익명클래스를 활용하는 방법이 있습니다.

익명클래스를 이용하면 클래스 선언과 인스턴스화를 동시에 할 수 있어 별도로 여러 클래스를 정의하는 과정이 필요하지 않습니다.
아래 코드는 익명클래스를활용하기 이전 filter메소드를 실행하는 코드입니다.

List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());
List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate());



위 코드에서는 사전에 정의된 클래스를 파라미터로 입력하여 실행하고 있습니다.
아래 코드는 익명클래스를 활용하여 filte메소드를 실행하는 코드입니다.

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
    public boolean test(Apple a) {
        return RED.equals(a.getColor());
    }
});



익명클래스를 활용하여 별도 클래스를 선언하고 작성하는 과정을 간소화하였지만,
여전히 코드는 불필요하게 공간을 많이 차지하고 있고, 프로그래머가 여전히 익숙하게 느껴질 수 있는 형태의 코드가 아닙니다.

이러한 코드의 장황함은 코드를 구현하고 유지보수하는 데 많은 시간을 소요하게끔 할 수 있습니다.

람다 표현식

위의 익명클래스의 단점을 보완하기 위해 람다 표현식이 사용될 수 있습니다.

List<Apple> result = filterApples(inventory, (Apple apple) -> RED.equals(apple.getColor()));



위 코드를 통해 익명클래스에서의 복잡성을 많이 줄일 수 있습니다.
람다표현식의 자세한 부분은 👉🏻다음글에서 다룰 것입니다.

리스트 형식의 추상화

아래 코드처럼 프레디케이트에 제너릭을 활용한다면,
사과 뿐만 아니라 바나나, 오렌지 등으로 대상까지 다양화할 수 있습니다.

public interface Predicate<T> {
    boolean test(T t);
}

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> result = new ArrayList<>();
    for (T e : list) {
        if (p.test(e)) {
            result.add(e);
        }
    }
    return result;
}



실전 예제

실전에서 사용하고 있는 예제로 Comparator, Runnable 등이 있고,
간단히 다뤄보겠습니다.

Comparator로 정렬하기

Java8에서 Collection에는 sort 메서드가 포함되어 있습니다.
해당 sort메서드의 정렬방식을 Comparator를 활용하여 정의할 수 있는데요.

아래 코드처럼 Comparator를 활용하여 정렬 기준을 정의한 후에
sort 메서드에 파라미터로 입력하면 됩니다.

// java.util.Comparator
public interface Comparator<T> {
    int compare(T o1, T o2);
}
  • 익명클래스
inventory.sort(new Comparator<Apple>() {
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
});
  • 람다표현식
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));



Runnable로 코드 블록 실행하기

자바 스레드를 이용하면 코드를 병렬로 실행할 수 있습니다.
Runnable은 스레드에 실행할 동작을 전달할 때, 사용되는 인터페이스입니다.

// java.lang.Runnable
public interface Runnable {
    void run();
}
  • 익명 클래스
Thread t = new Thread(new Runnable() {
    public void run() {
        System.out.println("Hello World!");
    }
});
  • 람다 표현식
Thread t = new Thread(() -> System.out.println("Hello World!"));



이어서

람다표현식에 대해 상세히 알아보겠습니다.

반응형
728x90
반응형


본 글은 모던자바인액션 책을 참고하여 작성한 글입니다.

Java 8 등장 배경

멀티코어 CPU의 대중화

Java 8이 등장하기 이전의 Java프로그램은 대부분 코어 중 하나만을 사용하였고, 나머지 코어는 유휴 idle 상태로 두었습니다.
유휴 idle 상태인 나머지 코어를 활용하기 위해서는 멀티 스레드 기법을 활용하면 되었지만, 관리하기 어렵고 많은 문제가 발생할 우려가 크다는 단점이 있었습니다.

이를 보완하기 위해 Java 8에서는 아래와 같이 새로운 기능이 추가되었습니다.

  1. 동작 파라미터화
  2. 메서드 참조
  3. 람다
  4. 스트림 API
  5. 기타 기능들 (디폴트 메서드, Optional 등)

동작 파라미터화기능을 추가하여 특정 메서드의 파라미터로 메서드도 입력할 수 있도록 하였습니다. 간단한 메서드의 경우에는 별도의 메서드 구현 없이도 입력 값에 바로 메서드 코드를 입력할 수 있게끔 람다 기능도 추가되었습니다.
그리고 이러한 동작 파라미터화 기능이 Java 8의 핵심 기능인 스트림 API의 토대가 되었습니다.

결론적으로 Java 8에서 위 기능들이 추가되어 보다 안정적인 멀티코어 기반의 Java 프로그램을 구현하기 위한 환경이 조성되었다고 볼 수 있습니다.
이제 아래 메뉴에서 새롭게 추가된 기능으로 어떤 것들이 있는지? 왜 해당 기능들을 추가하면 보다 안정적인 프로그램 구현이 가능한지? 알아보겠습니다.

새로운 기능

동작 파라미터화

동작 파라미터화는 말그대로 특정 동작을 파라미터로 입력할 수 있는 기능을 지칭합니다. 즉, 메서드(코드)를 다른 메서드의 인수로 넘겨줄 수 있는 기능이라고 보면 됩니다.

  • 예시
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
    public boolean accept(File file) {
        return file.isHidden();
    }
});



메서드 참조

메서드 참조는 특정 메소드의 파라미터로 입력될 메소드를 지칭하기 위한 기능입니다.

위에서 언급한 동작 파라미터화 기능의 예시 코드를 보면, 가독성 측면에서 꽤나 복잡하다고 느껴집니다.
단번에 해석하기도 쉽지 않고 isHidden이라는 메서드를 활용하기 위해서 굳이 별도 메서드를 추가적으로 만들어서 파라미터로 입력하고 있습니다.

이러한 비효율을 없애기 위해 Java 8에서는 메서드 참조기능을 제공하고 있습니다.

  • As-Is
File[] hiddenFiles = new File(".").listFiles(new FileFilter() {
    public boolean accept(File file) {
        return file.isHidden();
    }
});



  • To-Be
File[] hiddenFiles = new File(".").listFiles(File::isHidden);




위의 To-Be 코드처럼 별도 메소드 구현 없이 필요한 메서드를 참조하여 파라미터로 입력할 수 있습니다.
클래스명에 ::기호를 붙이고, 그 뒤에 참조할 메서드명을 붙여서 메서드 참조를 활용할 수 있습니다.

람다

람다기능은 메서드를 다른 메서드의 파라미터로 입력할 때, 메서드를 별도로 구현하지 않고 바로 코드를 파라미터로 입력하도록 할 수 있는 기능입니다.
먼저 프레디케이트에 대해서 언급하고 넘어가겠습니다.

프레디케이트

프레디케이트는 인수로 특정 값을 받아서 true/false로 반환하는 함수를 가리키는 말입니다.

Function<Apple, Boolean> == Predicate<Apple>형태로 볼 수 있고,
입력할 Object type을 제네릭 형태로 입력하게 됩니다.

Predicate를 활용하는 방식이 좀더 표준적인 방식이고, 구현은 아래 코드와 같이 진행할 수 있습니다.

public interface Predicate<T> {
    boolean test(T t);
}

static List<Apple> filterApples(Predicate<Apple> p) {
    List<Apple> result = new ArrayList<>();
    Apple input = new Apple();
    if (p.test(input)) {
        result.add(apple);
    }
    return result;
}

위와 같이 특정 Object를 입력으로 받아 특정 기준에 부합하는지에 대한 판단을 Predicate활용을 통해 진행할 수 있습니다.
그리고 아래와 같이 Predicate 파라미터의 메서드를 호출할 수 있습니다.

public static boolean isHeavyApple(Apple apple) {
    return apple.getWeight() > 150;
}

// Predicate 파라미터 메서드
filterApples(Apple::isHeavyApple);



람다의 활용

람다기능은 앞서 언급한 것처럼 메서드를 다른 메서드의 파라미터로 입력할 때, 메서드를 별도로 구현하지 않고 바로 코드를 파라미터로 입력하도록 할 수 있는 기능입니다.

위 프레디케이트 메서드 호출을 위해 사용되는 isHeavyApple메서드는 매우 간단한 코드입니다.
람다 기능을 활용한다면, 위 프레디케이트 메서드 호출 코드를 아래 코드와 같이 별도 메서드 구현없이 간단하게 표현할 수 있습니다.

filterApples( (Apple a) -> a.getWeight() > 150 );




()안에 람다 함수의 인수로 입력할 변수를 선언해주고, ->로 연결된 선언부에서 구현할 코드를 작성해주면 됩니다.

Stream API

Stream API는 Java 8이 등장하면서 동작 파라미터화기능을 토대로 출시된 API입니다.
Stream API를 활용한다면, Java 8이전에 길고 복잡한 코드로 처리했던 로직을 데이터베이스 질의 언어에서 표현식을 처리하는 것처럼 간결하게 코드를 작성하여 처리할 수 있습니다.

  • Java 8 이전 예시코드
Map<Currency, List<Transaction>> transactionByCurrencies = new HashMap<>();
for (Transaction transaction : transactions) {
    if (transaction.getPrice() > 1000) {
        Currency currency = transaction.getCurrency();
        List<Transaction> transactionsForCurrency = transactionByCurrencies.get(currency);
        if (transactionsForCurrency == null) {
            transactionsForCurrency = new ArrayList<>();
            transactionByCurrencies.put(currency, transactionsForCurrency);
        }
        transactionsForCurrency.add(transaction);
    }
}



  • Stream API 적용 에시코드
import static java.util.stream.Collectors.groupingBy;
Map<Currency, List<Transaction>> transactionByCurrencies = transactions.stream()
                                                                    .filter((Transaction t) -> t.getPrice() > 1000)
                                                                    .collect(groupingBy(Transaction::getCurrency));




위 코드에서 볼 수 있는 것처럼 Stream API를 활용하면 코드의 가독성 측면에서 이점이 있습니다.
뿐만 아니라 Stream API는 아래 문제 해결에 주안점을 두었습니다.

  1. 모호함과 반복적인 코드 문제
  2. 멀티코어 활용 어려움

이제 두 가지 문제의 관점에서 Stream API를 알아보겠습니다.

모호함과 반복적인 코드 문제 해결

기존의 컬렉션에서는 데이터를 처리할 때 아래와 같이 반복되는 패턴이 많았습니다.

  1. 주어진 조건으로 데이터를 필터링 (ex. 특정 무게를 기준으로 사과 선별)
  2. 데이터 추출 (ex. 특정 사과의 무게필드 추출)
  3. 데이터 그룹화 (ex. 숫자 리스트를 홀수와 짝수로 그룹화)
  4. 등등...

Stream API를 활용한다면, 위 반복되는 패턴을 제공되는 Stream 라이브러리를 활용하여 구현할 수 있습니다.
가령 데이터 필터링 패턴은 Stream의 Filter 라이브러리를 활용하여 구현할 수 있죠.





위처럼 패턴을 라이브러리화 시킬 수 있었던 배경은 Stream API에서는 내부반복을 활용하는 것이었습니다.
외부반복은 기존에 사용하던 방식처럼 For-loop를 활용하는 방식이고,
내부반복은 Stream 라이브러리 자체에서 진행되는 반복을 의미한다고 볼 수 있습니다.
그렇기 때문에 내부반복이 진행될 때에는 어떻게 돌아가는지 개발자가 직접 확인할 수 없죠.

멀티코어 활용 어려움 해결

Java 8 등장 이전에는 멀티스레딩 구현이 쉽지 않았습니다.
각각의 스레드에서 공유된 데이터에 접근하게 되면 동시성 문제가 발생할 수도 있는데, 각 스레드들을 잘 제어하지 못하면 데이터가 원치않은 방식으로 바뀔 수도 있습니다.

Stream API의 핵심적인 목표는 이러한 멀티스레딩 기능을 쉽게 구현하는 것입니다.
아래 Parallel 코드를 활용하면 쉽게 병렬처리를 구현할 수 있습니다.

  • Sequential
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples = inventory.stream().filter((Apple a) -> a.getWeight() > 150).collect(toList());



  • Parallel
import static java.util.stream.Collectors.toList;
List<Apple> heavyApples = inventory.parallelStream().filter((Apple a) -> a.getWeight() > 150).collect(toList());




Java 8 이전에는 멀티스레딩 기능을 활용하기 위한 데이터를 나누는 등의 작업을 모두 개발자가 진행해줘야 했지만,
Stream API의 parallelStream을 활용하면 Stream API 내부적으로 Java 7에서 추가된 포크-조인 프레임워크를 활용하여 멀티스레딩을 위한 작업이 진행됩니다. 아래 사진은 포크-조인 프레임워크에서 멀티스레딩 작업이 진행되는 과정을 도식화한 그림입니다.





그래서 Java 8에서는 데이터베이스 질의 언어에서 표현식을 처리하는 것처럼( 고수준 언어로 원하는 동작을 구현했을 때, 최적의 저수준 실행방법을 선택하는 방식 ) 병렬 연산을 지원하는 스트림이라는 새로운 API를 제공합니다. 이러한 스트림을 이용하면 에러를 자주 일으키고 이용 비용이 비싼 Synchronized를 사용하지 않아도 됩니다.

동시성 문제

멀티스레딩 기능을 활용한다면, 당연히 동시성 문제도 피해갈 수 없습니다.
이를 해결하기 위한 기존에 활용하던 첫번째 방법은 Synchronized를 활용하는 것입니다.

아래 코드와 같이 Synchronized Collection을 활용한다면,
하나의 스레드가 해당 Collection을 점유하는 시점에 Lock을 걸어 Thread Safe하게 작동될 수 있도록 합니다.

List<Interger> evenNumbers = Collections.synchronizedList(new ArrayList());




하지만, 스레드가 점유를 풀 때 Lock-Unlock 작업이 반복되기 때문에 성능이 떨어집니다.
(멀티코어 CPU의 각 코어는 별도의 캐시를 포함하고 있는데, 락을 사용하면 캐시의 동기화를 위해 속도가 느린 캐시 일관성 프로토콜 인터코어 통신이 이루어집니다.)
그래서 이를 보완하기 위한 방법으로 Stream 라이브러리의 Collect 메서드를 호출하는 것입니다.

List<Integer> evenNumbers = numbers.parallelStream()
                                .filter(number -> number % 2 == 0)
                                .collect(Collectors.toList());




Collect 메서드는 병렬처리와 동시성 여부를 확인하는 로직이 포함되어 있기 때문에,
이를 활용한다면 parallelStream이 종료되는 시점에 각 스레드에서 return되는 값들을 List로 수집하여 동시성 문제로부터 안전하게 값을 받아볼 수 있습니다.

이어서

지금까지 Java 8의 개요와 새로 추가된 기능들에 대해 간략히 알아보았습니다.
이어서 각 기능별로 어떻게 동작하는지나 활용해야 하는지 등에 대해서 자세히 알아보겠습니다.

Reference

반응형
728x90
반응형

앞선 👉에서 JDK에 대해 알아보았다.
이제 본 글에서는 Java 프로그램을 구동시켜주는 JVM에 대해 상세히 알아보고자 한다.

위 사진은 JVM 아키텍처를 나타낸다.
예제코드를 작성하고, JVM이 해당 예제코드를 어떻게 실행시키는지 보면서 위 아키텍처를 설명해보겠다.

예제 세팅

예제코드는 아래와 같다.

  • Main.java
public class Main {
    public static void main(String[] args) {
        Woong woong = new Woong();
        woong.methodA(3);
    }
}
  • Woong.java
public class Woong {

    public int methodA(int param) {
        int localVariable = 1;
        int sum = localVariable + param;
        methodB();
        return sum;
    }

    private void methodB() {

    }
}

위 Java코드를 컴파일하여 class파일을 생성해보겠다.
아래 명령어를 실행하면 Main.classWoong.class 두 개의 클래스 파일이 생성될 것이다.
javac Main.java Woong.java

생성된 클래스 파일을 HexDHex Viewer로 보면 직접 Byte형태로 볼 수 있지만,
사람이 이해하기 어려운 문법이므로 역어셈블이라는 과정을 거쳐 사람이 이해하기 쉬운 형태로 변환해보겠다.
javap -v -p -s Main.class, javap -v -p -s Woong.class

그리고 아래는 확인할 수 있는 형태의 바이트 코드이다.

  • Main.class
Classfile /Users/daewoong/JavaStudy/src/Main.class
  Last modified 2023. 7. 23.; size 318 bytes
  SHA-256 checksum de5de7f896a34cb4c31a490af4edcf325015e7050ecbb9d0bfeb3bda7854859d
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 63
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #14                         // Main
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // Woong
   #8 = Utf8               Woong
   #9 = Methodref          #7.#3          // Woong."<init>":()V
  #10 = Methodref          #7.#11         // Woong.methodA:(I)I
  #11 = NameAndType        #12:#13        // methodA:(I)I
  #12 = Utf8               methodA
  #13 = Utf8               (I)I
  #14 = Class              #15            // Main
  #15 = Utf8               Main
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               SourceFile
  #21 = Utf8               Main.java
{
  public Main();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #7                  // class Woong
         3: dup
         4: invokespecial #9                  // Method Woong."<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_3
        10: invokevirtual #10                 // Method Woong.methodA:(I)I
        13: pop
        14: return
      LineNumberTable:
        line 3: 0
        line 4: 8
        line 5: 14
}
SourceFile: "Main.java"
  • Woong.class
Classfile /Users/daewoong/JavaStudy/src/Woong.class
  Last modified 2023. 7. 23.; size 322 bytes
  SHA-256 checksum 9709d10e0bfcde2dd1b9477a8de2f210e322181f9f42835d3c3898b7e213f904
  Compiled from "Woong.java"
public class Woong
  minor version: 0
  major version: 63
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // Woong
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Methodref          #8.#9          // Woong.methodB:()V
   #8 = Class              #10            // Woong
   #9 = NameAndType        #11:#6         // methodB:()V
  #10 = Utf8               Woong
  #11 = Utf8               methodB
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               methodA
  #15 = Utf8               (I)I
  #16 = Utf8               SourceFile
  #17 = Utf8               Woong.java
{
  public Woong();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public int methodA(int);
    descriptor: (I)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: iconst_1
         1: istore_2
         2: iload_2
         3: iload_1
         4: iadd
         5: istore_3
         6: aload_0
         7: invokevirtual #7                  // Method methodB:()V
        10: iload_3
        11: ireturn
      LineNumberTable:
        line 4: 0
        line 5: 2
        line 6: 6
        line 7: 10

  private void methodB();
    descriptor: ()V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 12: 0
}
SourceFile: "Woong.java"

위 바이트코드는 크게 세 가지 정보로 분류된다.

  • 클래스 정보
  • Constant Pool
  • Instruction Set

우리는 Constant PoolInstruction Set에만 집중하여 JVM을 파악해볼 것이다.
자세한 바이트 코드 정보는 Oracle Doctistory 글에서 확인할 수 있다.

JVM 동작 원리

JVM의 목적 : 바이트코드 형태의 Class 파일을 컴퓨터가 읽을 수 있는 기계어로 번역하여 CPU에 명령을 내림

Java 프로그램이 실행되면 JVM은 먼저 Class Loader를 통해 Class파일을 읽는다.
Class Loader에 의해 읽혀진 Class파일은 검증과정과 초기화과정(static 변수 초기화 등)을 거쳐 Runtime Data AreasMethod Area라는 메모리 공간에 올려진다.

메모리 공간에 올려진 Class 파일의 바이트코드는 Execution EngineInterpreterJIT Compiler에 의해 기계어로 번역되어 CPU에 전달된다. 그리고 기계어로 번역될 프로그램 동작과 관련한 정보는 Runtime Data Areas 저장되고, 실시간으로 저장된 정보가 인터프리터에 의해 기계어로 번역되어 CPU로 전달될 것이다.

이제 Runtime Data Areas의 원리에 집중하면서 JVM의 동작을 살펴볼 것이다.

Runtime Data Areas

Runtime Data Areas는 크게 다섯가지 공간으로 분류된다.

  1. Method Area : 클래스에 대한 정보 저장 (스레드 공유 공간)
  2. Heap : 런타임에 생성되는 모든 객체들의 대한 정보 저장 (스레드 공유 공간)
  3. JVM Stacks : 메서드를 실행하기 위한 정보들이 저장되는 공간, Frame 자료구조 활용 (스레드당 1개)
  4. PC Registres : 현재 실행되고 있는 명령의 주소를 저장 (스레드당 1개)
  5. Native Method Stacks : C나 C++로 작성된 메서드를 실행할 때 사용되는 Stack (스레드당 1개)

JVM Stacks

Class의 메인 메서드가 실행되거나 스레드가 생성되면 JVM Stacks은 하나씩 생성된다.

JVM Stacks은 위 사진처럼 구성되어 있다.
생성된 스레드에서 메서드가 호출될 때마다 메서드 동작에 대한 정보가 Frame 자료구조 형태로 생성되어 JVM Stacks에 쌓인다.
Frame형태로 쌓인 메서드의 동작이 끝나거나 Exception이 발생하면 해당 Frame은 pop된다.

Frame 자료구조에 대해 자세히 알아보자.

  • Local Variables Array : 실행된 메서드의 지역변수의 공간이 해당 배열에 생성된다. 선언된 순서대로 1번 인덱스부터 할당된다. 0번 인덱스는 this로 자기자신을 가리킨다.
  • Operand Stack : Instruction Set에 따른 피연산값 및 연산의 중간값들을 저장하는 Stack
  • Constant Pool : 클래스 내에서 사용되는 상수(constant)들을 담은 테이블

Constant Pool은 Class 파일 상의 Constant Pool의 데이터를 가리킨다.
아래는 Main.class의 Constant Pool 이다.

   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // Woong
   #8 = Utf8               Woong
   #9 = Methodref          #7.#3          // Woong."<init>":()V
  #10 = Methodref          #7.#11         // Woong.methodA:(I)I
  #11 = NameAndType        #12:#13        // methodA:(I)I
  #12 = Utf8               methodA
  #13 = Utf8               (I)I
  #14 = Class              #15            // Main
  #15 = Utf8               Main
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               SourceFile
  #21 = Utf8               Main.java

#2 = Class #4로 예를들면,
#2 = Class 부분은 Index 및 Type을 가리키고 #4는 참조값을 가리킨다.
#2인덱스는 #4인덱스를 참조하고 있고, #4인덱스는 java/lang/Object를 가리키고 있으므로,
#2인덱스는 java/lang/Object를 가리키고 있다고 봐도 무방하다.

Instruction Set에 따른 JVM 동작

JVM 아키텍처의 각 모듈에 대해 알아보았으니, 이제 동작원리를 알아볼 것이다.

  1. 클래스로더에 의해 Class 파일이 Method Area에 올라온다. (스레드가 실행되면서 필요한 클래스들은 필요할 때마다 동적으로 클래스로더에 의해 불려진다.)
  2. JVM은 Class 파일을 해석하여 Instruction Set의 순서대로 프로그램을 동작시킨다.
         0: new           #7                  // class Woong
         3: dup
         4: invokespecial #9                  // Method Woong."<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_3
        10: invokevirtual #10                 // Method Woong.methodA:(I)I
        13: pop
        14: return
         0: iconst_1
         1: istore_2
         2: iload_2
         3: iload_1
         4: iadd
         5: istore_3
         6: aload_0
         7: invokevirtual #7                  // Method methodB:()V
        10: iload_3
        11: ireturn

주요 Instruction Set만 알아볼 것이다.

  • new : Constant Pool의 #7 인덱스에 해당하는 클래스의 인스턴스를 생성한다. Method Area상의 해당 클래스의 사이즈를 계산하고 Heap 메모리를 할당한다. 그리고 할당된 Heap 메모리에 대한 참조값이 지역변수에 저장된다. (Method Area에 해당 클래스가 없을 경우, 클래스 로더에 의해 Method Area로 해당 클래스가 불려진다.)
  • iconst_1 : 정수값 1을 Operand Stack에 올린다.
  • istore_2 : Operand Stack에서 값을 꺼내서 Local Variables Array의 2번 인덱스에 저장한다.
  • iload_2 : Local Variables Array의 2번 인덱스 값을 Operand Stack에 올린다.
  • iadd : Operand Stack상단 두 값을 더한 후에 다시 Operand Stack에 저장한다.
  1. Instruction Set의 각 명령어마다 인터프리터에 의해 기계어로 번역되어 실시간으로 실행된다.

클래스 로더에 의해 로드된 Class 파일은 JVM에 의해 위 Flow대로 실행된다.

Garbage Collection

GC(Garbage Collection)는 JVM 메모리를 자동으로 관리해준다. 객체 생명주기에 따라서 자동으로 메모리를 해제시켜주는데,
해당 GC의 원리는 다음 연재될 글에서 살펴보겠다.

반응형
728x90
반응형

Java 기반의 프로그램을 개발하기 전에 앞서서 필수적으로 설치해야하는 패키지가 있다.
바로 JDK이다.

JDK는 Java Development Kit의 약자로서, Java 기반 프로그램 개발에 필요한 필수적인 요소들이 담긴 패키지라고 할 수 있다.

JDK를 설명하라고 한다면, 위 사진 하나 업로드 해놓고 설명을 끝낼 수 있을 정도로 명확하다.
그래도 하나씩 살펴보자.

JVM

JVM은 Java Virtual Machine의 약자이다.
Class 파일 형식의 Java 프로그램을 실행시켜주는 하나의 프로그램이다.

여기서 두 가지가 궁금해질 것이다.

첫번째는 JVM이 어떻게 Class 파일을 읽어서 Java 프로그램을 구동시켜주는지에 대한 부분이다.
해당 부분은 👉다음 연재될 글에서 살펴볼 것이다.

두번째는 과연 Class파일이 뭔지에 대해 궁금해질 것이다.

Class파일이란? JVM이 읽을 수 있도록 Java 언어로 구성된 코드를 컴파일하여 생성한 파일이다.
해당 Class파일은 Java 컴파일러가 만들 수 있고, Java 코드 내 Class 단위마다 파일 하나씩 바이트코드로 생성한다.

JRE

JRE는 Java Runtime Environment의 약자이다.
JVM의 상위 개념인데, JVM과 Java 필수 라이브러리(ex. java.lang, java.util 등)로 구성되어 있다.


JDK

JDK는 앞서 언급한 것처럼 Java 개발에 필수적인 요소가 포함되어 있다.
하위개념인 JRE과 개발에 필요한 Java컴파일러, Java디버거, Java Heap 등과 같은 Tool을 종합하여 패키징한 것이 JDK이다.

그래서 Java 개발환경을 세팅한다고 하면,
JDK와 IDE (ex. Eclipse, IntelliJ) 두 가지를 설치하고 IDE에 Java 프로젝트를 생성한 후에 JDK를 연결하는 일련의 과정을 거치면 된다.

반응형

+ Recent posts