티스토리 뷰
[Java] 자바 언어의 명시적 타이핑(Nominal Typing) 개발 철학과 Record 클래스 도입에 대하여
망나니개발자 2025. 8. 26. 10:00
1. Record 클래스의 개념과 도입 배경
[ 개발 트렌드의 변화 ]
21세기는 두말 할 필요 없이 데이터가 중심인 세상이다. 많은 데이터들이 기록되고 수치화되며, 데이터를 분석하여 얻은 결과는 수 많은 의사결정의 근거가 된다. 또한 MSA의 시대가 도래함에 따라 데이터의 교환이 점점 더 중요해지고 있다. 자바는 객체 지향 프로그래밍(OOP, Object-Oriented Programming) 언어로서, 객체를 중심으로 설계되고 개발되어 왔다. 그러나 최근 몇 년간 프로그래밍 패러다임이 변화하면서 OOP의 한계가 드러나고 있다. 특히 과거에는 장점이라고 평가받았던 객체지향 프로그래밍의 복잡한 엔티티(Entity)나 바운더리(Boundary) 등이 점점 덜 중요해지고, 순수한 데이터의 교환이 더욱 중요해지는 시대가 되었다. 실제로 자바 언어 개발진들은 힘의 균형이 이동하고 있으며, OOP의 강점이였던 복잡한 엔티티나 바운더리 등이 덜 중요해지고 OOP의 약점이었던 순수한 데이터와 같은 것들이 더욱 중요해진 시대라고 얘기했다. 이를 위해 비즈니스 엔티티와 처리를 모델링하기 위해 클래스를 사용하는 것에서 데이터를 모델링하기 위해 클래스를 사용하도록 변화중(Data Oriented Programming)인 상황이라고 한다. 자바 언어의 개발자들 역시 이러한 트렌드에 발맞추어 여러 변화들을 시도하고 있는데, 해당 프로젝트가 바로 프로젝트 엠버(Project Amber)이다. 프로젝트 엠버는 자바 언어의 개발 생산성을 높이기 위한 다양한 기능들을 도입하고 있으며, 그 중 하나가 Record 클래스이다.
[ Record 클래스란? ]
Record 클래스는 자바 14에서 미리보기 기능(preview feature)으로 도입되었고, Java 16에서 정식으로 추가된 새로운 클래스 유형이다. Record 클래스는 데이터 모델링을 간소화하고, 불변 객체(Immutable Object)를 쉽게 생성할 수 있도록 설계되었으며, 주로 데이터 전송 객체(Data Transfer Object, DTO)나 값 객체(Value Object)를 표현하는 데 사용된다. 이러한 Record 클래스는 다음과 같은 특징을 가지고 있다.
- 불변성(Immutable): Record 클래스의 인스턴스는 생성 후 변경할 수 없다. 즉, 필드 값이 한 번 설정되면 변경할 수 없다.
- 자동 생성된 메서드: Record 클래스는 equals(), hashCode(), toString() 메서드를 자동으로 생성한다.
- 간결한 문법: Record 클래스는 간결한 문법을 제공하여, 데이터 모델링을 더욱 쉽게 할 수 있다. 필드 선언과 생성자를 한 줄로 작성할 수 있으며, getter 메서드도 자동으로 생성된다.
- 명시적 타입(Nominal Typing): Record 클래스는 명시적 타입(Nominal Typing)을 지원한다. 즉, Record 클래스의 인스턴스는 해당 Record 클래스의 타입으로만 사용될 수 있으며, 다른 Record 클래스의 인스턴스와는 호환되지 않는다.
Record 클래스는 다음과 같이 활용할 수 있으며, 원하면 일반 클래스처럼 생성자와 메서드를 명시적으로 정의할 수도 있다.
public record Person(
String name,
int age
) {
public void getFullInformation() {
return "Name: " + name + ", Age: " + age;
}
}
Record 클래스를 사용하면, 데이터 모델링을 간소화하고, 불변 객체를 쉽게 생성할 수 있으며, 코드의 가독성을 높일 수 있기에 사용이 권장된다.
[ Record 클래스의 도입 과정 ]
데이터 교환과 관련된 자바 언어의 개선 작업을 위해, 자바 언어 개발지들은 여러 가지 선택지들을 고려하였다.
- 구조적 튜플(Structural Tuple)
- Lombok과 같은 코드 생성기
- 명시적 튜플(Nominal Tuple)
구조적 튜플은 여러 값을 하나의 객체로 묶는 방법으로, 자바 언어에서 제공하는 기능이 아니다. 이는 간단한 데이터 집합을 표현하는 데 유용하지만, 타입 안전성을 보장하지 못하고, 코드의 가독성을 떨어뜨리는 단점이 있다.
// 구조적 튜플 예시, 자바 언어에서는 지원하지 않음
val person = ("망나니개발자", "Server")
Lombok과 같은 코드 생성기는 자바 언어의 문법을 확장하여, 코드의 양을 줄이고 가독성을 높이는 데 도움을 준다. 그러나 Lombok은 외부 라이브러리이므로, 프로젝트에 의존성을 추가해야 하며, 모든 개발자가 Lombok을 사용하지 않을 수도 있다.
@Getter
@RequiredArgsConstructor
public class Person {
private final String name;
private final String role;
}
자바 언어 개발자들이 최종적으로 선택한 것은 Record 클래스로, 이는 튜플의 명명된(nominal) 형태로 볼 수 있다. 이러한 선택의 이면에는 자바 언어의 설계 철학과 명시적 타이핑(Nominal Typing)에 대한 깊은 이해가 있다.
2. 자바 언어의 명시적 타이핑(Nominal Typing) 개발 철학과 Record 클래스 도입 배경
[ 자바 언어와 명시적 타이핑(Nominal Typing) ]
명시적 타이핑(Nominal Typing)은 타입의 이름(name)을 기준으로 타입의 동일성(equality)이나 호환성(compatibility)을 판단하는 타입 시스템이다. 명시적 타이핑 언어에서 객체의 타입은 해당 클래스의 이름에 의해 결정되며, 객체의 타입이 클래스의 구조나 속성에 의존하지 않는다. 예를 들어 다음의 두 클래스가 있다고 할 때, 두 클래스는 서로 다른 이름을 가지고 있으므로, 서로 다른 타입으로 간주된다.
public class A {
private String name;
}
public class B {
private String name;
}
Java 설계 철학의 핵심 중 하나는 “이름은 중요하다”는 것이다. 클래스와 그 구성 요소들은 의미 있는 이름을 가지지만, 튜플과 그 구성 요소는 그렇지 않다. 자바는 클래스의 이름을 바탕으로 의미 있는 데이터 구조를 만든다는 철학을 통해 코드의 가독성을 높이고, 유지보수를 용이하게 하며, 타입 안전성을 보장하고자 하였다. 따라서 자바 언어 개발자들의 입장에서 구조적 튜플은 자바 언어에 적합하지 않다고 판단했다. 그렇다면 자바 언어에서 일반 클래스가 아닌 별도의 레코드 타입을 추가한 이유는 무엇일까? 자바 언어에서 일반 클래스는 상태(state)와 동작(behavior)을 모두 포함할 수 있다. 또한 일반 클래스는 생성자를 통해 상태에 대한 유효성 검사를 할 수 있으며, 상태와 동작을 같은 곳에 모아둘 수 있다. 그리고 제공되는 상태가 가변적일 수 있다. 하지만 이번의 케이스는 주로 상태를 표현하는 것이며, 동작을 포함할 필요가 없었다. 또한 외부로 값을 주고받는 과정에서 주로 사용되기에 값이 가변일 필요도 없었다. 그저 간단한 데이터 구조를 표현하는 도구가 필요했다. 따라서 자바는 명시적 타이핑(Nominal Typing)을 지원하는 Record 클래스를 도입하여, 데이터 모델링을 간소화하고, 불변 객체를 쉽게 생성할 수 있도록 하였다. 이를 통해 자바 언어 사용자들의 생산성을 높일 수 있었을 뿐만 아니라, 타입 안전성을 높이고 코드의 가독성을 확보할 수 있었다.
[ 함수형 인터페이스 도입 과정에서의 명시적 타이핑(Nominal Typing) ]
자바 언어 개발자들은 Record 클래스를 도입하기 전에도 명시적 타이핑(Nominal Typing)을 바탕으로 신규 기능을 추가한 적이 많다. 가장 대표적인 사례는 바로 함수형 인터페이스(Functional Interface)이다. 자바는 함수형 프로그래밍을 지원하기 위해, 자바 8에서 람다 표현식(Lambda Expression)을 도입하면서 함수형 인터페이스 를 추가하였다. 함수형 인터페이스는 단 하나의 추상 메서드만을 가지는 인터페이스로, 람다 표현식을 사용하여 해당 인터페이스의 인스턴스를 생성할 수 있다. 자바 언어가 함수형 인터페이스를 도입할 때에도 구조적 함수 타입 또는 명시적 타입 함수 타입 중 하나를 선택해야 하는 순간이 있었다. 구조적 함수 타입(Structural Function Type)은 함수의 시그니처(signature)를 기반으로 타입을 결정한다. 따라서 만약 자바가 구조적 함수 타입을 채택했었다면, 인자와 반환값의 형태가 같으면 같은 타입으로 간주되었을 것이다. 예를 들어 다음의 두 가지 인터페이스는 동일한 시그니처를 가지고 있으므로, 같은 타입으로 간주되었을 것이다.
public interface Function1 {
String apply(String input);
}
public interface Function2 {
String apply(String input);
}
이렇게 되었다면 유연하고, 빠른 개발에 유리했겠지만, 타입에 의미를 부여하기 어렵고 코드의 가독성이 낮아지는 등의 문제가 발생할 수 있었다. 하지만 자바는 명시적 함수 타이핑(Nominal Function Type)을 통해 인터페이스의 이름을 기반으로 타입을 결정하도록 하였다. 따라서 이름이 같은 타입만 호환되는 것이다.
자바 언어는 Record 클래스의 도입을 통해 boiler plate 코드를 줄일 수 있다는 부수적인 장점을 얻을 수 있었지만, 핵심 목표는 사람들에게 이름을 기반으로 데이터를 모델링할 수 있는 깔끔한 semantic way를 제공하는 것이었다.
이러한 개발 철학은 다형성을 구현할 때에도 유사했는데, 이후 포스팅에서 관련 부분도 자세히 살펴보도록 하자.
참고 자료