[Java] 버전에 관하여

문설민·2024년 1월 10일

Java

목록 보기
1/1
post-thumbnail

우리는 백엔드 개발을 하기 위하여 여러 가지 프로그래밍 언어를 사용한다. 그 중에서 현재 한국에서 가장 많이 사용되는 언어는 Java라고 할 수 있다. 그렇지만 Java로 개발을 하기 위해서 가장 먼저 어떤 버전을 사용할 것인가를 정해야 하는데 이를 위해서 우리는 어떠한 기준으로 버전을 골라야 하는가 그리고 각각의 버전의 특징은 어떤 것이 있는가를 알아볼 필요가 있다.

Java의 버전

Java는 1995년 정식으로 출시한 이후에 수많은 버전들이 있어왔다. 지금까지도 자바는 6개월마다 새로운 버전이 나오고 있다. 이러한 버전들 중에서 LTS(Long Term Support) 라고 하는 장기 지원 버전이 있는데 이는 다른 버전보다 출시 이후 오랜 기간 보안 업데이트와 버그 수정을 지원할 것이라고 선언한 버전이다. 프로그래밍에서 안정성은 가장 중요한 부분 중의 하나이기 때문에 이를 위해서 LTS를 주로 이용하게 된다. 현재 그 중에서 가장 많이 쓰이는 버전은 8, 11, 17 버전이다. 이에 더해서 가장 최근에 출시된 21 버전까지 어떠한 특징들을 가지고 있는지 알아보자.

Java 8

  • Oracle이 Java를 인수한 이후 첫 번째 LTS
  • 32비트를 지원하는 공식적인 마지막 버전
  • OracleJDK(유료)와 OpenJDK(무료)로 나뉨
  • Lambda Expression
  • Stream API
  • interface default method
  • Optional
  • new Date and Time API

Lambda Expression

  • 메소드를 지칭하는 명칭없이 구현부를 선언하는 익명 메소드 생성 문법
Runnable runnable = new Runnable(){
	@Override
    public void run(){
    	System.out.println("Hello World");
    }
};

이전 버전에서는 위와 같이 별도의 익명 클래스를 만들어서 선언하던 방식이 Lambd Expression을 사용하면 다음과 같이 변경된다.

Runnable runnable = () -> System.out.println("Hello World");

Stream API

  • 데이터 컬렉션 반복을 처리하는 기능
  • 모호함과 반복적인 코드 문제 그리고 멀티코어 활용의 어려움을 해결
List<String> list = Arrays.asList("franz", "ferdinand", "fiel", "vom", "pferd");
list.stream()
    .filter(name -> name.startsWith("f"))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);

interface default method

  • interface에 default method 추가
  • default method가 구현된 interface 또한 상속 가능
public interface Soccer{
    public default void doShooting(int n){
        System.out.println("doShooting(Shooting)");
    }
}

public interface SoccerChild extends Soccer{
}

Optional

  • NPE(Null Pointer Exception)에 대응하기 위한 구조체
String value = null;
Optional<String> valueOpt = Optional.ofNullable(value);
String result = valueOpt.orElseThrow(Exception::new).toUpperCase();

new Date and Time API

  • Date와 Calendar 클래스의 기능 부족과 비 표준적인 명명 규칙, 그리고 일관되지 못한 속성 값의 문제를 해결하기 위해 새로운 날짜 API가 추가

Java 11

  • OracleJDK와 OpenJDK의 통합
  • OracleJDK가 구독형 유료 모델로 전환
  • JDK의 서드 파티로의 이전 필요
  • G1 GC(Garbage Collection)가 기본 GC로 설정
  • String & Files 새로운 method 추가
  • Lambda 지역 변수 사용법 변경

String & Files 새로운 method 추가

  • 기존의 trim() 은 유니코드의 다양한 공백 문자를 처리하기 위해 복잡하던 과정을 거쳤으나 strip() 으로 간편하게 처리
"Marco".isBlank();
"Mar\nco".lines();
"Marco  ".strip();

Path path = Files.writeString(Files.createTempFile("helloworld", ".txt"), "Hi, my name is!");
String s = Files.readString(path);

Lambda 지역 변수 사용법 변경

  • 10 버전에서 var 키워드로 변수를 선언하면 타입 추론 객체를 생성할 수 있는 기능이 추가되고 이를 Lamda Expression 에서도 선언 가능
(var X, var Y) -> X + Y

Java 17

  • 애플 M1 및 이후 프로세서 탑재 제품군에 대한 정식 지원
  • 난수 생성 API 추가
  • 봉인 클래스(Sealed Class) 정식 추가
  • recode class 키워드 사용 가능
  • String 여러줄 사용시 텍스트 블록 기능 사용 가능
  • NumberFormat,DateTimeFormatter 기능 향상
  • Stream.toList() 사용 가능
  • NullPointerException이 어떤 변수에 의해 발생했는지 설명
  • 기존의 GC보다 개선된 ZGC 도입

Sealed Class

  • 상속 가능한 클래스를 지정할 수 있는 클래스
  • 상속 가능한 대상은 상위 클래스 또는 인터페이스 패키지 내에 존재
public abstract sealed class Shape
	permits Circle, Rectangle, Square {...}

record Class

  • Java에서 많은 사용구를 작성하는 불편을 해소
final class Point{
    public final int x;
    public final int y;

    public Point(int x, int y){
        this.x = x;
        this.y = y;
    }
}

위와 같은 형식을 record 를 사용하면 다음과 같이 작성 가능

record Point(int x, int y) {}

Text Block

  • 여러 줄의 문자열을 입력 가능
String str = "Hello\n"
	+ "Hello\n"
    + "Hello";
String str = """Hello
		Hello
        Hello""";

NullPointerException

author.age = 35;
---
Exception in thread "main" java.lang.NullPointerException:
     Cannot assign field "age" because "author" is null

Java 21

  • Sequenced Collection
  • Virtual Threads
  • Record Patterns
  • Pattern Matching for Switch
  • KEM API

Sequenced Collection

  • 기존의 컬렉션 프레임워크는 일관되지 못한 방법으로 원소에 접근
  • 이를 해소하기 위해 컬렉션을 표현하는 새로운 인터페이스 도입
  • 정해진 순서의 원소에 접근하고 역순으로 처리하기 위한 일관된 API

Virtual Threads

  • Virtual Thread 는 처리량이 많은 동시성 애플리케이션을 개발하고 모니터링하고 유지 및 관리하는데 드는 비용을 획기적으로 줄여줄 경량 쓰레드
  • 기존에는 멀티쓰레드 모델을 기본적인 동시성 처리 방법으로 사용해왔고, 이로 인해 I/O 요청이 들어오면 쓰레드가 블로킹되면서 자원이 낭비

Record Patterns

21 이전의 record class 에서의 instanceof 연산자 사용

record Point(int x, int y) {}

static void printSum(Object obj) {
    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x+y);
    }
}

21 이후의 사용

static void printSum(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x+y);
    }
}

Pattern Matching for Switch

  • switch 문에서의 패턴 매칭 개선
  • intstaceof 사용 , null 검사 , case 세분화 , enum 을 개선

instatceof 사용

// 21 이전
static String formatter(Object obj) {
    String formatted = "unknown";
    if (obj instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (obj instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (obj instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (obj instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

// Java 21
static String formatterPatternSwitch(Object obj) {
    return switch (obj) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> obj.toString();
    };
}

null 검사

// 21 이전
static void testFooBarOld(String s) {
    if (s == null) {
        System.out.println("Oops!");
        return;
    }
    switch (s) {
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}
// Java 21
static void testFooBarNew(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

case 세분화

// 21 이전
static void testStringOld(String response) {
    switch (response) {
        case null -> { }
        case String s -> {
            if (s.equalsIgnoreCase("YES"))
                System.out.println("You got it");
            else if (s.equalsIgnoreCase("NO"))
                System.out.println("Shame");
            else
                System.out.println("Sorry?");
        }
    }
}

// Java 21
static void testStringNew(String response) {
    switch (response) {
        case null -> { }
        case String s
        when s.equalsIgnoreCase("YES") -> {
            System.out.println("You got it");
        }
        case String s
        when s.equalsIgnoreCase("NO") -> {
            System.out.println("Shame");
        }
        case String s -> {
            System.out.println("Sorry?");
        }
    }
}

enum 개선

// 21 이전
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
final class Tarot implements CardClassification {}

static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) {
    switch (c) {
        case Suit s when s == Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit s when s == Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit s when s == Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit s -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

// 21
static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
    switch (c) {
        case Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit.SPADES -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

KEM API

  • 공개 키 암호화를 사용하여 대칭 키를 보호하는 암호화 기술인 KEM(Key Encapsulation Mechanism) API 가 도입
package javax.crypto;

public class DecapsulateException extends GeneralSecurityException;

public final class KEM {

    public static KEM getInstance(String alg)
        throws NoSuchAlgorithmException;
    public static KEM getInstance(String alg, Provider p)
        throws NoSuchAlgorithmException;
    public static KEM getInstance(String alg, String p)
        throws NoSuchAlgorithmException, NoSuchProviderException;

    public static final class Encapsulated {
        public Encapsulated(SecretKey key, byte[] encapsulation, byte[] params);
        public SecretKey key();
        public byte[] encapsulation();
        public byte[] params();
    }

    public static final class Encapsulator {
        String providerName();
        int secretSize();           // Size of the shared secret
        int encapsulationSize();    // Size of the key encapsulation message
        Encapsulated encapsulate();
        Encapsulated encapsulate(int from, int to, String algorithm);
    }

    public Encapsulator newEncapsulator(PublicKey pk)
            throws InvalidKeyException;
    public Encapsulator newEncapsulator(PublicKey pk, SecureRandom sr)
            throws InvalidKeyException;
    public Encapsulator newEncapsulator(PublicKey pk, AlgorithmParameterSpec spec,
                                        SecureRandom sr)
            throws InvalidAlgorithmParameterException, InvalidKeyException;

    public static final class Decapsulator {
        String providerName();
        int secretSize();           // Size of the shared secret
        int encapsulationSize();    // Size of the key encapsulation message
        SecretKey decapsulate(byte[] encapsulation) throws DecapsulateException;
        SecretKey decapsulate(byte[] encapsulation, int from, int to,
                              String algorithm)
                throws DecapsulateException;
    }

    public Decapsulator newDecapsulator(PrivateKey sk)
            throws InvalidKeyException;
    public Decapsulator newDecapsulator(PrivateKey sk, AlgorithmParameterSpec spec)
            throws InvalidAlgorithmParameterException, InvalidKeyException;

}

현재 가장 많이 사용하는 버전은?

지금까지 주로 사용된다고 보여지는 8, 11, 17, 21 버전들의 특징들을 알아보았다. 그렇다면 현재 어떤 버전이 가장 많이 사용될까? 이를 알아보기 위해서 JetBrains 의 개발자 에코시스템 보고서 2023의 자바의 버전 사용에 관한 부분을 보자.

위의 그래프에서 볼 수 있듯이 현재 가장 많이 사용된다고 가장 오래된 8 버전이다. 그 다음으로는 17, 11 버전이 위치하고 있는 것을 볼 수 있다. 가장 최근 출시된 21 버전을 제외한 LTS 버전인 8, 17, 11 이 가장 많은 사용자를 보유한 것은 충분히 납득이 되는 결과이다. 그러나 기능이 가장 많고 개선된 17 버전이 아니라 8 버전의 사용자가 가장 많은 것일까?

그 이유로는 여러 가지가 있겠지만 나의 생각으로는 레거시 코드와의 호환성과 개발자의 경험이 주된 이유라고 생각이 된다. 기존에 Java 8을 사용하던 프로젝트나 레거시 코드와의 호환성을 유지해야 하는 경우에는 레거시 시스템을 유지보수하거나 통합해야 하는 경우 Java 17 으로 업그레이드 하는 것에 어려움을 초래할 수 있어 Java 8을 유지하는 것이 적절할 수 있다. 프로젝트 팀의 개발자들이 Java 8에 더 익숙하고 경험이 있을 경우 Java 8을 선택하는 것이 더 효율적일 수 있다.

어떤 버전을 사용해야 할까?

그렇다면 나는 어떤 버전을 사용해야 할까? 가장 기능이 많고 최신 버전인 Java 21 일까? 아니면 가장 사용자가 많은 Java 8 일까? 그것은 지금 어떠한 프로젝트를 수행할 것인가에 달려있다고 말할 수 있다. 예를 들어 안정성이 중요시되고 기존의 레거시 코드와 호환을 위해서라면 Java 8 을 사용해야 한다. 다른 예시로는 Spring Boot 3.0 이상의 환경에서 개발을 하기 위해서는 Java 17 이상의 버전을 사용해야 한다. 이처럼 버전은 개발자가 처한 환경 그리고 개발자의 경험에 따라서 유기적으로 선택해야 한다고 할 수 있다.

마무리

개인 프로젝트에 들어가기 전에 내가 어떠한 버전을 사용해야 하는지 궁금증이 생겨서 정리할 겸 글을 쓰게 되었는데 생각보다 많은 부분에 대해 알 수 있었다. Java 가 변화해온 과정들을 보면서 몰랐던 다양한 기능들에 대해 알게 되고 어떠한 언어를 지향하는 지에 대해서 느낄 수 있었다. 그리고 최종적으로 내가 어떠한 버전을 사용해야 하는지에 대해서도 여러 근거를 생각하게 하고 그로 인해 결정을 내릴 수 있게 되었다. 첫 글인지라 부족한 부분도 많고 만족스럽지 않은 부분도 많지만 앞으로 차근 차근 길을 밟아나가며 노력해야겠다.

참고

0개의 댓글