레코드는 간단한 구문으로 다양한 형태의 데이터를 집계할 수 있는 기능을 가진다.
데이터 집계란 특정 목적을 위해 데이터를 모으고 구조화하는 것을 의미한다.
대표적으로 튜플이 있다.
튜플
- 여러 값을 하나의 묶음으로 저장할 수 있는 자료형
1. 구조적 튜플
- 데이터의 순서와 개수를 기준으로 정의
- 데이터의 타입과 순서에만 의존하므로 인덱스를 통해서만 접근이 가능하다.
// Java는 기본적으로 튜플 제공 x, 외부 라이브러리나 레코드를 통해 유사한 구조를 만들 수 있다.
//org.apache.commons.lang3.tuple.Pair 라이브러리를 사용한 구조적 튜플 예제
public class TupleExample {
public static void main(String[] args) {
Pair<String, Integer> person = Pair.of("John", 25);
String name = person.getLeft();
int age = person.getRight();
System.out.println("Name: " + name + ", Age: " + age);
}
}
1. 명목상 튜플
- 데이터의 의미와 타입을 기반으로 정의
- 각 요소는 명명된 필드를 가지고 있으며, 인덱스를 사용하지 않고 타입명을 사용한다.
public class RecordExample {
public static void main(String[] args) {
Person person = new Person("John", 25);
System.out.println("Name: " + person.name() + ", Age: " + person.age());
}
public record Person(String name, int age) {}
}
레코드는 명목상 튜플과 같이 이름으로 데이터 컴포넌트에 접근하여 데이터 집계 타입을 정의한다.
특징
줄어든 보일러플레이트
- 기존 클래스를 정의할 때, 필드, 생성자, 접근자, equals(), hashcode(), toString() 등의 메서드를 모두 작성해줘야 했다.
- 레코드 사용시 레코드 선언만으로 위의 메서드들이 자동으로 생성된다.
불변성
- 모든 레코드 컴포넌트는 private final 필드로 저장된다.
- 레코드의 필드는 한 번 설정되면 변경할 수 없다.
- 레코드 내부에서는 필드에 직접 접근이 가능하지만, 외부에서는 public 접근자 메서드를 통해서만 접근 가능하다.
- 불변하게 설계되어 데이터의 일관성과 안정성을 유지할 수 있다.
사용 사례
1. 빌터 패턴
- 레코드에서 빌터 패턴을 사용한다면 레코드의 불변성과 간결함을 유지하면서도 객체 생성시 유연성을 높일 수 있다.
- 사용되는 빌더 클래스는 객체의 생성을 담당하고, 레코드는 객체를 표현하는 것에 집중하여 역할을 명확하게 분리할 수 있다.
public class BuilderRecordEx {
public static void main(String[] args) {
User user = User.builder()
.id(1L)
.username("성현")
.build();
System.out.println(user.id()); // 1
System.out.println(user.username()); // 성현
}
public record User(Long id, String username) {
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
private Long id;
private String username;
public UserBuilder id(Long id) {
this.id = id;
return this;
}
public UserBuilder username(String username) {
this.username = username;
return this;
}
public User build() {
return new User(id, username);
}
}
}
}
2. 로컬 레코드
- 메서드 내부에 선언되는 레코드를 의미한다.
- 특정 스코프 내에서만 사용되는 임시적인 데이터 구조를 정의할 때 유용하다.
public class LocalRecord {
public static void main(String[] args) {
Map<Integer, List<String>> albums = Map.of(1991, List.of("Music1", "Listen Without PreJudice"),
1992, List.of("Music2", "Ten", "Blue lines"),
1993, List.of("Music3", "CheekToCheek", "Christian Mcbride"),
1994, List.of("Music4", "One Not Samba"),
1995, List.of("Music5", "Jazz Crime", "Joshua Redman"));
List<String> result = filtersAlbums(albums, 1993);
for (String s : result) {
System.out.println(s);
}
// Music3
// CheekToCheek
// Christian Mcbride
// Music4
// One Not Samba
// Music5
// Jazz Crime
// Joshua Redman
}
public static List<String> filtersAlbums(Map<Integer, List<String>> albums, int minimumYear) {
record AlbumsPerYear(int year, List<String> titles) {
AlbumsPerYear(Map.Entry<Integer, List<String>> entry) {
this(entry.getKey(), entry.getValue());
}
static Predicate<AlbumsPerYear> minimumYear(int year) {
return albumsPerYear -> albumsPerYear.year() >= year;
}
static Comparator<AlbumsPerYear> sortByYear() {
return Comparator.comparing(AlbumsPerYear::year);
}
}
return albums.entrySet()
.stream()
.filter(entry -> entry.getKey() >= minimumYear)
.sorted(Comparator.comparing(Map.Entry::getKey))
.map(Map.Entry::getValue)
.flatMap(List::stream)
.toList();
}
}
3. Optional
- 레코드에 Optional을 적용하면 필드가 null일 수 있는 상황을 명시적이고 안전하게 처리할 수 있다.
- 일반적으로 NullPointerException(NPE)은 객체나 배열의 참조가 null인 상태에서 해당 참조를 사용하려 할 때 발생한다.
- 아래의 컴팩트 생성자를 통해 유효성을 검사하게 된다면, NPE 문제를 객체 사용 시점에서 객체 생성 시점으로 이동시킬 수 있다.
public static void main(String[] args) {
User user = new User("userId", "성현", null); // 에러 발생
System.out.println(user.email);
// Exception in thread "main" java.lang.NullPointerException:
// Optional<String> group must not be null
}
public record User(String id, String name, Optional<String> email) {
public User {
Objects.requireNonNull(email, "Optional<String> group must not be null");
}
}
- 객체의 생성 시점에 NPE가 발생하여 디버깅 용이성을 높일 수 있지만 생성자엔 여전히 null값을 입력할 수 있다.
- 객체의 사용, 생성은 모두 런타임에서 동작한다.
- 런타임 시점이 아닌 컴파일 시점에 오류를 발생한다면 더 안전하고 편리하게 사용할 수 있다.
public static void main(String[] args) {
User user = new User("userId", "성현", null); // 컴파일 에러
System.out.println(user.email);
}
public record User(String id, String name, Optional<String> email) {
public User(String id, String name, String email) {
this(id, name, Optional.ofNullable(email));
}
}
- 자바 레코드는 가능한 한 적은 코드로 많은 극도의 간결성을 제공한다.
- 레코드의 고정된 구조로 인해 POJO나 사용자 정의 타입만큼 유연하지 않을 수 있지만, 더 안전하고 일관된 사용성을 제공한다.
'함수형 프로그래밍' 카테고리의 다른 글
[ch.07] 스트림 사용 (0) | 2024.06.16 |
---|---|
[ch.06] 스트림을 이용한 데이터 처리 (0) | 2024.06.06 |
[ch.04] 가변성 & 불변성 (0) | 2024.05.23 |
[ch.03] JDK 함수형 인터페이스 (0) | 2024.05.16 |
[ch.02] 함수형 자바 (0) | 2024.05.09 |