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();
};
}
}