Java 14 이전에는 보일러플레이트 필드와 메서드를 가진 클래스를 작성해야 했으며, 이는 사소한 실수와 혼란스러운 의도를 초래할 수 있었습니다.
Java 14가 출시되면서, 이제 이러한 문제를 해결하기 위해 레코드를 사용할 수 있게 되었습니다.
보일러플레이트 : 최소한의 변경으로 여러 곳에 재사용되며 반복적으로 비슷한 형태를 띄는 코드, 강철로 찍어내는 것처럼 최소한의 수정으로 여러 곳에서 자주 반복되는 코드
목적
데이터베이스 결과, 쿼리 결과 또는 서비스에서 받은 정보를 단순히 저장하기 위해 클래스를 작성하는데, 이 때 대부분의 경우에 불변 객체를 사용한다.
불변 객체를 사용하게 됨으로써 내부 필드 값의 불변성을 보장해 주어서 유지보수에서 많은 이점을 챙길 수 있다.
이를 달성하기 위해 다음과 같은 데이터 클래스를 만든다.
- 각 데이터 항목에 대한 private, final 필드
- 각 필드에 대한 getter
- 각 필드에 해당하는 인수를 가진 public 생성자
- 모든 필드가 일치할 때 동일한 클래스의 객체에 대해 true를 반환하는 equals 메서드
- 모든 필드가 일치할 때 동일한 값을 반환하는 hashCode 메서드
- 클래스 이름과 각 필드 이름 및 해당 값을 포함하는 toString 메서드
e.g) 이름과 주소를 가진 간단한 Person 데이터 클래스
public class Person {
private final String name;
private final String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
@Override
public int hashCode() {
return Objects.hash(name, address);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof Person)) {
return false;
} else {
Person other = (Person) obj;
return Objects.equals(name, other.name)
&& Objects.equals(address, other.address);
}
}
@Override
public String toString() {
return "Person [name=" + name + ", address=" + address + "]";
}
// standard getters
}
문제점
1. 많은 보일러플레이트 코드가 필요하다.
각 데이터 클래스에 대해 동일한 과정을 반복해야 한다. (데이터 항목에 대해 새 필드를 만들고, equals, hashCode, toString 메서드를 생성하며, 각 필드를 수락하는 생성자를 만들어야 한다.)
IDE가 이러한 클래스들을 자동으로 생성할 수 있지만, 새 필드를 추가할 때 자동으로 클래스를 업데이트하지는 않는다. 예를 들어, 새 필드를 추가하면 equals 메서드를 업데이트하여 이 필드를 포함해야 한다.
2. 이름과 주소를 가진 사람을 나타내기 위한 클래스의 목적이 흐려진다.
추가된 코드는 우리의 클래스가 단순히 두 개의 String 필드, 이름과 주소를 가진 데이터 클래스라는 것을 흐리게 한다.
더 나은 접근 방식은 우리의 클래스가 데이터 클래스임을 명시적으로 선언하는 것이다.
기본 사항
JDK 14부터 우리는 반복적인 데이터 클래스를 레코드로 대체할 수 있다. 레코드는 필드의 타입과 이름만 필요로 하는 불변 데이터 클래스이다.
Java 컴파일러는 equals, hashCode, toString 메서드, private, final 필드, public 생성자를 자동으로 생성한다.
Person 레코드를 생성하려면 record 키워드를 사용한다:
public record Person (String name, String address) {}
- 선언 동시에 내부에서 사용할 필드값들을 정의해주어야 한다.
Constructor
레코드를 사용하면 각 필드에 대한 인수를 가진 public 생성자가 자동으로 생성된다.
Person 레코드의 경우, 해당 생성자는 다음과 같다:
public Person(String name, String address) {
this.name = name;
this.address = address;
}
이 생성자는 클래스와 동일한 방식으로 객체를 인스턴스화하는 데 사용할 수 있다:
Person person = new Person("John Doe", "100 Linda Ln.");
Getters
필드 이름과 일치하는 public getter method를 자동으로 생성한다.
Person 레코드에서는 name()과 address() 게터가 포함된다.
@Test
public void givenValidNameAndAddress_whenGetNameAndAddress_thenExpectedValuesReturned() {
String name = "John Doe";
String address = "100 Linda Ln.";
Person person = new Person(name, address);
assertEquals(name, person.name());
assertEquals(address, person.address());
}
- 일반 객체와 다른 점 : 내부 필드 값에 접근할 때 앞에 get이 붙지 않는다.
- record는 일반 객체의 문제점을 해결하기 위해 등장한 것이 아니기 때문에 일반 객체의 컨벤션을 따를 필요는 없다.
equals
equals 메서드도 자동으로 생성된다.
이 메서드는 제공된 객체가 동일한 타입이고 모든 필드의 값이 일치하면 true를 반환된다:
@Test
public void givenSameNameAndAddress_whenEquals_thenPersonsEqual() {
String name = "John Doe";
String address = "100 Linda Ln.";
Person person1 = new Person(name, address);
Person person2 = new Person(name, address);
assertTrue(person1.equals(person2));
}
hashCode
equals 메서드와 유사하게, 해당하는 hashCode 메서드도 자동으로 생성된다.
hashCode 메서드는 두 Person 객체의 모든 필드 값이 일치하면 동일한 값을 반환한다 (생일 역설에 의한 충돌은 제외):
@Test
public void givenSameNameAndAddress_whenHashCode_thenPersonsEqual() {
String name = "John Doe";
String address = "100 Linda Ln.";
Person person1 = new Person(name, address);
Person person2 = new Person(name, address);
assertEquals(person1.hashCode(), person2.hashCode());
}
hashCode 값은 어떤 필드 값이라도 다르면 대부분의 경우 다를 것이다. 그러나 이는 hashCode() 계약에 의해 보장되지 않는다.
일반적인 경우, 객체의 필드 값이 다르면 hashCode 값도 다를 가능성이 높다. Java의 hashCode() 메서드 계약은 동일한 객체가 항상 동일한 hashCode 값을 가지도록 요구하지만, 서로 다른 객체가 동일한 hashCode 값을 가질 수 있다는 점을 허용한다. 이것은 두 객체가 같지 않더라도 해시 값이 같을 수 있다는 의미이다. 예를 들어, 충돌 방지를 위해 고도로 설계된 해시 함수라 하더라도, 해시 공간이 유한하기 때문에 충돌이 발생할 수 있다.
toString
레코드의 이름과 각 필드의 이름 및 해당 값을 대괄호 안에 포함하는 문자열을 생성하는 toString 메서드도 자동으로 생성된다.
따라서 “John Doe”라는 이름과 “100 Linda Ln.”이라는 주소를 가진 Person을 인스턴스화하면 다음과 같은 toString 결과가 나온다:
Person[name=John Doe, address=100 Linda Ln.]
Constructor
public constructor가 자동으로 생성되지만 constructor 구현을 사용자 정의할 수 있다.
이 사용자 정의는 검증을 위해 사용되며 가능한 한 간단하게 유지되어야한다.
e.g) 다음과 같은 생성자 구현을 사용하여 Person 레코드에 제공된 이름과 주소가 null이 아닌지 확인할 수 있다.
public record Person(String name, String address) {
public Person {
Objects.requireNonNull(name);
Objects.requireNonNull(address);
}
}
다른 인수를 사용하여 새로운 생성자를 만들 수도 있다.
public record Person(String name, String address) {
public Person(String name) {
this(name, "Unknown");
}
}
클래스 생성자와 마찬가지로 필드는 this 키워드를 사용하여 참조할 수 있으며(e.g this.name 및 this.address), 인수는 필드 이름과 일치한다(name 및 address).
생성된 공개 생성자와 동일한 인수를 가진 생성자를 만드는 것도 유효하지만, 이 경우 각 필드를 수동으로 초기화해야한다.
public record Person(String name, String address) {
public Person(String name, String address) {
this.name = name;
this.address = address;
}
}
또한, 간결한 생성자와 생성된 생성자와 인수 목록이 일치하는 생성자를 선언하면 컴파일 오류가 발생한다. 따라서 다음 코드는 컴파일되지 않는다.
public record Person(String name, String address) {
public Person {
Objects.requireNonNull(name);
Objects.requireNonNull(address);
}
public Person(String name, String address) {
this.name = name;
this.address = address;
}
}
Static Variables & Methods
일반 Java 클래스와 마찬가지로, 레코드에서도 정적 변수와 메서드를 포함할 수 있다.
정적 변수는 클래스와 동일한 구문을 사용하여 선언한다.
public record Person(String name, String address) {
public static String UNKNOWN_ADDRESS = "Unknown";
}
마찬가지로, 정적 메서드는 클래스와 동일한 구문을 사용하여 선언한다.
public record Person(String name, String address) {
public static Person unnamed(String address) {
return new Person("Unnamed", address);
}
}
그런 다음, 레코드 이름을 사용하여 정적 변수와 정적 메서드를 참조할 수 있다.
Person.UNKNOWN_ADDRESS
Person.unnamed("100 Linda Ln.");
Lombok 사용
간결하고 의존성이 없는 프로젝트를 만들고 싶다 => record
기능과 유연성을 챙기고 싶다. => Lombok
Record를 도메인 객체에 사용할 수 있을까
지양하는게 좋다.
레코드의 공식 문서를 살펴보면, 레코드는 불변 데이터를 전달하기 위한 캐리어라고 되어있다.
도메인 객체는 단순히 데이터를 갖고 있는 것뿐만 아니라 추가적인 비즈니스 로직이 들어가는 경우가 대부분이기 때문에 단순히 데이터만 전달하기 위해 사용되는 레코드의 목적성에는 부합하지 않는다.
따라서 레코드는 도메인 객체보다는 DTO에서 활용하는 것이 좋다.
참조
https://www.baeldung.com/java-record-keyword
잡담
Lombok, 도메인 객체 좀 자세히 알아보자
'Java' 카테고리의 다른 글
[Java/김영한] 조건문 (0) | 2024.08.12 |
---|---|
[Java/김영한] 연산자 (0) | 2024.08.12 |
[Java/김영한] 변수 (0) | 2024.08.11 |
[Java/김영한] Hello World (0) | 2024.08.11 |
Servlet (0) | 2024.05.13 |