JDK21이 등장했다!
그렇다면, 먼저 JDK 21에서 어떤 기능이 추가되었는지 알아보는 것이 개발자된 도리가 아닌가!
일단, 다음 Hello, Java 21에 들어가보자. JDK 17부터 쭉 스토리를 이어가보자.
JDK 17
Multiline Lines
파이썬 주석을 쓰는 방식처럼 Java 17에서는 """ .... """ 을 이용하여 다중줄을 지원한다. JSON, JDBC, JPA QL, etc., 을 더 쉽게 표현할 수 있을 것이다.
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class MultilineStringTest {
    @Test
    void multiline() throws Exception {
        var shakespeare = """
                To be, or not to be, that is the question:
                Whether 'tis nobler in the mind to suffer
                The slings and arrows of outrageous fortune,
                Or to take arms against a sea of troubles
                And by opposing end them. To die—to sleep,
                No more; and by a sleep to say we end
                The heart-ache and the thousand natural shocks
                That flesh is heir to: 'tis a consummation
                Devoutly to be wish'd. To die, to sleep;
                To sleep, perchance to dream—ay, there's the rub:
                For in that sleep of death what dreams may come,
                """;
        Assertions.assertNotEquals(shakespeare.charAt(0), 'T');
        shakespeare = shakespeare.stripLeading();
        Assertions.assertEquals(shakespeare.charAt(0), 'T');
    }
}Record
프로젝트를 수행할 때, 필자는 이 녀석을 굉장히 좋아한다. 이 녀석은 오라클 문서에 잘 나와있다.Record는 외부 라이브러리인 Lombok의 의존성을 줄이고, 간결하고 가독성 높은 데이터 클래스를 작성할 수 있게 한다.
- 명시적인 필드 선언, 생성자, getter, setter 등을 대체 
- 기본적으로 불변객체로 생성해 데이터의 안정성과 무결성을 보장 (암묵적으로 final) 
- toString, equals, hansCode 메서드가 자동으로 생성 
- static 메소드, static 필드 선언 가능 (클래스 기능과 동일) 
- 중첩 클래스 사용 가능 및 제너릭 타입으로 지정 가능 (클래스 기능과 동일) 
record(헤더) {바디} 형태로 이루어져있는데, 헤더에 나열된 필드는 private final로 정의된다.
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class RecordTest {
    record JdkReleasedEvent(String name) { }
    @Test
    void records() throws Exception {
        var event = new JdkReleasedEvent("Java21");
        Assertions.assertEquals( event.name() , "Java21");
        System.out.println(event);
    }
}Record 클래스는 어디서 사용할 수 있을까? DTO에서 Record 클래스를 자주 사용한다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberResponse {
	private Long memberId;
	private String name;
	private Role role;
	private Building building;
	@Builder
	public MemberResponse(Long memberId, String name, Role role, Building building) {
		this.memberId = memberId;
		this.name = name;
		this.role = role;
		this.building = building;
	}
}Record도 특수한 종류의 클래스이므로 new키워드를 사용하여 레코드 객체(레코드 클래스의 인스턴스)를 생성한다.
public record MemberResponse(Long memberId, String name, Role role, Building building) {
    public static MemberResponse fromRequest(Request request) {
       return new MemberResponse(request.getMemberId, request.getName, request.getRole, request.getBuilding);
    }
}
// 사용
public MemberResponse createMember(MemberRequest reqeust) {
    ...
    return MemberResponse.fromRequest(request);
}💡 Entity가 Record 클래스가 될 수 없는 이유
Record는 불변객체이며, 매개변수가 없는 생성자를 제공하지 않으며, Record의 필드 또한 private final로 설정되어있다. 그러나 JPA를 사용하는 1) 엔티티 클래스에는 쿼리 결과를 매핑할 때, 객체를 인스턴스화할 수 있도록 public 또는 protected인 매개변수가 없는 생성자가 있어야한다. 또한, 2) 프록시를 생성할 수 있도록 클래스는 final이면 안된다. 3) 필드도 setter가 있어야하기 때문에 final이면 안된다. Hibernate를 사용하면, final 클래스를 유지할 수 있고, 매핑된 엔티티 속성에 대한 접근자 메서드가 필요하지않지만, 프록시를 생성하기 위해 여전히 기본 생성자와 final이 아닌 필드가 필요하다.
Switch Statement
잘 쓰고싶지않던 Switch문이 바뀌었다!
가장 눈에 띄는 것은 case label1, label2 .. -> 와 같은 형태로 바뀌었다는 것인데, 1. 여러개의 label를 동시에 작성 가능, 2. 번거로운 break문이 없어짐을 확인할 수 있다.
switch문은 이제 표현식이기 때문에 switch문의 결과를 변수에 할당하거나 반환 값으로 줄 수 있게 변경되었다는 점도 중요하다!
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.DayOfWeek;
class EnhancedSwitchTest {
    // 기존 
    int calculateTimeOffClassic(DayOfWeek dayOfWeek) {
        var timeoff = 0;
        switch (dayOfWeek) {
            case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY:
                timeoff = 16;
                break;
            case SATURDAY, SUNDAY:
                timeoff = 24;
                break;
        }
        return timeoff;
    }
    // 변경
    int calculateTimeOff(DayOfWeek dayOfWeek) {
        return switch (dayOfWeek) {
            case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> 16;
            case SATURDAY, SUNDAY -> 24;
        };
    }
    @Test
    void timeoff() {
        Assertions.assertEquals(calculateTimeOffClassic(DayOfWeek.SATURDAY), calculateTimeOff (DayOfWeek.SATURDAY));
        Assertions.assertEquals(calculateTimeOff(DayOfWeek.FRIDAY), 16);
        Assertions.assertEquals(calculateTimeOff(DayOfWeek.FRIDAY), 16);
    }
}반환값이 있기 때문에 아래 코드같이 작성도 가능하고, 출력문에도 쓸 수 있다.
static int coverage(Object o) {
    return switch (o) {
        case String s  -> s.length();
        case Integer i -> i;
        default -> 0;
    };
}InstanceOf Check & Sealed Types
Sealed Class, Interface는 간단하게 상속하거나(extends), 구현(implements)할 클래스를 지정해두고 해당 클래스들만 상속 혹은 구현을 허용하는 키워드이다. 이것의 목표는 어떤 Super Class의 Sub Class들을 명확히 인지할 수 있어야 한다는 것이다! 
몇가지 제약사항이 존재한다.
- 모든 하위 클래스를 알아야함 
- 하위클래스는 sealed된 클래스를 직접 상속받아야함 (중간에 다른 클래스가 끼어들면 안됨) 
- 세 가지 종류의 하위 클래스 규칙 - final: 더 이상 다른 클래스가 이 클래스를 상속받을 수 없다. 즉, 이 클래스가 상속의 끝이다.
- sealed: 다시 봉인된 상태로, 특정한 클래스들만 이 클래스를 상속받을 수 있다.
- non-sealed: 제한 없이 어떤 클래스든 이 클래스를 상속받을 수 있다. 봉인된 클래스가 하위 클래스를 제한해도,- non-sealed로 선언되면 그 제한을 풀어버릴 수 있다.
 
- 같은 모듈이나 패키지 안에 있어야함 
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class SealedTypesTest {
    // ①
    sealed interface Animal permits Bird, Cat, Dog {
    }
    // ②
    final class Cat implements Animal {
        String meow() {
            return "meow";
        }
    }
    final class Dog implements Animal {
        String bark() {
            return "woof";
        }
    }
    final class Bird implements Animal {
        String chirp() {
            return "chirp";
        }
    }
    @Test
    void doLittleTest() {
        Assertions.assertEquals(communicate(new Dog()), "woof");
        Assertions.assertEquals(communicate(new Cat()), "meow");
    }
    // ③
    String classicCommunicate(Animal animal) {
        var message = (String) null;
        if (animal instanceof Dog dog) {
            message = dog.bark();
        }
        if (animal instanceof Cat cat) {
            message = cat.meow();
        }
        if (animal instanceof Bird bird) {
            message = bird.chirp();
        }
        return message;
    }
    // ④
    String communicate(Animal animal) {
        return switch (animal) {
            case Cat cat -> cat.meow();
            case Dog dog -> dog.bark();
            case Bird bird -> bird.chirp();
        };
    }
}