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나 기존의 데이터 집계자 타입을 완전히 대체하려는 것이 아닌 더 함수적이고 불변한 접근 방식에 옵션을 제공한다.
반응형
'스터디 > 함수형 프로그래밍 with 자바' 카테고리의 다른 글
Chapter 07. 스트림 사용하기 (0) | 2024.06.16 |
---|---|
Chapter 06. 스트림 (Stream) (1) | 2024.06.06 |
Chapter 04. 불변성 (0) | 2024.05.21 |
Chapter 03. JDK의 함수형 인터페이스 (1) | 2024.05.16 |
Chapter 02. 자바 람다 (0) | 2024.05.08 |