본문 바로가기
Java

Java 17 학습테스트

by 나는후니 2022. 10. 3.

제가 개발을 처음 시작할 즈음 Java 17 버전이 나왔고, 최근에는 Java 19 버전까지 나온 것으로 알고 있는데요. 17, 19 버전 내용을 훑어보니 꽤나 재미있는 내용들이 있는 것 같아 간단하게 학습 테스트를 진행해보았습니다. 특히 코틀린을 사용하셨던 분들이라면 꽤나 익숙한 것들이 있을 거에요. 깊이 있게는 아니고, 그냥 간단하게 Java 17 버전을 학습테스트 한 내용을 정리해보겠습니다.

Record

기존에는 VO를 만들기 위해, 각 field에 private final 키워드를 붙이고, Getter를 재정의 해주고, Equals & hashcode를 재정의 해야 합니다. 많은 boilerplate 때문에 lombok을 주로 사용해왔는데요. java 17 버전부터는 그러지 않아도 됩니다.

과거에는 아래와 같이 작성했다면

private final class Member {
  private final String name;
  private final int age;
  private final Job job;

  // constructor
  // ,, getters
  // .. equals & hashcode
  // .. to String
}

record를 이용하면 아래와 같이 해결이 가능합니다.

public record Member (String name,
                            int age,
                            Job job) {}

마치 코틀린의 data class와 동일한 기능을 해준다고 볼 수 있습니다. 또 더 다양하게 record를 활용할 수 있는데요.

위에서 말한 것 처럼 모든 보일러플레이트 코드들이 작성되어 있고요.

@Test
void record_의_다양한_기능() {
  BackendDeveloper backend = new BackendDeveloper();
  Member member = new Member("name", 12, backend);
  Member other = new Member("name", 12, backend);

  // getter 제정의
  assertThat(member.name()).isEqualTo("name");
  assertThat(member.age()).isEqualTo(12);
  assertThat(member.job()).isInstanceOf(BackendDeveloper.class);

  // equals hashcode 재정의
  assertThat(member).isEqualTo(other);

  // toString 재정의
  assertThat(member.toString()).isEqualTo("Member[name=name, age=12, job=" + backend + "]");
}

Compact 생성자를 두어 주 생성자에 대한 검증을 할 수 있습니다.

// 주생성자 역할
public Member {
  if (name.length() > 10) {
    throw new IllegalArgumentException("이름 초과");
  }

  if (age < 0) {
    throw new IllegalArgumentException("불가능한 나이");
  }
}

@Test
void record_의_compact_생성자_이름_에러() {
  assertThatThrownBy(() -> new Member("ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ", 10, new BackendDeveloper()))
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessage("이름 초과");
}

@Test
void record_의_compact_생성자_나이_에러() {
  assertThatThrownBy(() -> new Member("최재훈", -10, new BackendDeveloper()))
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessage("불가능한 나이");
}

또, 부생성자도 얼마든지 생성할 수 있습니다.

public Member(final String name, final int age) {
  this(name, age, new NoJob());
}

@Test
void record_의_canonical_생성자() {
  assertThat(new Member("name", 10).job()).isInstanceOf(NoJob.class);
}

이제 Java 17 버전을 사용하면 정말 간편하게 VO를 정의 할 수 있습니다. 이제 이 record 클래스를 평소에 이용하는 DTO 에도 적용하고 싶다는 마음이 굴뚝같아 지실텐데요. Final 특성상 빈 생성자를 만들지 못해 어려울 거라 생각이 들겠지만 Jackson 2.12 버전부터는 record 클래스에 대해 자동으로 Json serialize를 해줍니다. (그 이전버전도 애노테이션 몇개를 추가하여 가능합니다.)

Sealed class

코틀린을 사용해보신 분들이라면 익숙하실텐데요. 코틀린에서는 sealed class 내부에만 하위 클래스를 중첩하여 작성하는 형태로 상속을 하여. 패턴 매처 기술과 접목하여 하위 클래스를 안정적으로 관리할 수 있었는데요. java 에서도 거의 유사하지만 조금 다른 점이 있다면, 동일 모듈 / 패키지 내 클래스를 permits라는 키워드에 등록해놓고 사용할 수 있다는 것입니다.

이렇게 봉인 해놓으면 하위 클래스에서는 다시 non-sealed 키워드를 통해 일반 클래스로 만들 수 있고, 혹은 final 키워드를 통해 더이상의 상속을 막을 수 도 있습니다. 혹은 하위 클래스도 sealed 클래스로 만들어 줄 수 있어요.

sealed class를 많이 써먹을 수 있을 지는 모르겠지만 Java 17 preview에 함께 등장한 Pattern Matching과 합이 잘 맞을 것이라 느꼈습니다.

@Test
void sealedClassPatternMatching() {
    Job job = new Designer();
    assertThat(mapJobCode(job)).isEqualTo(100);
}

private int mapJobCode(final Job job) {
  return switch (job) {
    case Designer designer -> 100;
    case Developer developer -> 200;
    case NoJob noJob -> 300;
  };
}

Switch case를 즐겨쓰는 편은 아니지만, 위와 같이 type casting의 불편함도 없고, 바로 return이 가능하고, sealed class의 하위 클래스가 존재하지 않을 경우 컴파일 에러를 발생시켜 switch case에서 굳이 default, break를 걸어주지 않아도 쉽게 사용이 가능해졌습니다. (sealed 외에도 기본형, enum에 적용이 가능합니다.)

정리

최근에 자바가 버전을 업데이트하면서 코틀린을 많이 닮아간다는 생각을 했습니다. 점점 언어가 간결해지고, 다양한 개발 패러다임을 쉽게 적용할 수 있도록 만들어준다는 생각이 들었습니다. 기회가 된다면 다음 프로젝트에는 Java 17 버전을 적용하여 Java에서 제공하는 새로운 기술들을 사용해보고 싶네요. (참고로 위 기능 외에도 Text Block, 안정적인 난수 생성기 등을 17버전에서 제공합니다.)

 

다음에는 Java 19버전을 간단하게 리뷰해보겠습니다. Java 19 + 에는 OS가 아닌 JDK에서 스레드를 스케줄링 하는 loom이라는 내용이 도입되었다고 하네요. 이 부분이 정말 인상깊었습니다. 아직 그래들 최신 버전에서 지원을 안하는지 계속 컴파일 에러가 났는데, 한 번 자세히 확인해보고 테스트 해보고 싶습니다.