Java 8 vs 11 vs 17

HenryHong·2023년 6월 13일
0

https://velog.io/@ililil9482/Java17을-고려해야할까

https://techblog.gccompany.co.kr/우리팀이-jdk-17을-도입한-이유-ced2b754cd7

Java 9 to Java 11


LTS

Oracle

Java 8 : 2030년 12월까지
Java 11 : 2026년 9월까지
Java 17 : 2029년 9월까지

Jigsaw

기존 java의 jar 패키징을 통해 모듈화를 진행하고 있지만 jar에는 문제점이 존재한다. 우선 Jar Hell이 있는데 복잡한 ClassLoader로 정의되었을 때 JVM 컴파일은 성공하지만 런타임시 ClassNotFonundException을 마주하게 된다는 것이다. 또한 Jar는 생각보다 무겁다.

그래서 jigsaw라는 새로운 모듈화를 통해 가볍고 복잡하지 않은 java 모듈 시스템을 구축한 것이다. 특히 라즈베리 파이 같은 저사양 컴퓨터에서 잘 실행될 수 있도록 구조를 잡았다고 하니 앞으로 더 범용성이 커질지도 모른다.

jigsaw를 활용하여 프로젝트를 모듈화 + 분리하여 개발하는 방법의 방법론은 따로 없는듯하다. 필자는 개인적으로 프로젝트 관점에서 MSA 아키텍쳐와 유사하게 java 모듈 단위에서의 분리를 하기 위한 기능이라고 느꼈다.

New GC

JDK 11부터 공개되었고, “Stop-The-World”로 인한 성능저하를 개선하기 위한 목적을 가지고 Oracle에서 개발하였습니다.

※ Stop-The-World는 가비지 컬렉션(Garbage Collection) 과정 중에 발생하는 일시적인 정지 현상을 의미합니다. 가비지 컬렉션은 사용하지 않는 객체를 메모리에서 해제하여 가용한 메모리를 확보하는 작업입니다. 하지만 이 작업을 수행하기 위해서는 애플리케이션의 실행을 일시적으로 멈추어야 합니다. 이 때, Stop-The-World 현상이 발생합니다. ( Full GC / Young Generation GC )

동작 원리

  • Pause Mark Start: Colored pointers 알고리즘을 기반해 ZGC Root에서 가리키는 객체의 상태값을 Mark(저장) 합니다.
  • Pause Mark End: 새로 들어온 객체를 대상으로 Mark가 일어나고, ZPage(ZGC에서 다루는 영역)를 찾아 RelocationSet()에 배치합니다.
  • Pause Relocate Start: Root 참조 객체에 대한 재배치를 하며, 이후 Load barriers 알고리즘을 통해 모든 객체를 안전하게 업데이트합니다.

특징

  • 대기 시간이 짧은 Application에 적합한 Garbage Collection입니다.
  • 처리 시간이 10ms를 초과하지 않아 짧은 지연시간을 보장합니다.
  • 8MB부터 16TB까지의 Heap 크기를 지원합니다.

개선 사항

  1. 병렬 Full GC 개선: JDK 11에서는 Full GC 과정에서 병렬 처리(Thread가 실행 중일 때 동시 작업을 수행)를 더욱 향상시켰습니다. 이로 인해 Full GC 시간이 감소하고 애플리케이션의 중단 시간이 단축됩니다.
  2. 애플리케이션 중단 시간 최적화: JDK 11에서는 G1 GC의 중단 시간 최적화를 위해 다양한 향상 사항이 도입되었습니다. 이로 인해 G1 GC의 Stop-The-World 시간이 줄어들고 애플리케이션의 응답성이 향상됩니다.
  3. 메모리 할당 최적화: JDK 11에서는 G1 GC의 메모리 할당 동작을 최적화하였습니다. 이로 인해 객체 할당과 해제에 필요한 작업이 최소화되고, 메모리 사용량이 최적화됩니다.
  4. 효율적인 GC 로그 기능: JDK 11에서는 G1 GC의 로깅 기능이 개선되었습니다. G1 GC 로그는 GC 작업에 대한 정보를 제공하며, JDK 11에서는 더 많은 세부 정보를 포함하고 더 효율적인 로그 파일 생성을 지원합니다.

적용 방법

  • Java Application 실행 시 다음 옵션 실행 (기본 설정: G1GC)
java -XX:+UseZGC -jar Application.java

Thread local handshake

GC 실행 전 우선 발생하는 "STOP-THE-WORLD" 발생 시 이전에는 모든 Thread가 동시에 중단이 되었다면, 이제는 Thread 개별로 중단 가능

JVM 메모리할당

HotSpot JVM 이 사용자가 지정한 대체 메모리 장치 또는 서로 다른 메모리장치를 이용해서 JVM Heap 영역의 메모리를 할당

Collection

List, Set, Map 인터페이스에 immutable 생성을 할 수 있는 새로운 Method 추가

List<Integer> mutable = Arrays.asList(1, 2, 3);
List<Integer> immutable = List.of(1, 2, 3);

mutable.set(0, 4);      // OK
immutable.set(0, 4);    // UnsupportedOperationException
  • Before
Set<String> set = new HashSet<>();
set.add("제이든");
set.add("Jayden");

List<String> list = new ArrayList<>();
list.add("제이든");
list.add("버나드");
list.add("자이노");
list.add("메이슨");
list.add("엘빈");

Map<String, String> map = new HashMap<>();
map.put("J","Jayden");
map.put("B","Bernard");
map.put("Z","Zino");
map.put("M","Mason");
map.put("E","Elvin");
  • After
Set<String> set = Set.of("제이든", "Jayden", "개발3팀");

List<String> list = List.of("제이든", "버나드", "자이노", "메이슨", "엘빈");

Map<String, String> map
   = Map.of(
   "J", "Jayden",
   "B", "Bernard",
   "Z", "Zino",
   "M", "Mason",
   "E", "Elvin");

Interface private method

기존 interface는 추상 메서드만을 정의할 수 있었는데 default 선언을 통해 method를 미리 정의해둘 수 있다. 또한 정의된 default method는 상속된 class에서 오버라이딩 될 수 있다.

public interface Calculator {
	public int plus(int i, int j);
	public int multiple(int i, int j);
	default int exec(int i, int j){      //default로 선언함으로 메소드를 구현할 수 있다.
		return i + j;
	}
}

//Calculator인터페이스를 구현한 MyCalculator클래스
public class MyCalculator implements Calculator {

	@Override
	public int plus(int i, int j) {
		return i + j;
	}

	@Override
	public int multiple(int i, int j) {
		return i * j;
	}
}

public class MyCalculatorExam {
	public static void main(String[] args){
		Calculator cal = new MyCalculator();
		int value = cal.exec(5, 10);
		System.out.println(value);
	}
}

다음과 같이 default method로 선언되면 상속받은 class에서 따로 구현하지 않아도 exec method를 사용할 수 있으며 필요시 오버라이드하여 사용할 수 있다.

optional

기존 ifPresent Method 경우 Optional 객체가 값을 담고 있는 경우만 처리를 하였으나, 추가 된 ifPresentOrElse Method는 해당 객체가 값이 없을 경우 처리할 내용까지 정의가 가능

// 주문할 커피 : latte
Order order=new Order("latte");
Optional.ofNullable(order)
        .map(Order::getCoffee)
        .ifPresent(coffee -> System.out.println("만들 커피: " + coffee));

// result - 만들 커피: latte
Optional.ofNullable(order)
        .map(Order::getCoffee)
        .ifPresentOrElse(
                coffee -> System.out.println("만들 커피: " + coffee),
                () -> System.out.println("만들 커피: Ex")
        );

// result - 만들 커피: latte

//주문할 커피 : x
Order order2=new Order();
Optional.ofNullable(order2)
                .map(Order::getCoffee)
                .ifPresent(coffee -> System.out.println("만들 커피: " + coffee));

// result - X

Optional.ofNullable(order2)
        .map(Order::getCoffee)
        .ifPresentOrElse(
                coffee -> System.out.println("만들 커피: " + coffee),
                () -> System.out.println("만들 커피: 없음")
        );

// result - 만들 커피: 없음

HttpClient

http2를 구현하는 신규 클라이언트 API 제공, 기존 HttpURLConnection API 대체 가능

https://brush-up.github.io/java/java11-http-client/

Reactive Stream


Non-Blocking Backpressure를 이용한 비동기 스트림 처리 지원 API 추가

https://www.getoutsidedoor.com/2020/11/23/reactive-streams-에-대해서/

var

로컬 변수 선언 시, “타입 추론”을 이용하여 명시적 타입 선언 없이도 변수 선언이 가능하도록 지원하는 신규 Keyword입니다.

기존 Lombok Library에서 지원하던 기능이었으나, JDK 10 버전에 등장하며Java에서 공식 지원하였습니다. 이후 LTS 버전인 JDK 11부터는 람다 타입에서도 사용 가능하도록 지원하고 있습니다.

컴파일 시 변수 타입을 추론하기 때문에 성능에 영향을 주지는 않습니다. 그러나 가독성 높은 코드 작성을 위해 무분별한 “var” Keyword 이용은 지양하는 것이 좋겠습니다.

제약

  • 멤버 변수 또는 Method 파라미터로 선언은 불가능하며, 오로지 Method 내 로컬 변수로만 선언 가능합니다.
// Java 10 이전
String name = "juno";

// Java 10 이후
var name = "juno";
var num = 1;

활용법 예시

  1. 긴 타입 이름을 간결하게 표현할 때: var를 사용하면 긴 타입 이름을 생략하고 코드를 더 간결하게 표현할 수 있습니다. 예를 들어, List<String> names = new ArrayList<>(); 대신 var names = new ArrayList<String>();와 같이 사용할 수 있습니다.
  2. 복잡한 제네릭 타입을 다룰 때: 제네릭을 사용하는 경우 타입 이름이 복잡해질 수 있습니다. var를 사용하면 복잡한 제네릭 타입을 간결하게 표현할 수 있습니다. 예를 들어, Map<String, List<Integer>> map = new HashMap<>(); 대신 var map = new HashMap<String, List<Integer>>();와 같이 사용할 수 있습니다.
  3. 익명 클래스 또는 람다 표현식의 타입을 추론할 때: 익명 클래스나 람다 표현식을 사용하는 경우 var를 사용하여 타입을 간결하게 표현할 수 있습니다. 예를 들어, Runnable runnable = () -> System.out.println("Hello, World!"); 대신 var runnable = (Runnable) () -> System.out.println("Hello, World!");와 같이 사용할 수 있습니다.
  4. 반복문에서 컬렉션 요소의 타입 추론할 때: 반복문에서 컬렉션의 요소를 타입 추론하여 사용할 수 있습니다. 예를 들어, for (String name : names) 대신 for (var name : names)와 같이 사용할 수 있습니다.

var을 사용할 때 주의해야 할 점은 변수의 타입이 명시적으로 선언되지 않기 때문에 가독성이나 코드 이해에 영향을 줄 수 있다는 점입니다. 따라서 변수의 용도와 의도를 명확하게 전달하기 위해 타입 이름을 명시적으로 선언하는 것이 좋습니다. var는 코드를 간결하게 만들기 위해 사용되는 도구이지만, 적절하게 활용하는 것이 중요합니다.

Java 12 to Java 17


LTS

텍스트 블록

//Java 13 이전
String htmlBeforeJava13 = *"<html>\n"* +
              *"    <body>\n"* +
              *"        <p>Hello, world</p>\n"* +
              *"    </body>\n"* +
              *"</html>\n"*;
//Java 13 이후
String htmlWithJava13 = *"""
              <html><body><p>Hello, world</p>
                  </body>
              </html>
              """*;

Switch

//java 12 이전
String time;
switch (weekday) {
	case MONDAY:
	case FRIDAY:
		time = "10:00-18:00";
		break;
	case TUESDAY:
	case THURSDAY:
		time = "10:00-14:00";
		break;
	default:
		time = "휴일";
}
//java12 이후
String time = switch (weekday) {
	case MONDAY, FRIDAY -> "10:00-18:00";
	case TUESDAY, THURSDAY -> "10:00-14:00";
	default -> "휴일";
};
//java14
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    default      -> {
      String s = day.toString();
      int result = s.length();
      yield result;
    }
};
//java17
public String test(Object obj) {

    return switch(obj) {
      case Integer i -> *"An integer"*;
      case String s -> *"A string"*;
      case Cat c -> *"A Cat"*;
      default -> *"I don't know what it is"*;
    };
}

Record

Record Data Class란, JDK 14 버전부터 공개된 Immutable 객체를 생성하는 새로운 유형의 클래스입니다.

  • Record 선언을 하게 되면 기존 toString, equals, hashCode 메소드를 자동으로 구현해주며, 모든 인스턴스 필드를 초기화해주는 생성자가 생성이 됩니다.
  • Immutable 객체이기 때문에 모든 값은 생성자를 통해 설정되어야 합니다.

※ 불변객체란 ?

값의 보장: 불변 객체는 생성될 때의 초기 상태를 유지합니다. 이는 다른 객체나 코드에 의해 객체의 내부 상태가 변경되지 않음을 보장합니다.

Thread-safe: 불변 객체는 여러 스레드에서 동시에 접근하더라도 안전합니다. 여러 스레드가 동시에 불변 객체에 접근하여 값을 읽거나 사용할 수 있습니다.

  • DTO와 같은 Data Object 용도로 활용 시 보다 편리하고 간결하게 구분할 수 있습니다.
  • 현재 우리가 사용 중 인 ModelMapper 라이브러리는 setter를 통해 값을 설정하기 때문에 Record Class를 ModelMapper에 사용할 수 없는 것으로 확인하였습니다.
  • Record Class는 상속이 불가능합니다. (모든 필드는 “private final ,,”로 선언이 되기에,,)
public record RecordUserData(String name, int weight) {
      
}
...
RecordUserData userRecord = new RecordUserData("Jayden", 110);

// Output :: Jayden
System.out.println(userRecord.name());

// Output :: 110
System.out.println(userRecord.weight());

// Output :: false
System.out.println(userRecord.equals(new RecordUserData("Jayden", 90)));

// Output :: RecordUserData[name=Jayden, weight=110]
System.out.println(userRecord);

NullPointerException 개선

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

어떤 변수가 null인지 설명한다.

Unix-Domain Socket Channels

**// 소켓 구성**
// 소켓 파일의 경로를 정의하고 UnixDomainSocketAddress로 변환
Path socketPath = Path
  .of(System.getProperty("user.home"))
  .resolve("baeldung.socket");
UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(socketPath);

// 서버를 종료할 때마다 소켓 파일을 삭제
Files.deleteIfExists(socketPath);

// 삭제하지 않으면
java.net.BindException: Address already in use //예외발생

**// 메세지 수신**
// 소켓 채널에서 메세지를 받을 서버 생성
ServerSocketChannel serverChannel = ServerSocketChannel
  .open(StandardProtocolFamily.UNIX);

// 소켓 주소와 서버 바인딩
serverChannel.bind(socketAddress);

// 채널 열기
SocketChannel channel = serverChannel.accept();

// 메세지 읽기
while (true) {
    readSocketMessage(channel)
      .ifPresent(message -> System.out.printf("[Client message] %s", message));
    Thread.sleep(100);
}

// 단순 문자열 변환작업
private Optional<String> readSocketMessage(SocketChannel channel) throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    int bytesRead = channel.read(buffer);
    if (bytesRead < 0)
        return Optional.empty();

    byte[] bytes = new byte[bytesRead];
    buffer.flip();
    buffer.get(bytes);
    String message = new String(bytes);
    return Optional.of(message);
}

**// 메세지 송신**

SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX);

channel.connect(socketAddress);

String message = "Hello from Baeldung Unix domain socket article";

ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put(message.getBytes());
buffer.flip();

while (buffer.hasRemaining()) {
    channel.write(buffer);
}

Java 16 이전에도 java.net.ServerSocket 을 활용해서 Unix 운영 체제에서도 프로세스 간 통신을 구현할 수 있었는데.. JNI 도 함꼐 사용해야만 했기 떄문에 상당히 번거로운 작업이었음..

Java 16부터는 java.nio.channels.UnixDomainSocketChannel 클래스를 통해 Unix-Domain Socket을 사용할 수 있게 되었고, 네이티브 코드 없이 Java 코드로 Unix-Domain Socket을 다룰 수 있게됨

NumberFormat , DateTimeFormatter

기존 숫자 Format 클래스(NumberFormat) 내 Method 추가(getCompactNumberInstance)

기존 날짜 Format 클래스(DateTimeFormatter) 내 패턴 Method 형식 추가("B")

sealed class

한글로 직역하면 봉인된 클래스로 Java에서 자주 사용하던 상속을 제한할 수 있다.

public sealed interface SafetyBelt permits Car, Truck {
    String belt();
}

다음과 같이 SfatetyBelt 안전벨트를 선언해놓자.

public final class Car implements SafetyBelt{

    @Override
    public String belt() {
        // TODO Auto-generated method stub
        return null;
    }

}
public final class Truck implements SafetyBelt{

    @Override
    public String belt() {
        // TODO Auto-generated method stub
        return null;
    }

}

다음과 같이 안전벨트는 Car와 Truck에서 사용할 수 있도록 허용했고 실제 상속받아서 belt mehtod를 구현한다.

상속받은 class는 추가 확장을 방지하기 위해 final로 선언한다.

!https://velog.velcdn.com/images/ililil9482/post/80c08a5a-c197-4ca1-ad39-346e7d9ded55/image.png

다음과 같이 다른 class에서 해당 class를 상속하지 못하도록 막아버린다.

그럼 여기서 만약 오토바이 Vehicle class에 안전벨트를 구현하려고 하면 어떻게 될까?

!https://velog.velcdn.com/images/ililil9482/post/f5b4581a-c2fa-489e-8f20-106dbfd44526/image.png

당연하게도 허용되지 않은 class는 해당 인터페이스를 상속받을 수 없다.

public sealed class Animal permits Dog, Cat {
    // 클래스 내용
}

public final class Dog extends Animal {
    // Dog 클래스 내용
}

public non-sealed class Cat extends Animal {
    // Cat 클래스 내용
}
  1. Animal 클래스:
    • sealed 키워드로 선언된 sealed 클래스입니다.
    • Animal 클래스는 DogCat 클래스에 대한 상속을 허용합니다. 이를 permits 절에서 명시하고 있습니다.
  2. Dog 클래스:
    • Animal 클래스를 확장하는 final 클래스입니다. final 키워드는 클래스가 더 이상 다른 클래스에 의해 상속될 수 없음을 의미합니다.
    • Dog 클래스는 Animal 클래스의 하위 클래스로 정의되어 있으며, Animal 클래스의 내용을 상속받습니다.
  3. Cat 클래스:
    • Animal 클래스를 확장하는 non-sealed 클래스입니다. non-sealed 클래스는 다른 클래스에 의해 상속될 수 있음을 의미합니다.
    • Cat 클래스는 Animal 클래스의 하위 클래스로 정의되어 있으며, Animal 클래스의 내용을 상속받습니다.

변경된 switch 예제

String fruit = "apple";
int fruitCount = switch (fruit) {
    case "apple" -> {
        System.out.println("Selected fruit: Apple");
        yield 5;
    }
    case "banana" -> {
        System.out.println("Selected fruit: Banana");
        yield 3;
    }
    default -> {
        System.out.println("Unknown fruit");
        yield 0;
    }
};

System.out.println("Total fruit count: " + fruitCount);

========================================================================================

int dayOfWeek = 3;
switch (dayOfWeek) {
    case 1, 2, 3, 4, 5 -> System.out.println("Weekday");
    case 6, 7 -> System.out.println("Weekend");
    default -> System.out.println("Invalid day");
}

System.out.println("Total fruit count: " + fruitCount);

Record Class - ModelMapper 호환 이슈

MapStruct, Dozer, Orika와 같은 매핑 라이브러리는 기본적으로 Java Bean 규약을 따르는 객체에 대해 매핑을 수행합니다. 따라서 Record 클래스와 함께 사용할 때에도 ModelMapper와 동일한 문제가 발생할 수 있습니다. Record 클래스는 필드 접근자 메서드를 제공하지 않기 때문에, 매핑 라이브러리가 내부 필드에 접근하여 값을 설정하는 데 제한이 있을 수 있습니다.

그러나, 몇 가지 접근 방식과 설정을 조정함으로써 MapStruct, Dozer, Orika와 같은 매핑 라이브러리를 Record 클래스와 함께 사용할 수 있는 경우도 있습니다. 각 매핑 라이브러리는 커스터마이즈 가능한 기능과 설정을 제공하므로, 필요한 경우에는 라이브러리의 문서와 설정 옵션을 확인하여 Record 클래스와의 매핑을 처리할 수 있는지 확인해야 합니다.

예를 들어, MapStruct는 @Mapper 애너테이션의 componentModel 속성을 사용하여 매핑 클래스의 생성 방식을 설정할 수 있습니다. componentModel 속성을 "record"로 설정하면 Record 클래스와 함께 사용할 수 있는 매핑 클래스가 생성될 수 있습니다.

(자바 10) Optional orElseThrow 개선

// 자바 10 이전에는 인자값이 필요했다
String name = repository.findNameByAge(-1)
        .orElseThrow(NoSuchElementException::new);

// 자바 10 부터는 인자값이 없이도 가능하다
String name = repository.findNameByAge(-1)
        .orElseThrow();
-> default NosuchElementException 반환

(자바11) String API 메서드 추가

isBlank

String whiteSpace = "    ";
// 자바 6에 추가된 isEmpty는 빈 공백으로만 이루어진 문자열을 비어있다고 판단하지 않음
System.out.println(whiteSpace.isEmpty());  // false
// 자바 11에 추가된 isBlank은 빈 공백으로만 이루어진 문자열을 비어있다고 판단함
System.out.println(whiteSpace.isBlank());  // true

strip

trim은 앞뒤 공백문자를 제거한다. 여기서 공백문자는 유니코드 “\u0020” 이하의 공백문자만 해당된다. 예를들면 스페이스(” “), 탭(”\t”), 개행(”\n”)

자바 11부터 추가된 strip은 유니코드의 공백문자를 모두 제거한다.

// 앞뒤로 공백이 있는 문자열
String str = "\u2003Hello World!\u2003";

String trimStr = str.trim();    // 자바 11 이전
String stripStr = str.strip();  // 자바 11 이후

System.out.println(trimStr);    //" Hello World! "
System.out.println(stripStr);   //"Hello World!"

repeat

String str = "Hello".repeat(3);
System.out.println(str);    // HelloHelloHello

(자바12)instanceof 개선

// 자바 12 이전
private void instanceof_before_Java12(Object request) {
    if (request instanceof String) {
        System.out.println("Is String");
        // String 의 메소드에 접근하기 위해서는 명시적인 Casting이 필요했다
        System.out.println("Length = " + ((String) request).length());
    }
}

// 자바 12 이후
private void instanceof_after_Java12(Object request) {
	// 자바 12부터는 statement에 typecasted 변수를 직접 선언할 수 있다
	if (request instanceof String str) {
	System.out.println("Is String");
	System.out.println("Length = " + str.length());
	}
}

(자바16) Stream.toList 추가

List<Integer> integers = List.of(-2, -1, 0, 1, 2, 3, 4);

List<Integer> beforeJava16 = integers.stream()
        .filter(x -> x >= 0)
        .collect(Collectors.toList());

List<Integer> afterJava16 = integers.stream()
        .filter(x -> x >= 0)
        .toList();
profile
주니어 백엔드 개발자

0개의 댓글