본문 바로가기

함수형 자바

[JAVA] 레코드 (Record) - Chapter 05

함수형 프로그래밍 자바의 5장에서 다루는 레코드에 대해 정리해 보았다.

 

레코드 (Record)

레코드란 자바 14에서 도입된 데이터 집계 유형 중 하나이다.

가장 대표적인 데이터 집계 유형에는 튜플이 있다.

레코드는 명목상 튜플과 마찬가지로, 순서대로 정렬된 값의 시퀀스를 집계하여 인덱스 대신 이름을 통해 데이터에 접근 할 수 있다.

또한 다른 클래스와 구분하기 위해 새로운 키워드인 record를 사용한다.

 

아래에서 불변 POJO 객체인 User객체와 record를 통해 생성된 User를 비교해 보자.

 

간단한 User POJO 객체

public final class PojoUser {

	private final String userName;
	private final LocalDateTime lastLogin;

	public PojoUser(String userName, LocalDateTime lastLogin) {
		this.userName = userName;
		this.lastLogin = lastLogin;
	}

	public String getUserName() {
		return userName;
	}

	public LocalDateTime getLastLogin() {
		return lastLogin;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o == null || getClass() != o.getClass())
			return false;
		PojoUser pojoUser = (PojoUser)o;
		return Objects.equals(userName, pojoUser.userName) && Objects.equals(lastLogin,
			pojoUser.lastLogin);
	}

	@Override
	public int hashCode() {
		return Objects.hash(userName, lastLogin);
	}

	@Override
	public String toString() {
		return "PojoUser{" +
			"userName='" + userName + '\'' +
			", lastLogin=" + lastLogin +
			'}';
	}
}

 

@Getter 나 @allargsconstructor 같은 롬복 어노테이션 등으로 보일러플레이트를 줄일 수는 있지만,

기본적인 작업을 위해 형식적인 코드가 많이 필요하다.

 

간단한 User record

public record RecordUser(String userName, LocalDateTime lastLogin) {
}

 

위의 간단한 PojoUser 객체와 동일한 기능을 갖춘 User record이다.

다른 클래스나 인터페이스와 마찬가지로 헤더에 접근 제한자 키원드를 지원하고, 제네릭 타입을 지원한다.

그리고 마찬가지로 바디를 가질 수 있다.

그리고 java.lang.Object가 아닌 java.lang.Record 를 명시적으로 상속한다.

 

레코드의 특징

1) 컴포넌트 접근자

 

모든 레코드 컴포넌트는 private 필드로 저장된다.

일반적인 getter의 get 없이 컴포넌트 이름과 동일한 public 메서드를 통해 컴포넌트에 접근 가능하다.

recordUser recordUser = new RecordUser("haneul", LocalDateTime.now());
String userName = recordUser.userName();

 

Override를 통해 메서드를 재정의 할 수 있지만, 추천하진 않는다.

 

2) 표준 생성자

 

레코드의 컴포넌트에 따라 자동으로 생성되는 생성자를 뜻한다.

유효성 검사나 필요에 따라 재정의 가능하다.

public record RecordUser(String userName, LocalDateTime lastLogin) {

	//표준 생성자 재정의
	public RecordUser(String userName, LocalDateTime lastLogin) {
		Objects.requireNonNull(userName);
		Objects.requireNonNull(lastLogin);

		this.userName = userName;
		this.lastLogin = lastLogin;
	}

	//위와 동일한 간결한 형태
	public RecordUser {
		Objects.requireNonNull(userName);
		Objects.requireNonNull(lastLogin);
		userName = userName;
		lastLogin = lastLogin;
	}
}

 

생성자에서 괄호를 포함한 모든 인수를 생략 가능한 간결한 형태가 제공된다.

이런 컴팩트 생성자는 유효성 검사를 수행하기 완벽한 지점이다.

또한 데이터를 정제하고 값을 조정할 수 있다.

 

3) 객체 식별과 설명

 

레코드에서는 데이터 동등성을 기반으로 하는 hashCode와 equaals메서드의 표준 구현을 제공한다.

또한 toString()역시 컴포넌트에서 자동 생성된다.

이러한 메서드 역시 재정의가 가능하다.

 

4) 제네릭

 

레코드에서도 일반적인 규칙을 따르는 제네릭을 지원한다.

다만 도메인 모델을 더 정확하게 나타내는 구체적은 레코드를 사용하여 표현력을 높이고 오용 경우를 줄이는 것을 권장한다 한다.

 

5) 어노테이션

 

레코드의 컴포넌트에 어노테이션을 사용할 경우 다양한 곳에 전파될 수 있다.

 

그 이유는, 각각의 컴포넌트는
이름과 타입이 같은 필드 (FIELD)에 상응하고,

이름과 리턴 타입이 같은 컴포넌트 접근 (METHOD)와 상응하고,

같은 이름과 타입이 같은 표즌 생성자의 인수 (PARAMETER)와 상응하기 때문에,

컴포넌트에 어노테이션이 붙으면 이 어노테이션이 적용가능한 곳 모두에 적용된다 한다.

 

즉, 컴포넌트에 어노테이션이 붙으면 필드, 접근 메서드, 생성자의 인수에 전파가 될 수 있다.

public record Card(@MyAnno Rank rank, @MyAnno Suit suit) { ... }

public final class Card {
    private final @MyAnno Rank rank;
    private final @MyAnno Suit suit;
    @MyAnno Rank rank() { return this.rank; }
    @MyAnno Suit suit() { return this.suit; }
    ...
}

 

다만 명시적으로 선언된 컴포넌트 접근자와 표준 생성자의 경우에는

레코드의 컴포넌트의 어노테이션이 전파되지 않고, 해당 메서드나 생성자에서 사용한 어노테이션만 적용된다고 한다.

 

또한 레코드 컴포넌트에 선언된 주석의 어노테이션 타입이 RECORD_COMPONENT이 아니라면, 런타임에 Reflection API를 통해 해당 어노테이션에 접근할 수 없다고 한다.

public static void main(String[] args) {
        for (RecordComponent component : RecordUser.class.getRecordComponents()) {
            System.out.println("Component: " + component.getName());
            for (Annotation annotation : component.getAnnotations()) {
                System.out.println("Annotation: " + annotation);
            }
        }
}

//@NonNull을 컴포넌트에 붙였을 때 아무런 결과가 나오지 않는다.

 

<reference>

https://openjdk.org/jeps/395

 

JEP 395: Records

JEP 395: Records OwnerGavin BiermanTypeFeatureScopeSEStatusClosed / DeliveredRelease16Componentspecification / languageDiscussionamber dash dev at openjdk dot java dot netRelates toJEP 359: Records (Preview)JEP 384: Records (Second Preview)Reviewed

openjdk.org

 

6) 리플렉션

 

자바 16에 리플렉션에 getRecordComponents메서드가 추가되었다.

RecordComponent 객체의 배열을 반환하며, 다른 타입의 Class에 대해서는 null을 반환한다.

또한 getDeclaredConstructors 메서드를 통해 표준 생성자를 찾을 수 있다.

public static void main(String[] args) {
        for (RecordComponent component : RecordUser.class.getRecordComponents()) {
            System.out.println("Component: " + component.getName());
            for (Annotation annotation : component.getAnnotations()) {
                System.out.println("Annotation: " + annotation);
            }
        }

        for (Constructor constructor : RecordUser.class.getDeclaredConstructors()) {
            System.out.println("constructor: " + constructor.getName());
        }
    }

//result
Component: userName
Component: lastLogin
constructor: com.functional_java.chapter5.RecordUser

 

레코드의 누락된 기능 보완

1) 추가적인 상태

 

메서드 추가로 기존 컴포넌트 기반으로 한 파생 상태를 만들 수 있다.

public record RecordUser(@NonNull String userName, LocalDateTime lastLogin) {

	//상태
	public boolean hasLoggedInAtLeastOnce() {
		return this.lastLogin != null;
	}
}

 

2) 상속

 

레코드는 이미 java.lang.Record를 상속한 final 타입으로, 상속을 사용 할 수 없다. (하나 이상의 타입을 상속 불가능)

다만, 인터페이스를 통한 구현한 레코드 타입은 생성 가능하다.

다만 이렇게 만든 복잡한 계층 구조와 상호의존성은 원래 취지인 단순한 데이터 수집기로써의 역할에서 벗어나게 된다고 한다.

 

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

 

레코드는 모든 필드를 갖는 표준 생성자만을 가지고 있다.

따라서 합리적인 기본 값을 갖는 사용자 정의 생성자를 만들 수 있다.

 

public record Origin(int x, int y) {

	//기본 값에 대한 사용자 정의 생성자
	public Origin() {
		this(0, 0);
	}

	//다만 인수가 없는 생성자의 경우 상수를 활용하는 것 이 적절하다.
	public static Origin ZERO = new Origin(0, 0);
}

 

public record Rectangle(Origin origin, int width, int height) {

	//다른 레코드와 조합하고 추가 생성자 활용
	public Rectangle(int x, int y, int width, int height) {
		this(new Origin(x, y), width, height);
	}

	public Rectangle(int width, int height) {
		this(new Origin(), width, height);
	}

	// //네이밍 규칙에 따라 두 조합을 한번에 사용은 불가능 하다. 정적 메서드 사용

	// public Rectangle(int x, int width, int height) {
	// 	this(new Origin(x, 0), width, height);
	// }
	// public Rectangle(int y, int width, int height) {
	// 	this(new Origin(0, y), width, height);
	// }

	public static Rectangle atX(int x, int width, int height) {
		return new Rectangle(x, 0, width, height);
	}

	public static Rectangle atY(int y, int width, int height) {
		return new Rectangle(0, y, width, height);
	}

}

 

기본 생성자 정의나 정적 메서드 정의를 통해 여러 조합의 레코드를 생성 할 수 있다.

 

4) 단계별 생성 (Builder)

 

불변 자료 구조에는 객체의 필드 중 일부가 할당되지 않은 경우가 있을 수 없다.

다만 모든 자료 구조가 동시에 초기화 되는 것은 아니기 때문에, 빌더 패턴을 활용해 변경 가능한 중간 단계의 변수를 사용하고, 최종적으로 불변한 결과를 생성 할 수 있다.

 

public record UserWithBuilder(String userName, boolean isActive, LocalDateTime lastLogin) {

	public static final class Builder {
		private final String userName;
		//생성 중 변경될 수 있는 필드
		private boolean active;
		private LocalDateTime lastLogin;

		public Builder(String userName) {
			this.userName = userName;
			this.active = true;
		}

		public Builder active(boolean isActive) {
			if (this.active == false) {
				throw new IllegalArgumentException();
			}

			this.active = isActive;
			return this;
		}

		public Builder lastLogin(LocalDateTime lastLogin) {
			this.lastLogin = lastLogin;
			return this;
		}

		//최종 불변 객체 return
		public UserWithBuilder build() {
			return new UserWithBuilder(this.userName, this.active, this.lastLogin);
		}

	}

}
UserWithBuilder user = new UserWithBuilder.Builder("haneul")
			.active(false)
			.lastLogin(LocalDateTime.now())
			.build();

 

빌더 메서드에 유효성 검사 메서드도 포함 시킬 수 있으며,

build() 메서드를 통해 실제 불변인 레코드가 생성된다.

 

정적 중첩 클래스로 직접 배치 할 수 있다.

 

혹은 오픈소스 라이브러리 RecordBuilder를 통해 간단하게 빌더패턴을 구현 할 수 있다.

 

<reference>

https://openjdk.org/jeps/395

 

JEP 395: Records

JEP 395: Records OwnerGavin BiermanTypeFeatureScopeSEStatusClosed / DeliveredRelease16Componentspecification / languageDiscussionamber dash dev at openjdk dot java dot netRelates toJEP 359: Records (Preview)JEP 384: Records (Second Preview)Reviewed

openjdk.org

https://github.com/Randgalt/record-builder

 

GitHub - Randgalt/record-builder: Record builder generator for Java records

Record builder generator for Java records. Contribute to Randgalt/record-builder development by creating an account on GitHub.

github.com