2021년 JDK 11 LTS버전 이후 최신 LTS버전이 업데이트 됐다. 바로 JDK 17. 비록 이 글을 작성하는 2023년 7월 기준 Java 17부터 LTS 릴리스 버전이 3년에서 2년으로 변경됐기에, 2달 뒤인 23년 9월 새로운 LTS 버전 Java 21이 공개된다.
그러나 현재 부트캠프에서 메인프로젝트를 돌입해야하기에 JDK 17을 사용하기로 했다. 하지만 덕분에, 추후 JDK 21 LTS가 출시되었을 때 Java 버전을 업그레이드하여 적용해볼 수 있는 기능들을 찾아 새로 적용 해볼 수 있는 재미가 있을 것 같다.
제목 옆에 달린 자바버전은 Standard버전이 출시된 기준으로 작성했다.
기존에 Java에서는 JSON을 문자열로 직접 생성하 할 때, 큰 따옴표나 줄 변경 등 이스케이프 처리하는 문자열을 포함해야했기에 가독성이 좋지 못했다.
| Java 11
private static void oldStyle() {
String text = "{\n" +
" \"name\": \"홍길동\", \n" +
" \"age\": 45, \n" +
" \"address\": \"Gang Street, 1, Seoul\"\n" +
"}";
System.out.println(text);
}
위의 코드를 보다시피 우리가 보고싶은 JSON 요소 외의 요소들이 많이 포함되어있어 가독성이 떨어진다. 17에서는 """로 텍스트블록을 정의하여 아래와 같이 작성할 수 있도록 변경됐다.
| Java 17
private static void jsonBlock() {
String text = """
{
"name": "홍길동",
"age": 45,
"address": "Gang Street, 1, Seoul"
}
""";
System.out.println(text);
}

아래와 같이 .formatted() 메서드를 사용할 수 있다.
| .formatted()
public static void main(String[] args) {
String text = """
{
"name": "홍길동",
"age": 45,
"address": "Gang Street, 1, Seoul"
}
""";
String formattedTest = getFormattedTest(text);
System.out.println(formattedTest);
System.out.println(text);
}
public static String getFormattedTest(String param) {
return """
Test param: %s
""".formatted(param);
}

formatted()뿐만 아니라 replace()도 사용가능하다.
두 번째는 바로 개선된 switch표현식이다. 먼저 기존의 swtich 표현식을 보자
| Java 11
public static void oldStyleWithBreak(Fruit fruit) {
Fruit fruit = Fruit.APPLE;
switch (fruit) {
case APPLE, PEAR:
System.out.println("Common fruit");
break;
case ORANGE, AVOCADO:
System.out.println("Exotic fruit");
break;
default:
System.out.println("Undefined fruit");
}
}
위 코드의 결과는 Common fruit일 것이다. 그러나 break문이 생략되었다면 다음과 같은 결과가 출력되었을 것이다.
Common fruit
Exotic fruit
Underfined fruit
위와 같은 break문 누락에 따른 잘못된 결과물 출력을 미연에 방지하고자 개선된 switch표현식은 콜론:을 화살표->로 변경하고 break문을 더이상 작성하지 않아도 된다.
| 개선된 Switch ver.1
public static void withSwitchExpression(Fruit fruit) {
Fruit fruit = Fruit.APPLE;
switch (fruit) {
case APPLE, PEAR -> System.out.println("Common fruit");
case ORANGE, AVOCADO -> System.out.println("Exotic fruit");
default -> System.out.println("Undefined fruit");
}
}
/* ===== result =====
Common fruit
===================== */
System.out.println()부분을 생략하고 다음과 같이 결과 값을 변수에 할당하는 방법도 가능하다.
| 개선된 Switch ver.2
public static void withReturnValue(Fruit fruit) {
Fruit fruit = Fruit.APPLE;
String text = switch (fruit) {
case APPLE, PEAR -> "Common fruit";
case ORANGE, AVOCADO -> "Exotic fruit";
default -> "Undefined fruit";
};
System.out.println(text);
}
}
/* ===== result =====
Common fruit
===================== */
위의 코드에서는 switch 표현식을 닫는 대괄호}뒤에 세미콜론;을 붙여야한다. 꼭 반환 값이 String이 아니라도 괜찮다.
| 개선된 Switch ver.3
public static void withReturnValue(Fruit fruit) {
Fruit fruit = Fruit.APPLE;
int num = switch (fruit) {
case APPLE, PEAR -> 1;
case ORANGE, AVOCADO -> 2;
default -> 3;
};
System.out.println(num);
}
}
/* ===== result =====
1
===================== */
특정 case에서 한 가지 이상의 작업이 필요할 때 괄호를 사용하고 특정 값을 반환하기 위해 yield를 사용할 수 있다. 이떄 return의 사용은 불가능 하다.
| 개선된 Switch ver.4
public static void withYield(Fruit fruit) {
Fruit fruit = Fruit.APPLE;
String text = switch (fruit) {
case APPLE, PEAR -> {
System.out.println("사과 혹은 배 입니다.");
yield "Common fruit";
}
case ORANGE, AVOCADO -> "Exotic fruit";
default -> "Underfined fruit";
};
System.out.println(text);
}
/* ===== result =====
사과 혹은 배 입니다.
Common fruit
===================== */
또한 마지막 예제로 다음과 같이 사용할 수 있다는 것도 알아두자.
| 개선된 Switch ver.5
public static void withYield(Fruit fruit) {
Fruit fruit = Fruit.APPLE;
System.out.println(switch (fruit) {
case APPLE, PEAR:
yield "Common fruit";
case ORANGE, AVOCADO:
yield "Exotic fruit";
default:
yield"Underfined fruit";
});
System.out.println(text);
}
/* ===== result =====
Common fruit
===================== */
Records는 Spring에서 사용하던 Lombok의 여러 기능을 Record를 사용하여 구현할 수 있도록 해준다. 기존의 Entity클래스를 보다 간결하게 작성할 수 있는 방식이다.
Lombok 중 @Data에 포함된 Getter, Setter, EqualsAndHashCode, toString등의 기능을 내재하고있다. 먼저 기존대로 클래스에 Person이라는 클래스를 작성해보겠다.
| Person
@Data
@AllArgsConstructor
public class Person {
private String name;
private int age;
}
위의 코드를 record형식으로 변경하면 다음과 같다.
| Person
public record Person(String name, int age) {}
별도의 애너테이션 없이 간단하게 @Data로 수행할 수 있는 기능을 자동 생성해서 사용할 수 있게 해준다.
| recordTest
public class recordTest {
public static void main(String[] args) {
Person john = new Person("John", 20);
Person sara = new Person("Sara", 21);
Person kane = new Person("Kane", 21);
Person john1 = new Person("John", 20);
System.out.println(john.name()); // John
System.out.println(kane.age()); // 21
System.out.println(sara.equals(kane)); // false
System.out.println(john.equals(john1)); // true
System.out.println(john.hashCode() == john1.hashCode()); // true
}
}
/* ===== result =====
John
21
false
true
true
===================== */
기존의 instanceof는 해당 객체를 특정 타입인지 확인 후, 해당 타입을 명시적으로 캐스팅해야했다.
| oldInstanceof
if(obj instanceof String) {
String str = (String) obj;
...
}
그러나 패턴매칭기능이 추가되어 명시적으로 캐스팅할 필요없이 객체 타입을 확인하는 동시에 해당 타입으로 캐스팅이 가능해졌다. 이는 코드의 가독성과 안전성을 향상시켜준다.
| newInstancof
if(obj instance String str) {
...
}
이후swtich문에서도 사용가능하도록 업데이트 될 예정이라고 한다.
Java 17에서 새로 도입된 Sealed 클래스는 클래스 또는 인터페이스가 허용한 타입에 의해서만 상속가능하도록 제한하는 기능이다. 이 기능은 객체 지향 설계의 캡슐화를 더욱 강화할 수 있다.
Sealed Class는 sealed, non-sealed, permits 키워드를 통해 구성되어있다.
sealed: 클래스 또는 인터페이스가 제한된 하위 클래스를 갖는다는 것을 의미한다.non-sealed: 하위 클래스들에서 추가적인 하위 클래스를 가질 수 있음을 의미한다. 여기서 non-sealed가 아니라 final로 하위 클래스가 상속받아 구현된다면 sealed의 기능을 따라 추가적인 하위 클래스를 가질 수 없다.permits: 상속이나 구현을 허용하는 구체적 클래스 지정.| SealedEx
public sealed abstract class Shape
permits Circle, Polygon {
abstract void draw();
}
public final class Circle extends Shape {
@Override
void draw() {
System.out.println("Draw a circle");
}
}
public non-sealed class Polygon extends Shape {
@Override
void draw() {
System.out.println("Draw a polygon");
}
}
public final class Triangle extends Polygon {
@Override
void draw() {
System.out.println("Draw a triangle");
}
}
public final class Rectangle extends Polygon {
@Override
void draw() {
System.out.println("Draw a rectangle");
}
}
Helpful NullPointerExceptions은 JDK 14부터 포함되어있지만 기본값으로 비활성화 되어 있어 JVM 시작 시 -XX:+ShowCodeDetailsInExceptionMessages라는 JVM플래그를 전달해야 사용가능했다.
결론적으로 Java 15버전부터 사용가능하다. Helpful NullPointerExceptions은 NullPointerException이 발생했을 때, 기존에는 스택 트레이스를 통해 문제가 발생한 위치를 파악해야 했지만, 메세지에 어떤 변수에서 null값이었는지를 제공해줘서 해당 에러 메세지를 읽고 Null값이 발생한 변수를 빠르게 찾아 낼 수 있다.
| Helpful NullPointerExceptions
public class Main {
static class User {
String name;
}
public static void main(String[] args) {
User user = null;
System.out.println(user.name.length());
}
}
/* ===== result =====
Exception in thread "main" java.lang.NullPointerException: Cannot read field "name" because "user" is null
at Main.main(Main.java:10)
===================== */
Java 12부터 NumberFormat클래스에서 Compact Number Formatting Suppor를 지원한다. 이 기능은
NumberFormat클래스의 사용예제를 작성해보고 비교해볼 수 있도록 하겠다.| standartNumberFormat
public static void main(String[] args) throws Exception {
double number = 1234567.89;
NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.KOREA);
System.out.println("Number: " + numberFormat.format(number));
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance(Locale.KOREA);
System.out.println("Currency: " + currencyFormat.format(number));
NumberFormat percentFormat = NumberFormat.getPercentInstance();
System.out.println("Percentage: " + percentFormat.format(0.51));
}
/* ===== result =====
Number: 1,234,567.89
Currency: ₩1,234,568
Percentage: 51%
===================== */
그리고 다음은 컴팩트한 숫자 포맷이다.
| CNFS
public static void main(String[] args) {
NumberFormat nf = NumberFormat.getCompactNumberInstance(new Locale("ko", "KR"), NumberFormat.Style.SHORT);
nf.setMaximumFractionDigits(2);
System.out.println("Number: " + nf.format(1000));
System.out.println("Number: " + nf.format(15000));
System.out.println("Number: " + nf.format(1000000));
System.out.println("Number: " + nf.format(12000000));
}
/* ===== result =====
Number: 1천
Number: 1.5만
Number: 100만
Number: 1200만
===================== */
| CNFS
public static void main(String[] args) {
NumberFormat nf = NumberFormat.getCompactNumberInstance(new Locale("en", "US"), NumberFormat.Style.LONG);
nf.setMaximumFractionDigits(2);
System.out.println("Number: " + nf.format(1000));
System.out.println("Number: " + nf.format(15000));
System.out.println("Number: " + nf.format(1000000));
System.out.println("Number: " + nf.format(12000000));
}
/* ===== result =====
Number: 1 thousand
Number: 15 thousand
Number: 1 million
Number: 12 million
===================== */
Java 16에서 Stream()기능 중 하나로 toList() 메서드가 추가되었다. 이 메서드는 collect(Collectors.toList())를 호출하는 것과 비슷하지만 차이점이 있다. 바로 불변성의 성질을 가진다는 말이다.
toList()메서드를 사용해서 반한된 리스트는 변경 불가능(immutable)한 성격을 가지고 있다. 불변성의 성질을 가지고 있기 때문에 얻을 수 있는 장점도 있는데 안전하게 리스트를 공유할 수 있고 병렬 스트림(parallel Stream)에서 사용할 수 있음을 보장할 수 있다.
그리고 불변의 성질을 가지고 있기 때무에 크기를 미리 계산해서 초기 리스트를 생성해서 메모리 사용을 줄이고 성능을 향상 시킬 수 있는 장점이 있다.
| toListEx
List<Integer> list = Stream.of(1, 2, 3, 4, 5)
.toList();
System.out.println(list); // Prints: [1, 2, 3, 4, 5]
💡 병렬 스트림에서 불변성을 가진 리스트가 가변성을 가진 리스트보다 장점을 가지는 이유
- 스레드 안전: 불변성을 가진 객체는 여러 스레드에서 동시에 접근하더라도 상태변경이 불가능하기 때문에 여러 스레드가 동시에 접근하더라도 안전하게 작업할 수 있다.
- 사이드 이펙트❌: 병렬 스트림에서 작업을 수행하면서 원본 리스트에 영향을 미치지 않음. 데이터 일관성 유지 가능
- 최적화:
Compiler와JVM은 불변성을 가진 데이터 구조체에 대해 최적화를 수행할 수 있기 때문에 성능을 향상 시킬 수 있다.
G1(Garbage-First) Garbage Collector 향상ZGC(Z Garbage Collector 향상Concurrent Thread-Stack Processing, Uncommit Unused Memory 등의 기능을 통해 성능 향상Elastic MetaspaceClass Loader Metaspace Memory가 운영체제에 반환되어 메모리 사용량이 줄어든다.향상된 유니버설 Java 바이트코드 생성기(Universal Java Bytecode Generator)Graal JIT Compiler가 유니버설 바이트코드 생성기로 강화되었다. 이는 네이티브 이미지 생성 및 서드파티 언어 지원등에 사용되는 기능이다.