# Java 16 Record 💿: 10분 안에 익히는 레코드 레시피 적극 활용

> 🇰🇷 Kor: 지금 보는 중!  
> 🗺 Eng: Not yet.  
> 🇯🇵 Jap: まだです。⚠️

---

# 💿 What is the Record?

💿 **레코드**는 **<mark>불변 객체 생성을 위한 클래스 유형</mark>입니다.**  
💿 쉽게 말해 모든 필드가 `final` 필드가 되죠.

![만화풍으로 된 이 그림에서는 왼쪽 남자가 엘피 레코드판을 하나 들고 "다시 녹음할 수 있을까요?"라고 물어보고 있다. 오른쪽에 있는 썬글라스를 쓰고 콧수염이 있는 남자는 "새로운 레코드판으로는요."라며 설명하고 있고, 그의 앞에는 "캐낫 모더파이" 즉 "변경할 수 없음."이라는 글씨가 써 있다.](https://cdn.hashnode.com/res/hashnode/image/upload/v1699473048990/606cc04b-3fae-42ec-8712-433cc1602cac.png align="center")

`record`는 비교적 높은 버전에서 공개된 기능인 만큼 꽤 쉽습니다.  
이러저러한 보일러 플레이트를 작성할 필요도 없구요. 코드를 보죠.

```java
// @AllArgsConstructor가 자동으로 생성됩니다. (모든 필드를 인자로 갖는 생성자)
// Getter가 각 필드에 붙게 됩니다.
public record MyRecord(String name, Integer age) {}
// All fields are `final`
// @Override ... toString() automatically.
// @Override ... hashCode() and equals() automatically.

// ⚠️ No builder automatically.
```

눈에 띄는 것은 소괄호에 필드를 작성하고 있다는 것입니다. 레코드 선언은 마치 **생성자를 작성**하는 듯한 생김새를 띠고 있고, 실제로 동일한 **<mark>생성자가 생성</mark>되죠.**

이를 포함해 레코드 클래스는 다음 특징을 갖습니다.

* 선언한 **<mark>모든 필드에 대한 인자를 갖고 있는 생성자</mark>**(전체 인자 생성자)가 존재합니다.
    
* `record`의 **<mark>모든 필드는</mark>** `final`**<mark>로 선언</mark>됩니다.** (가장 중요한 특징)
    
* 모든 필드에 대해 `getter`가 생성됩니다. (네이밍 컨벤션 ex: `name()`, `age()`)
    
* `toString()`, `equals()`, `hashCode()` 등도 적절히 생성됩니다.
    

그간 자바 진영의 유명한 라이브러리인 `lombok`을 통해 이 작업들이 쉽게 수행되었습니다. `record` 클래스에서는 그중 대다수를 생략할 수 있습니다.

## Usage of Record

💿 **생성**

```java
MyRecord record = new MyRecord("John", 21);
```

특이한 것은 자바가 하위호환에 대한 의식 때문인지 `record`를 일부에서 식별자로 사용할 수 있도록 한다는 것입니다. 즉 변수명을 `record`로 사용할 수도 있죠. 🤔 그러나 자바의 버전이 빠르게 오르고 있고, 이러한 허용에 대해 (비록 오랫동안 보장될 것 같더라도) 맹신을 삼가고 싶기 때문에 `record`라는 변수명을 일부러 사용하는 것은 삼가면 좋겠습니다.

```java
MyRecord myRecord = new MyRecord("John", 21);
```

💿 **Getter**

적용된 네이밍 컨벤션

* `getFieldName()` (X)
    
* `fieldName()` (O)
    

불변 객체이기 때문에 `setter`는 없습니다.

```java
String name = myRecord.name(); // returns "John"
```

💿 **toString**

`toString()`, `hashCode()`, `equals()` 등도 자동으로 적절히 생성되어 있습니다.

```java
System.out.println(myRecord); // MyRecord[name=John, age=21]
```

## 💿 콤팩트 생성자

레코드 클래스에는 소괄호가 없는 특별한 생성자가 있습니다. 사실 독립적인 일반 생성자는 아닙니다. <mark>생성자 </mark> **<mark>파라미터</mark>**<mark>를 검증하고 수정</mark>할 수 있는 **<mark>'콤팩트 생성자'</mark>라는 보조 생성자**입니다.

```java
public record MyRecord(String name) {
    // Compact Constructor
    public MyRecord {
        Objects.requireNonNull(name); // Check arguments
        name = name.strip(); // Preprocess arguments
    }
}
```

위 콤팩트 생성자에서 취급한 변수 `name`은 필드 `name`이 아니라 생성자 매개변수 `name`입니다. 무슨 말이냐 하면, **필드** `name`**은** `final`**이기 때문에 여기에서 여러 차례 대입을 할 수 없는 반면, <mark>매개변수 </mark>** `name`**<mark>은 가변성을 띠기 때문에 몇 차례 전처리에 사용</mark>할 수 있다**는 말이죠.

<div data-node-type="callout">
<div data-node-type="callout-emoji">🏗</div>
<div data-node-type="callout-text"><strong>콤팩트 생성자</strong>(검증 및 전처리) → <strong>전체 인자 생성자</strong>(필드에 대입) → 🚀 인스턴스 생성 완료!</div>
</div>

여기에서 전처리된 데이터(`name`)는 전체 인자 생성자로 전달되어, 동일한 이름의 필드에 대입됩니다. 관례적으로 수행하던 `this.name = name;` 등 동작은 작성할 필요가 없기 때문에 콤팩트한 생성자입니다.

## 💿 생성자! 특별한 규칙 양상추 치즈 피클 양파까지

> 💿 모든 레코드 객체는 <mark>'전체 인자 생성자(All Arguments Constructor)'</mark>에 의해 생성되어야 합니다. 💿

레코드에서 객체의 생성은 반드시 전체 인자 생성자로 수행하게 됩니다. 그러나 <mark>다른 생성자를 만들 수 없다는 뜻은 아닙니다</mark>. 다른 생성자를 사용할 때는 수행문의 첫 줄에서 `this(...)` 생성자를 통해 전체 인자 생성자를 호출해야 합니다. (특이한 구성을 갖는 경우 전체 인자 생성자가 아닌 제3의 생성자를 호출할 수도 있는데, 결국엔 어느 단계에서든 전체 인자 생성자를 호출합니다.)

### 🏗 추가적인 생성자

예를 들어 `Map`을 받아서 그중 `name` 속성과 `age` 속성을 레코드로 빚어낸다고 해 보겠습니다.

```java
import java.util.Map;

public record MyRecord(String name, Integer age) {

    // 추가적인 생성자
    public MyRecord(Map<String, Object> map) {
        this((String) map.get("name"), (Integer) map.get("age"));
        // `this(String, Integer);` instead of `super();`
    }
}
```

이처럼 추가적인 생성자에서는 실행문의 첫 문장으로 전체 인자 생성자인 `this(...);` 생성자를 거쳐 객체를 생성합니다.

마치 상속 관계에서 부모 클래스에서 자식 클래스 순으로 객체를 생성해 가기 위해서 각 생성자의 첫 줄에 `super(...)` 생성자(또는 다른 `this(...)` 생성자)를 호출하듯, 생성자 호출에 대한 제약으로 볼 수 있습니다. 자바는 객체 생성 순서에 대한 제약이 다른 언어에 비해 강력한 편이기 때문에, 다른 실행문보다 `this(...);`나 `super(...);` 생성자 호출이 선행되어야 합니다.

**자바 객체 생성 순서** (부모 클래스부터 자식 클래스 순으로)

![수퍼클래스의 생성자에서 서브클래스의 생성자를 거쳐 하나의 인스턴스가 된다는 도식화. 그냥 화살표로 방향 표시를 해 놨다. 화살표 끝에는 사람이자 학생인 하나의 인스턴스라고 쓰여 있다. 그림 오른쪽에는 스튜던트 클래스의 생성자에서 첫 줄에 수퍼 생성자를 호출하여 펄슨 클래스의 생성자를 호출하고 있다는 것을 코드와 화살표로 표현했다.](https://cdn.hashnode.com/res/hashnode/image/upload/v1699526978526/8d6b4948-95ab-44b2-aa83-700cdca06602.png align="center")

위 흐름은 `new Student();`를 수행했을 때 예시입니다. 물론 저 위로 계속 부모 클래스의 계보를 타게 되니 `Object` 클래스부터 마지막 클래스까지 이처럼 직렬적으로 연결되겠죠.

**레코드 클래스의 생성자 동작 순서** (호출 순서와 다름.)

![콤팩트 생성자, 전체 인자 생성자, 최초 호출된 생성자 순으로 동작한다는 것을 도식화하였다. 오른쪽에는 각 단계에 대한 코드를 써 두었다. 콤팩트 생성자에서는 오브젝트 쩜 리콰이어스논널 괄호열고 네임 괄호닫고 세미콜론. 그 다음 줄에 네임 은 네임 쩜 스트립 괄호열고 괄호닫고 세미콜론. 전체 인자 생성자에서는 특별히 추가된 코드가 없다고 쓰여 있고, 엔트리 포인트로서의 생성자에서 디스 괄호열고 또 괄호 열고 스트링 괄호 닫고 맵 쩜 겟 괄호열고 큰따옴표열고 네임 큰따옴표닫고 괄호닫고 또 괄호 닫고 세미콜론.](https://cdn.hashnode.com/res/hashnode/image/upload/v1699555521798/b4c86cd1-e746-4358-820b-9c6cb0c1707a.png align="center")

한편 레코드 클래스의 생성자 동작은 이런 순서를 갖습니다. 위에는 영어니까 한궁말로 봐 보죠.

1. **<mark>콤팩트 생성자</mark>에서** 파라미터를 체크·수정합니다.
    
2. 그대로 **<mark>전체 인자 생성자</mark>로** 연결되어 필드에 값을 담습니다. 이때 모든 필드를 채웁니다.
    
3. 여기까지 `this(...)` 호출에 의한 동작이며, 이후 최초 호출되었던 생성자에서 **남은 작업**을 수행합니다. 남은 작업은 필드를 채우는 것과는 무관할 것입니다. (`this.field = value;` 동작은 이미 전체 인자 생성자가 수행했고, 모든 필드가 `final`이기 때문에 이제 값을 변경할 수 없음.)
    

그래서 실질적으로 **<mark>인스턴스</mark>**<mark>는 </mark> **<mark>전체 인자 생성자 때 이미 완성</mark>**<mark>된 셈</mark>이죠.

유념할 것은 `this(...)` 생성자가 첫 줄에 와야 하기 때문에, `this(...);` 생성자에 넘겨 줄 데이터 처리가 필요하더라도 `this(...);`의 소괄호 내에서 모든 처리를 마쳐야 한다는 것입니다. **만약 복잡한 처리가 필요하다면 함수의 사용을 고려해 보세요.**

---

# 🛠 어떻게 커스텀할까요? My Convention

용례마다 다르기 때문에 절대적인 관례로 삼을 수 없어서 '일반적인 경우'에 대한 개인적 컨벤션을 소개하려고 합니다.

## 🔧 Builder는 쓰는 게 신상에 좋습니다

🤷‍♀️🤔 레코드는 전체 인자 생성자(All Args Constructor)를 통해 생성되기 때문에, **<mark>나중에 필드를 추가하면</mark>** 전체 인자 생성자가 쓰이는 **모든 곳을 수정해야 할 수도** 있습니다. 기존에 쓰이던 생성자는 더 이상 없기 때문이죠.

🚫 아니면 기존에 쓰이던 생성자를 명시적으로 작성하고 `this(...);`로 새 전체 인자 생성자에게 값을 넘겨 인스턴스를 생성하도록 임시 방편을 둘 수도 있겠죠. 이렇게 필드가 또 추가되면 생성자를 또 추가하고, 필드가 또 추가되면 생성자를 또 추가하고 그렇게 젠가를 쌓아도 됩니다. 🤦‍♀️🤦‍♂️ 레코드로 편리함을 누리다 보니 종속성을 높인 것이죠.

```java
// 🚫 Not Suggested Pattern 🚫

public record EditedRecord(
        String field, // 원래 이것만 있었다가
        String newField, // 어느날 이게 추가되고
        String newNewField // 다른 날 이것도 추가됐다면
) {
    // old old constructor: 처음 레코드 때 사용되었던 생성자.
    public EditedRecord(String field) {
        this(field, null, null);
    }

    // old constructor: 얼마 전까지 전체 인자 생성자로 사용되었을 생성자
    public EditedRecord(String field, String newField) {
        this(field, newField, null);
    }

    // 나중에 필드를 또 늘린다면 이제 세 필드에 대한 생성자도 작성해야 하겠죠. 🤦‍♀️🤦‍♂️
    // public EditedRecord(String, String, String) {}
}
```

✌️ 또는 이렇게 빌더를 다는 습관을 써도 됩니다.

```java
import lombok.Builder;

@Builder
public record SignUpResponseDto(String token) {}
```

이러면 나중에 필드를 추가해도 멀쩡합니다! 👍

이렇게만 하면 추가된 필드는 모두 `null`로 담기겠죠. (전체 인자 생성자는 빌더가 알아서 사용하니까요.)

```java
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Builder;

@Builder
public record SignUpResponseDto(
        @JsonProperty("access_token")
        @JsonInclude(Include.NON_EMPTY)
        String token,
        
        @JsonInclude(Include.NON_DEFAULT)
        Boolean shouldPass2Fa
) {}
```

기존에 이 레코드를 사용하고 있던 곳에서 빌더를 사용했다면, 추가된 필드를 무시할 수 있습니다.

```java
// 👍✨ 새 필드를 추가해도 기존 코드를 반드시 고칠 필요는 없습니다.
return SignUpResponseDto.builder()
        .token("THIS IS AN EXAMPLE TOKEN")
        .build();

// 나중에 추가된 필드 shouldPass2Fa는 이때 `null`로 처리되죠.
```

### 🏗✨ 빌더 + 콤팩트 생성자로 기본값 주기

원한다면 **콤팩트 생성자를 통해 기본값을 할당**할 수 있습니다. 콤팩트 생성자의 주요 활용 중 하나입니다.

```java
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;

@Builder
public record SignUpResponseDto(
        @JsonProperty("access_token")
        @JsonInclude(Include.NON_EMPTY)
        String token,

        @JsonInclude(Include.NON_DEFAULT)
        Boolean shouldPass2Fa
) {
    public SignUpResponseDto { // 콤팩트 생성자를 통해 기본값 추가
        if (shouldPass2Fa == null) {
            shouldPass2Fa = false; // 원한다면 이런 대입을 추가할 수 있습니다.
        }
    }
}
```

## 🧐 한국인 바쁜 사수 특 "모르겠으면 이렇게 하세요.": 모르겠으면 래퍼 클래스 쓰세요!

우리는 참조 타입이 필요하거나, `nullable` 취급이 필요할 때 래퍼 클래스를 사용할 수 있죠. 예를 들어 `Nullable`이 필요하면 `Integer` 타입을 사용하고, `Not null`이면 `int` 타입을 사용할 수도 있어요.

🙋‍♀️ 그런데 너무 강한 제약으로 나중에 작업이 불편할 일이 없도록! **타입은 항상** `Integer`**로 정하고,** `null` **여부 등 제약은 콤팩트 생성자에서만 관리하는 건 어떨까요?**

```java
public recrod MyRecord(String name, Integer age) {
    public MyRecord {
        Objects.requireNonNull(name);
        Objects.requireNonNull(age);
    }
}
```

나중에 이 필드들에 대한 제약을 변경해서 `nullable`로 취급하거나, 아니면 기본값을 부여하기로 정책을 추가했다면, **기존에 기본 타입보다는 <mark>래퍼 클래스</mark>를 사용해 두었을 때 <mark>제약을 훨씬 편하게 수정</mark>할 수 있을 겁니다.**

```java
public recrod MyRecord(String name, Integer age) {
    public MyRecord {
        Objects.requireNonNull(age);

        if (name == null || name.isBlank()) {
            name = "noname";
        }

        name = name.strip();
    }
}
```

제약 조건 관리 외에도 참조 타입으로 관리하는 이점이 더 있습니다. 다음은 몇 가지 예시입니다.

* 데이터를 JSON 문자열로 직렬화할 때 일부 데이터가 JSON 문자열에서 자동으로 제외되도록, 애노테이션이나 메시지 포매터를 통해 작업해 둔다면 **기본 타입보다는 참조 타입이 더 많은 기술과 호환되며 관리하기 편합니다**.  
    (예를 들어 `gson`은 기본적으로 `null` 데이터를 직렬화에서 제외합니다. `jackson`은 `null`이거나 기본값이면 제외하는 옵션을 추가할 수 있습니다. 두 기술 중 무엇을 쓰더라도 `null`일 때 제외할 수 있으니 `null`로 제어할 가능성을 열어 두는 참조 타입이 호환성에서 낫겠죠.)
    
* 반대로 역직렬화를 하거나 또는 빌더 등 추가적인 수단을 통해 생성할 때도 기본 타입의 기본값(`0`, `0.0`, `false`, `'\0'` 등)이 아닌 다른 기본값을 원한다면, 파라미터로 전달된 값이 있는지 없는지 `null`로 구분하고, `null`일 때 원하는 기본값을 대입하는 것이 좋습니다. (예를 들어, `null`일 때 `Integer` 데이터의 기본값을 `-1`로 하기를 원할 수 있습니다.)
    

반면 어떤 분들은 더욱 강한 제약을 선호해서 기본 타입을 사용하려고 할 수도 있습니다. 베스트 프랙티스는 항상 관점에 따라 바뀔 수 있습니다.

---

# 💿 Record 이럴 때 쓰세요!

* **DTO**
    
    요즘은 데이터 전달 구간에서 일시적으로 객체를 생성한 후 전달을 완료하면 역할을 다하도록 구성하곤 합니다. 이때 불변 객체는 전달 과정에서 변경이 없기 때문에 데이터의 안정성과 신뢰도가 높습니다. 예를 들어 여러 스레드에 여러 작업으로 전달될 때 이 객체의 어떤 필드도 변경될 리 없다고 믿을 수 있죠.
    
* **Properties Bean**
    
    우리는 `@Value(...)`의 대체재로 `@ConfigurationProperties(...)`를 적극적으로 사용할 수 있습니다. 레코드에 대한 바인딩을 지원하고 있기 때문에 레코드로 작성하면 편리합니다. 주로 `*.yml`, `*.properties` 파일로 작성하는 구성 속성 파일에서 케밥 케이스(`kebab-case`)로 작성하고 레코드의 필드에는 카멜 케이스(`camelCase`)로 작성하면 자동으로 바인딩됩니다.
    
* **Projection**
    
    우리는 Spring Data JPA를 사용할 때도 필요한 컬럼만 골라서 조회하고 싶을 수 있습니다. 레코드 이전에는 이때 프로젝션이라는, 테이블의 일부 컬럼을 떼어 놓은 개념을 적용하기 위해서 인터페이스를 사용하여 하나하나 `getter`로 작성하여야 했습니다. 레코드에서는 그럴 필요 없이 컬럼 이름만 카멜 케이스로 작성해 주면 됩니다. 타입만 맞추구요.
    

## ❌ 이럴 때 삼가세요!

JPA 엔티티 등 Persistence Entity로는 부적합합니다. 일반적으로 프록시를 생성하여 동작할 때 알맞지 않습니다. 불변성 때문에 프록시에서의 핸들링에 제약이 발생하기 때문이죠.

---

자료는 모두 DALL·E, excalidraw, Power Point 등을 통해 스스로 만들었습니다. (그래서 영어가 어색할 수 있습니다.) 어색한 영어 문장은 남대문 열렸다고 알려 주듯 살짝 말씀해 주시면 얼른 고쳐 보겠습니다. 그러면 다들 모쪼록 착한 개발 하세요.
