4P by GN⁺ | ★ favorite | 댓글 1개
  • 2023년 9월 19일 출시된 Java 21은 record patternsswitch pattern matching을 통해 Kotlin·Rust·C#에 가까운 함수형 패턴 표현을 Java 안으로 끌어옴
  • Java 14의 switch expressions, Java 16의 records와 instanceof pattern matching, Java 17의 sealed classes가 누적되며 대수적 데이터 타입을 다룰 기반이 Java 21에서 맞물림
  • records는 final·불변 참조·정형화된 getter 같은 제약으로 데이터를 안정적으로 분해하게 만들고, record pattern은 중첩 데이터를 switch에서 바로 꺼낼 수 있게 함
  • sealed classes/interfaces는 허용된 하위 타입만 열어 sum type에 가까운 모델을 만들며, sealed interface와 record를 함께 쓰면 RGB·CMYK·YUV·HSL 같은 변형을 제한할 수 있음
  • Java 21의 switch는 null case와 when guard까지 지원하지만, 잘못된 record accessor나 guard 실행 중 예외는 java.lang.MatchException 으로 이어질 수 있음

Java 21에서 안정화된 패턴 매칭

  • Java 21은 2023년 9월 19일 출시됐고, switch 블록과 switch 표현식에서 record patterns를 지원함
  • 이 문법은 Java에서도 Kotlin, Rust, C#과 비슷한 방식으로 함수형 프로그래밍 패턴을 표현할 수 있는 전환점으로 평가됨
  • 최근 Java 버전의 주요 문법 변화는 Java 21의 패턴 매칭으로 이어짐
    • Java 14: switch expressions 안정화
    • Java 16: records, instanceof pattern matching 안정화
    • Java 17: sealed classes 안정화
    • Java 21: record patterns, switch pattern matching 안정화
  • 이 변화 묶음으로 Java는 이전에 표현하기 어려웠던 대수적 데이터 타입(algebraic data types) 과 그 관용적 사용 방식을 다룰 수 있게 됨

타입 이론에서 필요한 최소 개념

  • Java 21의 기능을 이해하려면 몇 가지 타입 이론 개념이 필요함
  • bottom/empty type은 계산될 수 없는 값의 집합을 나타내며, 일반적인 프로그래밍 언어에서는 보통 빈 집합임
    • Kotlin의 Nothing은 생성자가 private이라 인스턴스가 존재할 수 없음
    • Java의 Void는 생성자가 private이지만 null을 담을 수 있어 진짜 bottom type으로 보기 어려움
    • Java의 primitive void는 변수 타입으로 사용할 수 없어 이 점에서는 더 가깝게 동작함
  • top type은 모든 타입의 모든 값을 나타내는 보편 집합임
    • Kotlin에서는 Any가 해당 역할을 함
    • Java의 Object는 primitive가 객체 모델과 분리돼 있어 다른 언어의 top type과 같은 의미로 보기 어려움
  • unit type은 값이 하나뿐인 타입임
    • Java의 void는 메서드 반환에서는 unit type처럼 다룰 수 있지만, 매개변수 타입으로 전달할 수 없음
    • Kotlin의 Unitobject로 정의되며 메서드 매개변수로도 사용할 수 있음
  • boolean typetruefalse 두 값을 가지며, nullable unit type으로도 표현할 수 있지만 실용적이지 않음

Product type과 Java records

  • product type은 둘 이상의 구성 타입을 묶은 타입이며, 구성 타입의 수가 arity 또는 degree가 됨
  • C의 struct는 product type의 예시임
    • int, char *, double, int처럼 구성 타입이 반복될 수 있음
    • 반복되는 타입은 필드 이름과 함께 ordered pair로 생각하면 구분 가능함
  • Python이나 Rust의 tuple도 product type으로 볼 수 있으며, 이 경우 구성 요소의 이름 역할을 인덱스가 맡음
  • Java 16에서 안정화된 record class는 product type의 좋은 예시임
    • record의 필드는 final이며, record는 상속될 수 없음
    • record의 상태는 생성 시점에 설정되고 이후 유지됨
    • 단, record 내부에 mutable data type을 넣으면 그 내용의 불변성까지 보장되지는 않음
  • 일반 Java class는 public/private 상태, 상속으로 생기는 숨은 상태, mutable/static field, 비표준 getter가 섞일 수 있어 구성 요소를 일반화하기 어려움
  • records는 다음 제약으로 패턴 매칭 같은 언어 기능이 안정적으로 동작할 구조를 보장함
    • record는 암묵적으로 final class이며 상속될 수 없음
    • java.lang.Record 외의 클래스를 extend할 수 없음
    • record component에는 visibility modifier를 붙일 수 없음
    • component 참조는 항상 final이며 불변으로 취급됨
    • 기본 getter는 필드 이름을 그대로 사용하며, a 필드의 getter는 a()가 됨
    • backing field는 암묵적으로 private이고 getter를 통해 접근됨

record pattern으로 중첩 데이터 분해

  • Java 21의 switch pattern은 instanceof 검사와 명시적 cast를 반복하지 않고 중첩 record 데이터를 분해함
  • 예시에서는 record A(Record inner), record B(char b), record SomeOtherRecord()를 사용함
    • 기존 방식은 if (r instanceof A) 이후 cast하고, 내부 값에 대해 다시 instanceof와 cast를 반복함
    • switch pattern은 case A(B(char a)) -> String.valueOf(a)처럼 중첩 값을 바로 추출함
  • switch 블록은 if-else ladder보다 구조가 명확하며, 깊게 중첩된 데이터를 빠르게 꺼내는 데 적합함
  • Java 21에서 직접 실행하려면 main.java에 코드를 넣고 다음 명령을 사용할 수 있음
java --enable-preview --source 21 main.java

Sum type과 sealed classes/interfaces

  • 제한된 선택지를 표현할 때 Java enum을 사용할 수 있지만, RGB·HSL·YUV·CMYK처럼 서로 다른 데이터 구조를 가진 색상 표현은 enum만으로 다루기 번거로움
  • 상속 기반 다형성으로 Color 추상 클래스와 RGB, CMYK, YUV, HSL 클래스를 만들 수 있지만, 일반 class hierarchy는 열려 있음
    • 라이브러리 사용자가 RYB 같은 새 클래스를 만들어 Color를 상속할 수 있음
    • API가 확장을 의도하지 않았다면 새 변형이 크래시나 멀리 떨어진 코드의 미묘한 버그를 일으킬 수 있음
  • sealed classes는 sum type 개념을 Java에서 표현하는 데 쓰임
    • sum type은 한 시점에 구성 요소 중 하나일 수 있는 타입임
    • tagged union type이라고도 불림
  • sealed modifier와 permits clause를 사용하면 특정 class만 상속을 허용할 수 있음
public sealed class Color permits RGB, CMYK, YUV, HSL {
}
  • sealed class hierarchy에서는 직접 또는 간접 상속자가 sealed, non-sealed, final 중 하나를 가져야 하며, 없으면 compile error가 발생함
    • sealed: permits에 이름이 있는 타입만 상속 가능
    • non-sealed: 일반 class처럼 상속 가능
    • final: 상속 트리의 leaf로 더 이상 확장 불가

sealed interface와 record를 함께 쓰는 방식

  • switch pattern matching의 destructuring은 records에 동작하지만, records는 Record 외의 클래스를 상속할 수 없음
  • 해결책은 sealed interface를 사용하는 것임
    • sealed interface는 sealed class와 비슷하게 동작함
    • records와 enums도 sealed interface를 implement할 수 있음
  • 예시에서는 Color를 sealed interface로 만들고 RGB, CMYK, YUV, HSL을 record로 구현함
public sealed interface Color permits RGB, CMYK, YUV, HSL {
    String getDescription();
}

record RGB(int red, int green, int blue) implements Color {
    public String getDescription() {
        return "RGB Color: (" + red + ", " + green + ", " + blue + ")";
    }
}
  • 이후 switch에서 각 record의 값을 바로 추출할 수 있음
switch (color) {
  case RGB(int red, int green, int blue) -> {
  }
  case CMYK(double cyan, double magenta, double yellow, double black) -> {
  }
  case YUV(int y, int u, int v) -> {
  }
  case HSL hsl -> {
    System.out.println(hsl.getDescription());
  }
  case null -> {
    System.out.println("How did color become null?!");
  }
}
  • Java 21은 switch 블록과 표현식에서 null case를 처리할 수 있어 switch 전에 별도 null 검사를 하지 않아도 됨
  • Color가 sealed type이면 Java가 모든 case 처리 여부를 알 수 있어 default case 없이도 exhaustive switch가 가능함

Guard clause와 when

  • Java 21은 switch arm에 추가 조건을 붙이는 guard clause를 지원함
  • guard clause는 when 키워드로 case label에 조건을 통합함
switch (color) {
  case RGB(int red, int green, int blue) when red > 200 -> {
    System.out.println("Very red.");
  }
  case RGB rgb when rgb.green > 100 -> {
    System.out.println("Sort of green...");
  }
  case RGB rgb -> {
    System.out.println("Not that red...");
  }
}
  • 기존에는 case RGB(...) 본문 안에 다시 if (red > 200)을 넣어야 했음
  • Java는 true로 평가되는 첫 번째 case를 eager하게 매칭하므로, 더 구체적인 case를 먼저 두고 덜 구체적인 case를 뒤에 두어야 함
  • guard가 붙은 RGB case 뒤에는 exhaustive를 유지하기 위해 일반 case RGB rgb가 필요함

MatchException이 발생하는 경우

  • Java 21의 패턴 매칭에는 java.lang.MatchException 이 추가로 관련됨
  • record accessor가 예외를 던지면 switch pattern이 실패하며 MatchException이 발생할 수 있음
record R(int i) {
    public int i() {
        return i / 0;
    }
}

static void exampleAnR(R r) {
    switch(r) {
        case R(var i): System.out.println(i);
    }
}
  • 위 예시에서는 i() accessor가 ArithmeticException을 던지기 때문에 switch 블록이 MatchException을 던짐
  • JEP 441에 따르면 항상 예외를 던지는 record accessor는 매우 비정상적이며, exhaustive pattern switch가 MatchException을 던지는 경우도 매우 이례적임
  • exhaustive switch에서도 selector에 대해 지정된 variant가 하나도 매칭되지 않으면 예외가 발생할 수 있음
    • JEP 441은 enum에 대한 exhaustive switch가 매칭에 실패하는 경우를 switch가 컴파일된 뒤 enum class가 변경된 상황으로 설명함
  • guard clause 실행 중 예외가 발생해도 MatchException이 발생할 수 있음
static void example(Object obj) {
    switch (obj) {
        case R r when (r.i / 0 == 1): System.out.println("It's an R!");
        default: break;
    }
}

남은 범위

  • Java 21의 records, sealed types, switch pattern matching, guard clause를 조합하면 함수형 프로그래밍의 빌딩 블록을 Java 코드에 적용할 수 있음
  • generics가 switch patterns와 상호작용하는 방식 같은 일부 주제는 다루지 않음
  • 다음 글에서는 Java 코드 작성 방식을 개선하는 데 사용할 수 있는 quirks와 실용적인 예시를 다룰 예정임

댓글과 토론

Hacker News 의견들
  • Java 21의 가장 큰 기능은 가상 스레드 릴리스임: https://openjdk.org/jeps/444
    어째서인지 글에서는 빠져 있음. 기존 Go 개발자를 Java로 끌어올 만한 기능이 있다면 이게 될 수 있고, 반응형 스타일 동시성 패턴을 싫어하던 사람들도 설득할 수 있을 듯함

    • 기존 Go 개발자가 Java로 돌아갈 것 같지는 않음. Java로 10년 일하다 Go로 옮겼고, 다시 돌아갈 생각은 없음
      Java 애플리케이션과 라이브러리는 상속, 패키징, 객체지향, 빌드 도구 등 때문에 Go에 비해 추론하고 이해하기가 너무 어렵기 때문임
      Go는 단순하고 이해·읽기·유지보수가 쉬움. 패키징은 컴퓨터에서 파일을 단일 폴더에 정리하는 방식과 비슷하고, 도구도 언어에 내장되어 있음. IntelliJ 같은 IDE가 있어야 겨우 쓸 만해지는 느낌도 아님
      지금은 달라졌을 수도 있지만, 요즘 보는 Java 라이브러리 대부분은 여전히 이런 모습으로 보임
    • Java 21 때문에 가상 스레드가 들어간 다음 JRuby 릴리스를 기대하고 있음. Charles Nutter가 8월 JRuby 발표에서 Ruby 파이버에 미치는 영향을 데모로 보여줬는데 꽤 큼
      JVM과 그 도구 체계에는 좋아하는 점이 많지만, 이제 Java 코드를 쓰는 건 별로임. JRuby는 양쪽의 장점을 어느 정도 같이 줌
      발표는 여기 있고, 가상 스레드 데모는 45분쯤부터 나옴
      https://youtu.be/pzm6I4liJlg?si=GtxQ4MThEaNDfC67
    • 기존 Golang 개발자가 이 기능 때문에 Java로 옮길지는 잘 모르겠음. 다만 방향성 측면에서, 왜 Go 커뮤니티에는 동시성 컨테이너가 이렇게 적고 Java 커뮤니티에는 많은지 궁금함
      sync.Map조차 Java의 범용 ConcurrentMap 같은 것이 아니라 두 가지 특정 사용 사례에 특화되어 있음. Java에는 동시성 집합, 큐, 배리어, 페이저, 포크-조인 풀 등이 있음. Go 루틴이 있어도 이런 컨테이너는 충분히 쓸모가 있을 텐데, 적어도 포크-조인은 구현이 그렇게 사소하지 않음. 어디에나 뮤텍스를 쓰는 건 너무 저수준처럼 느껴짐
      서드파티 구현이 있다는 건 알지만, 동시성은 제대로 만들기 너무 까다로워서 Java의 JCTools나 Google Guava처럼 성숙하고 많은 사용자와 개발자가 뒷받침하는 수준이 아니라면 서드파티 패키지를 채택하기 망설여짐
    • Executor.newVirtualThreadPerTaskExecutorgo의 대비가, Go 개발자들이 Java로 옮기지 않을 거라고 보는 핵심을 잘 보여줌
      정정하면 실제로는 go 대신 try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(...) }에 가까움
    • Java 21은 구조적 동시성도 프리뷰로 제공함(https://openjdk.org/jeps/453). 가상 스레드 구현을 사용하고, 예제만 봐도 꽤 좋아 보이며 스레드 기반 동시성을 다룰 때의 여러 고통을 줄여줌
  • 블로그 글 제목은 선택이 별로였다고 봄. 숨겨진 부제는 "Algebraic data types in Java"인데, 이쪽이 내용 설명에 훨씬 맞음. 더 나은 제목은 Algebraic data types in Java 21이었을 것임
    아마 제목 때문인지 여기 댓글 상당수가 주제에서 벗어남. 대수적 자료형, Java 구현의 장단점, 다른 언어와의 기술적 비교를 더 보고 싶었음

    • 대수적 자료형이 더 인기 있는 언어가 있었으면 좋겠다고 생각하지만, Java에 대수적 자료형이 들어가는 걸 정말 보고 싶은지는 잘 모르겠음
      기존 Java 코드가 사라지는 건 아니니, 이런 코드가 무작위로 섞이면 정말 더 나아질까 싶음
    • 처음엔 그렇게 제목을 붙였지만 마지막 순간에 바꿨고, 결과적으로 글의 방향을 빗나가게 만든 듯함
  • 여기서 설명한 sealed classes 기능은 뭔가 완전히 잘못된 느낌임
    일반 인터페이스가 있으면 누구나 그것을 구현하는 새 클래스를 만들 수 있고, if (x instanceof Foo) { ... } else if (x instanceof Bar) { ... } 같은 코드는 누군가 새 클래스를 추가하면 런타임에 깨진다는 논리임. 그래서 새 sealed 인터페이스 기능으로 아무도 그 인터페이스를 구현하는 새 클래스를 만들지 못하게 하면 if 문이 깨지지 않는다는 식임
    그런데 객체지향 프로그래밍은 이미 이런 문제를 생각하고 해결해두지 않았나? 지금 객체지향이 유행에서 멀어졌다는 건 알지만 Java는 객체지향 언어임
    해결책은 인터페이스에 메서드를 추가하고 모든 클래스가 그걸 구현하게 하는 것임. 그러면 모든 선택지를 거대한 if/switch 문으로 나열하는 대신 메서드를 호출하면 됨
    이 방식은 코드를 확장하지 못하게 막는 것보다 낫고, 오히려 확장할 수 있게 해줌. 새 구현자는 그 메서드만 구현하면 되고, 컴파일러가 강제하므로 실수로 빼먹을 수도 없음
    글의 색 공간 예시(RGB, CMYK 등)는 아주 좋은 반례임. 색 공간을 쓰는 코드를 작성했는데, 사용자나 고객이 내가 생각 못 한 이상하고 드문 색 공간을 써야 할 수도 있음. 거대한 if/switch 문에 나열한 색 공간만 지원하며, 그런 구조 때문에 확장 불가능한 코드를 만들고 싶지는 않음

    • 인터페이스에 메서드를 추가하라는 해결책은, 앞으로 필요한 모든 연산을 미리 알 수 없으면 곤란해짐
      이 문제를 sealed classes가 해결함. 하지만 대신 "확장 클래스가 더 필요하고 그 전부를 미리 알 수 없다면?"이라는 새 문제가 생김. 결국 둘 다 달성할 방법이 있느냐는 질문이 됨
      이 문제는 표현 문제(expression problem) 라고 부름 [1]
      표현 문제를 풀 수 있는 정적 타입 언어들이 있고, Java도 그중 하나임 [2]. 다만 Java에서 그렇게 하는 방식은 여전히 매우 복잡하고 불편해서 거의 쓰이지 않음. Haskell이나 JVM 세계에 머물고 싶다면 Scala가 이 부분을 훨씬 잘함
      [1] https://en.wikipedia.org/wiki/Expression_problem
      [2] https://koerbitz.me/posts/Solving-the-Expression-Problem-in-...
    • sealed classes 방식도 누구나 코드를 확장할 수 있게 해주지만, 인터페이스 메서드 방식과는 다른 차원에서 확장됨
      인터페이스 메서드와 가상 호출 방식은 새 클래스를 추가하는 대신 새 연산을 추가하고 싶을 때 매우 유연하지 않음. 새 연산 하나만 추가하려 해도 모든 구현체에 가서 새 메서드를 넣어야 하고, 접근 권한이 없는 구현체를 깨뜨릴 수도 있음. 서로 관련 없는 메서드들도 한 클래스 안에 정의되어야 하므로 코드 가독성이 크게 나빠지고, 가상 호출도 공짜가 아니라 성능에도 영향이 있음
      이 경우 sealed class가 훨씬 잘 확장됨. 한 곳에 새 switch를 추가하면 끝이고, 하위 호환성도 깨지지 않음
      이것이 유명한 표현 문제
      https://pkolaczk.github.io/in-defense-of-switch/
    • instanceof 대신 다형적 디스패치를 쓰라는 추천은 이해하고, Bob Martin이 그 얘기를 길게 하는 것도 봤지만 동의하지 않음
      이런 다형적 디스패치를 하려면 객체가 여러 관심사를 스스로 처리해야 함
      비디오 게임에서 Car.render(), .collide(), .playSound()를 가질 수 있음. 나중에 Dog를 추가해도 이 세 메서드를 구현하면 되고, Renderer, PhysicsEngine, SoundEngine을 수정하거나 다시 컴파일할 필요가 없음. 다른 프로그래머도 내 소중한 코드에 버그를 넣지 않고 엔티티를 추가할 수 있음. 좋아 보임
      하지만 이제 CarDog가 그래픽, 물리, 사운드를 모두 알아야 함. 그리고 엔티티는 고립되어 존재하지 않음. 자동차와 개는 올바른 순서로 렌더링되어야 하고 서로 가릴 수도 있음. 충돌도 서로 확인해야 함. 실제 게임 잼에서 겪었듯, 사운드 담당자가 소리 동작을 추가하려고 모든 객체 안으로 들어가야 하는 상황도 생김
      차라리 물리를 생각할 때는 Physics.collideAll() 안에서 작업하고 필요하면 instanceof로 특수 처리하며, 그래픽을 생각할 때는 Graphics.renderAll() 안에서 작업하는 편이 훨씬 좋음
      일상적인 백엔드 Java 웹 개발에서도 비슷함. REST 컨트롤러에서 Java 객체를 HTTP 응답으로 바꾸는 방식을 결정할 때, 한 메서드 안에서 전부 보고 {instanceof Forbidden}은 403, {instanceof NotFound}는 404로 매핑하는 편이 좋음. Java 클래스 자체에 getCode()나 REST 전용 내용을 넣고 싶지는 않음
    • 확장을 허용하는 것이 항상 말이 되는 건 아님. Stringfinal인 데는 이유가 있고, 심지어 final을 기본값으로 두고 하위 클래스화를 허용할 클래스만 open으로 명시해야 한다고 볼 수도 있음
      함수형 프로그래밍에서 합 타입의 전형적인 예는 리스트임. 여기에는 Element(T head, List tail)Nil()만 있음. 이를 확장할 이유가 없고, 실제로 확장하면 리스트를 다루는 모든 함수와 결합될 때 잘못된 코드가 될 수 있음
      또 패턴 매칭과 유사한 방문자 패턴은 매우 장황하고 Java의 일반 메서드 디스패치 의미론을 이용한 해킹에 의존함. 여기서는 패턴 매칭이 몇 배는 더 읽기 쉽다고 봄
    • 이런 기능에는 유효한 사용 사례가 있음
      예를 들어 보안 토큰을 검증하는 보안 인터페이스를 생각해볼 수 있음
      일반 인터페이스라면 구현해서 토큰을 무시하거나(전부 허용), 토큰을 빼돌리거나, 백도어를 넣는 일이 쉬움. 그런 클래스가 보안 검사가 이루어지는 곳에 주입되면 보안이 뚫릴 수 있음
      sealed 인터페이스라면 승인되지 않은 새 구현체가 존재할 수 없음. 그 인터페이스를 구현한다고 주장하는 객체를 받았다면, 실제 보안 검사를 수행하는 검증된 구현체 중 하나임이 보장됨. 보안 버그와 익스플로잇의 한 부류를 통째로 제거한 셈임
  • 합 타입은 알지만 Java의 합 타입은 잘 모르는 입장에서 좋은 글임
    다만 합 타입만으로 Java를 좋아하게 될지는 모르겠음. 광범위한 null 가능성은 여전히 남아 있고, 이 글에서도 여러 번 고개를 듦

    • Java에서 null 가능성은 큰 문제지만, 어노테이션 기반 null 가능성 프레임워크는 효과적이고 생태계 전반에 퍼져 있음. 개인적으로는 거의 필수라고 봄
      Google, Meta, Microsoft 등이 @Nullable부터 시작해 어노테이션을 표준화하려는 https://jspecify.dev/가 정말 기대됨
    • Valhalla가 들어오면 명시적 null 가능성도 생길 테니 그 문제도 처리될 것임
    • 아직 표현식 지향 언어도 아님
  • "왜 product type이라고 부르나?"에 대한 글의 답이 틀린 건 아니지만, 더 직관적이고 간결하게 말하면 product type에서 가능한 값의 총수는 구성 타입들의 가능한 값 수를 곱한 것임
    product를 sum으로 바꿔도 똑같이 성립함
    흥미롭게도 a -> b 형태의 고유 함수 총수는 입력과 출력만 놓고 보면 지수로 구할 수 있음. 즉 (b의 가능한 값 수) ^ (a의 가능한 값 수)

    • 더 직관적이고 간결하게 말하면, product type은 집합의 데카르트 곱과 동등함
    • 맵과 리스트도 지수 타입의 다른 예임. 순수 함수는 이론적으로 미리 계산된 값에 대한 맵 조회로 대체될 수 있으므로, 이것들이 함수와 같다는 직관은 자연스러움. 이 맥락에서 리스트는 키가 정수인 특수한 맵임
      수학적으로 써보면, Bool의 리스트에서 왼쪽은 원소 수이고 오른쪽은 전체 가능성 수임
      0 : 1
      1 : 2
      2 : 4
      3 : 8
      4 : 16
      5 : 32
      이런 식으로 이어짐
  • Project Valhalla가 완료되어 Java에 마침내 값 타입이 들어오기를 기다리고 있음. 그러면 합 타입, 값 타입, 고루틴까지 갖춰서 꽤 괜찮은 언어 중 하나가 될 듯함

  • Java는 원래 정말 나쁜 언어는 아니었음
    문제는 사람들이었음. 거대한 과잉 엔지니어링, 코드베이스 파악을 어렵게 만드는 너무 많은 추상 개념, 역방향 GOTO 문 같은 어노테이션 형태의 코드 주술, DI 프레임워크가 문제였음
    고쳐야 할 것은 언어가 아니라 생태계임. Java 생태계 안에 일종의 "종교개혁" 운동이 필요함
    Kotlin, Clojure, Scala로 옮겨가는 것만으로는 충분하지 않음

    • 어떤 언어에서든 HammerFactory를 찍어내는 HammerFactoryFactory를 만들 수 있음. 하지만 Java 생태계는 이런 문제 해결 방식을 조장하고 장려함. C#도 비슷하다고 봄
      Java에 정말 필요한 한 가지는 독립 함수, 혹은 네임스페이스가 붙은 함수임. 가끔은 클래스가 필요 없고 모듈이나 네임스페이스 안의 함수면 충분한데, 그게 왜 안 되는지 모르겠음
  • 저자는 Records가 필요한 이유를 설명하며, 대부분의 Java 객체가 모든 필드를 private으로 두고 읽기·쓰기 접근자 메서드로만 접근하게 한다는 점을 듦
    하지만 접근자를 정의하는 언어 차원의 강제 규약은 없어서, foo의 getter 이름을 getBar로 붙여도 동작은 하지만 bar에 접근하려는 사람을 혼란스럽게 만들 수 있음
    Scala는 unapply 메서드를 구현한 객체에 대한 패턴 매칭을 지원함. 이 방식이 해로운 것으로 여겨지는 걸까? Java는 왜 이 경로를 따르지 않았을까?

    • 또 표준화 문제임. Java의 표준화는 C++처럼 느림. record pattern JEP의 마지막 각주를 보면 unapply 같은 것이 준비 중일 수도 있다고 암시하니, 아직 희망이 완전히 사라진 건 아님
  • Java는 늘 훌륭한 언어였음. 토할 것 같게 만드는 건 엔터프라이즈식 생태계임. 한 줄의 로직을 구현하려고 수십 개의 클래스와 인터페이스를 쓰는 걸 본 적이 있음