728x90

5. 레코드

5.1 데이터 집계 유형

  • 데이터 집계는 다양한 소스로부터 데이터를 수집하고 의도한 목적에 부합하도록 표현하는 과정

튜플

  • 튜플은 요소의 유한하며 순서가 있는 집합이며 여러 값 또는 객체를 모은 자료 구조
  • 종류
    • 구조적 튜플: 요소들의 순서에만 의존하므로 인덱스를 통해서만 접근이 가능
    • 명목상 튜플: 데이터에 접근하기 위한 방법으로 인덱스를 사용하지 않고 컴포넌트명을 사용
public class TupleExample {
  public static void main(String[] args) {
    Map.Entry<String, Integer> person = new AbstractMap.SimpleEntry<>("Pli", 30);
    System.out.println("이름: " + person.getKey());
    System.out.println("나이: " + person.getValue());
  }
}

간단한 POJO

  • 자바에서 가장 불편한 점은 기본적인 작업을 수행하기 위해 형식적인 코드가 과도하게 많이 필요한 것.
public class SimplePojoPerson {
  private final String name;
  private final int age;

  public SimplePojoPerson(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public String getName() {
    return name;
  }

  public int getAge() {
    return age;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    SimplePojoPerson person = (SimplePojoPerson) o;
    return age == person.age && name.equals(person.name);
  }

  @Override
  public int hashCode() {
    return Objects.hash(name, age);
  }

  @Override
  public String toString() {
    return "SimplePojoPerson{" +
        "name='" + name + '\'' +
        ", age=" + age +
        '}';
  }

}

불변 POJO 만들기

  • 타입을 변형할 수 없게 하면 setter 메서드와 빈 생성자 코드를 작성하지 않아도 되지만, 그 외의 모든 코드는 여전히 필요하다.
public class ImmutablePerson {
  private final String name;
  private final int age;

  public ImmutablePerson(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public String getName() {
    return name;
  }

  public int getAge() {
    return age;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    ImmutablePerson person = (ImmutablePerson) o;
    return age == person.age && name.equals(person.name);
  }

  @Override
  public int hashCode() {
    return Objects.hash(name, age);
  }

  @Override
  public String toString() {
    return "ImmutablePerson{" +
        "name='" + name + '\'' +
        ", age=" + age +
        '}';
  }
}

POJO를 레코드로 만들기

public record PersonRecord(String name, int age) {

}

5.2 도움을 주기 위한 레코드

  • 레코드는 명목상 튜플과 같이 이름으로 데이터 컴포넌트에 접근하는 단순한 데이터 집계 타입을 정의하는 방법
  • 레코드의 일반적인 구문
    • 다른 유형과 동일한 속성을 정의하는 헤더(Header), 추가 생성자와 메서드를 지원하기 위한 바디(Body)로 구성
  • 가시성
    • 레코드는 자바의 접근 제한자 키워드를 지원합니다.
  • record 키워드
    • 헤더를 다른 타입의 class, enum, interface와 구분합니다.
  • 이름
    • 네이버 규칙은 자바 언어 명세서에 정의된 다른 식별자와 동일합니다.
  • 제네릭 타입
    • 자바에서의 다른 타입과 마찬가지로 지원됩니다.
public record Generic<T>(T value) {
  public T getValue() {
    return value;
  }
}
  • 데이터 컴포넌트
    • 이름 다음에는 레코드의 컴포넌트를 포함하는 한 쌍의 괄호가 따릅니다. 각 컴포넌트는 내부적으로 private final 필드와 public 접근자 메서드로 변환되며, 컴포넌트 목록은 레코드의 생성자를 통해 확인할 수 있습니다.
  • 바디
    • 일반적인 자바 바디를 가질 수 있습니다.

내부 동작

  • 접근자 메서드의 차이만 존재할 뿐,두 클래스는 기능적으로 동일하다.
  • 레코드는 필요에 따라 코드를 더 작성하지 않고도 완전한 데이터 집계 유형을 제공한다.
public record InnerAction(String action, int tryCount) {
  public static void main(String[] args) {
    InnerAction innerAction = new InnerAction("Action", 3);
    System.out.println(innerAction);
    System.out.println(innerAction.action());
    System.out.println(innerAction.tryCount());
  }
}

레코드의 특징

  • 반복적으로 보일러 플레이트를 작성할 필요 없이 효율적인 기능을 제공할 수 있도록 하며, 데이터 집계자로써의 역할을 한다.

컴포넌트 접근자

  • 모든 레코드 컴포넌트는 private 필드로 저장되며, 레코드 내부에서는 필드에 직접적으로 접근할 수 있다.
  • 접근자 메서드는 해당 필드의 값을 그대로 반환한다.
  • 레코드는 불변한 데이터를 보관하도록 설계되었으므로, 데이터에 접근하면서 어떠한 처리나 판단을 하는 것은 좋지 않다.

표준, 간결, 사용자 정의 생성자

  • 레코드의 각 컴포넌트에 따라 자동으로 생성되는 생성자를 표준생성자라고 부릅니다.
  • 레코드의 컴포넌트들은 그대로 해당 필드에 매칭되어 할당된다.
public record ConstructorEx(String value, int count) {
  public ConstructorEx(){
    this("default", 0);
  }

  public ConstructorEx(String newValue) {
    this(newValue, 0);
  }

  public static void main(String[] args) {
    ConstructorEx constructorEx = new ConstructorEx();
    System.out.println(constructorEx);
    ConstructorEx constructorEx2 = new ConstructorEx("new value");
    System.out.println(constructorEx2);
  }
}

객체 식별과 설명

  • 레코드는 데이터 동등성을 기반으로 하는 hashCode와 equals 메서드의 표준구현을 제공한다.
public record Company(String name, String address) {
  public static void main(String[] args) {
    Company company = new Company("JavaJob", "Seoul");
    Company company2 = new Company("GoJob", "Seoul");
    System.out.println(company);
    System.out.println(company.equals(company2));
    System.out.println(company.hashCode() == company2.hashCode());
  }

제네릭

  • 레코드는 일반적인 규칙을 따라는 제네릭을 지원합니다.
public record Pair<T, U>(T first, U second) {
  public static void main(String[] args) {
    Pair<String, Integer> pair = new Pair<>("One", 1);
    System.out.println(pair.first());
    System.out.println(pair.second());
  }
}

어노테이션

  • 레코드는 자동으로 생성되는 필드, 컴포넌트 접근자로 인하여 추가적인 고려 사항이 피룡합니다. 어노테이션을 지원하기 위해서 대상 필드, 파라미터 또는 메서드가 있는 어노테이션은 컴포넌트에 적용되는 경우 해당 위치로 전파된다.
public record Account(@NonNull String owner, double balance) {
  public static void main(String[] args) {
    Account account = new Account("Pli", 100.0);
    System.out.println(account.owner());
    System.out.println(account.balance());
  }
}

리플렉션

  • 자바 16에서 기존 리플렉션 기능을 보완하기 위해 getRecordComponents()메서드를 추가하였다.
  • java.lang.reflect.RecordComponent객체의 배열을 반환하여 다른 타입의 Class에 대해서는 null을 반환한다.
public record RecordReflection(String value, int count) {
  public static void main(String[] args) {
    RecordComponent[] components = RecordReflection.class.getRecordComponents();
    for (RecordComponent component : components) {
      System.out.println("Component: " + component.getName());
    }
  }
}

5.2.3 누락된 기능

  • 레코드는 단순하고 투명하며 얕은 불변성을 가진 데이터 수집기입니다.

추가적인 상태

  • 바디에 필드가 추가되는 경우 컴파일러 에러가 발생한다.
  • 레코드에 새로운 메서드를 추가함으로써 기존 컴포넌트를 기반으로 한 파생 상태를 만들 수 있다.
public record Circle(int radius) {
//  private final double PI = 3.141592; -> 불가
  public double area() { // -> 메서드를 만들어서 처리
    return Math.PI * radius * radius;
  }

  public static void main(String[] args) {
    Circle circle = new Circle(10);
    System.out.println(circle.area());
  }
}

상속

  • 레코드는 이미 내부적으로 java.lang.Record를 상속한 final 타입입니다.
    • 이미 상속을 하고 있기 때문에, 레코드는 상속을 사용할 수 없지만, 인터페이스를 사용하여 레코드 템플릿을 정의하고 default 메서드를 통해 공통 기능을 공유할 수 있습니다.
interface Describable {
  default String describe() {
    return "Describable: " + this.toString();
  }
}
public record Item(String name) implements Describable{
  public static void main(String[] args) {
    Item item = new Item("자바칩");
    System.out.println(item.describe());
  }
}

컴포넌트의 기본값과 컴팩트 생성자

  • 자바는 생성자나 메서드 인스에 대한 기본값을 지원하지 않으며, 레코드는 모든 요소를 갖는 표준 생성자만 가지고 있습니다.
public record Computer(String cpu, String ram) {
  public Computer{
    if (cpu == null || ram == null) {
      throw new IllegalArgumentException("둘 다 필요해");
    }
  }
  public static void main(String[] args) {
    Computer computer = new Computer("i7", "16G");
    System.out.println(computer);
  }
}

단계별 생성

  • 불변 자료 구조에서는 하프 초기화상태의 객체가 없습니다.
  • 모든 자료 구조가 동시에 초기화 되는 것은 아니므로, 변경 가능한 자료구조 대신 빌더 패턴을 활용하여 변경 가능한 중간 단계의 변수를 사용하고, 최종적으로 불변한 결과를 생성할 수 있다.

5.3 사용 사례와 일반적인 관행

레코드 유효성 검사 및 데이터 정제

  • 레코드는 일반적인 생성자와 달리 컴팩트 생성자를 지원한다. 표준 생성자의 모든 컴포넌트에 접근할 수 있지만, 별도의 인수가 필요하지 않다.
  • 예외를 발생 시킬 수도 있지만, 데이터를 정제하고, 적절한 값을 조정하여 유효한 레코드는 만드는 방법도 있다.

불변성 강화

  • 본질적으로 불변이 아닌 레코드 컴포넌트에도 예기치 않은 변경과 같은 기본적인 문제를 고려해야 한다.
  • 간단한 방법은 해당 컴포넌트를 복사하거나 래핑하여 불변성 수준을 강화하는 방법이 있다.

변형된 복사본 생성

wither 메서드

  • 일반적으로 setter와 유사하지만 기존 인스턴스를 변경하는 대신 새 인스턴스를 반환한다.
  • 한계로는 각 컴포넌트마다 메서드를 작성해야한다는 점
public record Computer(String cpu, String ram) {
  public Computer withCpu(String cpu) {
    return new Computer(cpu, this.ram);
  }

  public Computer withRam(String ram) {
    return new Computer(this.cpu, ram);
  }

  public static void main(String[] args) {
    Computer computer = new Computer("i7", "16G");
    System.out.println(computer);
    Computer computer2 = computer.withCpu("i9");
    System.out.println(computer2);
    Computer computer3 = computer.withRam("32G");
    System.out.println(computer3);
  }
}

필더 패턴

  • 생성자를 사용하고 기존 레코드로 빌더를 초기화한 후, 필요한 변경을 수행하고 새로운 레코드를 생성할 수 있다.
  • 컴포넌트와 레코드 복제에 필요한 코드 사이에 강한 응집도로 인하여 리팩터링이 어려워 진다.
    • 도구적 접근법을 사용하여 보완할 수 있다.

도구 지원 빌더

  • 어노테이션 프로세서를 활용하는 방법이 있다.
  • RecordBuilder같은 도구를 활용하면 다양한 레코드에 대한 빌더를 편리하게 생성할 수 있고, 사용자는 어노테이션만 추가하면 된다.

명목상 튜플로써의 로컬 레코드

  • 자바에서는 동적 튜플을 지원히지 않는다.
  • 대부분의 튜플 구현과 레코드(명목상 튜플)의 큰 차이점은, 자바의 타입 시스템으로 인해 포함된 데이터가 umbrella 타입으로 유지된다는 점이다.

Optional 데이터 처리

null이 아닌 컨테이너 확보

  • Optional<String>null이 되지 않도록 보장해야 합니다.
  • 가장 쉬운 방법은 컴팩트 생성자로 유효성을 검사하는 것입니다.
public record OptionalComputer(String cpu, Optional<String> ram) {
  public OptionalComputer{
    if (cpu == null || cpu.isBlank()) {
      throw new IllegalArgumentException("CPU 필요해");
    }
    ram = Optional.of(ram.orElse("4G"));
  }

  public static void main(String[] args) {
    OptionalComputer computer = new OptionalComputer("i7", Optional.empty());
    System.out.println(computer);
  }
}

컴팩트 생성자 추가

  • Non-Optional<T> 기반의 인수들로 추가 생성자를 제공하고, 컨테이너 타입을 직접 생성하는 것

레코드의 진화된 직렬화

  • 레코드는 컴포넌트로 표현되는 불변 상태로만 정의된다. 생성 후 상ㅇ태에 영향을 주는 어떠한 코드도 없기 때문에 직렬화 과정은 꽤 간단하다.
    • 직렬화는 레코드의 컴포넌트만을 기반으로 한다.
    • 역직렬화는 리플렉션 대신 기본 생성자만을 필요로 한다.
  • JVM이 레코드의 직렬화된 형태를 파생하면 해당 인스턴스 생성자는 캐싱될 수 있다. 이 과정을 커스터마이징 하는 것은 불가능하며, JVM에 레코드의 직렬화된 표현에 제어권을 부여함으로써 더 안전한 직렬화 과정을 수행할 수 있게 한다.
public record SerializedComputer(String cpu, String ram) implements Serializable {

  public static void main(String[] args) {
    SerializedComputer computer = new SerializedComputer("i7", "16G");

    try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("computer.txt"))) {
      out.writeObject(computer);
    } catch (Exception e) {
      e.printStackTrace();
    }

    try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("computer.txt"))) {
      SerializedComputer computer2 = (SerializedComputer) in.readObject();
      System.out.println(computer2);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

}

레코드 패턴 매칭(자바 19+)

public class PatternMatching {

  public static void main(String[] args) {
    Object obj = new Computer("i7", "16G");

    if (obj instanceof Computer(String cpu, String ram)) {
      System.out.println(cpu);
      System.out.println(ram);
    }
  }

  public record Computer(String cpu, String ram) {
  }

}

레코드를 마무리하며

  • 처음에는 임의적이고 제한적으로 보일 수 있지만 더 안전하고 일관된 사용성을 제공한다.
  • POJO나 기존의 데이터 집계자 타입을 완전히 대체하려는 것이 아닌 더 함수적이고 불변한 접근 방식에 옵션을 제공한다.
반응형
코드플리