✏️ [Java] record에 대하여

박상민·2024년 1월 19일
4

JAVA

목록 보기
3/3
post-thumbnail

최근 JPA를 사용해서 만든 게시판 프로젝트를 리팩토링 하면서 여러가지를 적용하고 있다.
그 중 큰 흥미와 도움이 됐던 record에 대해서 글을 써보려고한다.

record는 자바 14, 15에서 preview로 추가된 이후 16버전에서 정식 스펙이 됐다.

파이썬의 dictionary, C#의 record, kotlin의 data class처럼 특정 값 타입을 담는데 특화되었다는 record, 보면 볼수록 VO와 DTO로 쓰기 좋아 보인다.

📌 record 등장 배경

본격적으로 들어가기에 앞서 DTO를 생각해보자.
DTO를 구현하기 위해서는 getter, setter, equals, hashCode, toString 같은 데이터 처리 혹은 특정 연산을 수행하기 위해 오버라이드된 메소드를 반복해서 작성하게 된다.

public class Address {
    private String city;
    private String street;
    private String zipCode;

    public Address() {
    }
    
    public Address(String city, String street, String zipCode) {
        this.city = city;
        this.street = street;
        this.zipCode = zipCode;
    }

    public String getCity() {
        return city;
    }

    public String getStreet() {
        return street;
    }

    public String getZipCode() {
        return zipCode;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Address)) return false;
        Address address = (Address) o;
        return Objects.equals(city, address.city) &&
                Objects.equals(street, address.street) &&
                Objects.equals(zipCode, address.zipCode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(city, street, zipCode);
    }
}

address라는 주소에 해당하는 정보를 가지고 있는 단순한 DTO를 만든 코드이다. 굳이 자세히 보지 않아도 보일러 플레이트 코드가 불필요하게 큰 것을 알 수 있다.

보일러 플레이트 코드
최소한의 변경(인자, 혹은 결과 타입)으로 여러 곳에서 재사용 되면 반복적으로 비슷한 형태를 가지고 있는 코드
→ getter, setter, equals, hashCode, toString 등이 여기에 해당

일반적으로 이러한 보일러 플레이트 코드는 lombok 또는 kotlin(보다 정확하게는 data class) 사용해 중복으로 발생하는 코드의 양을 줄인다.

이렇게 비대해진 보일러 플레이트 코드는 자바가 가지고 있는 단점으로 작용한다. lombok이나 IDE의 도움을 받아 코드를 간결하게 만들 수 있지만, 이는 근본적으로 자바가 가지고 있는 한계를 해결하지 못한다.
자바가 가지고 있는 한계를 극복하기 위해 자바를 업그레이드하면서 다양한 기능들을 추가하게 됐는데 그 기능들 중 하나가 바로 record이다.

record의 목표

  • 객체 지향의 사상에 맞게 데이터를 간결하게 표현하기 위한 방법을 제공
  • 개발자가 동작을 확장하는 것보다 불변 데이터를 모델링하는데 집중하도록 함
  • 데이터 지향 메소드를 자동으로 구현
  • 단, java beans를 대체하기 위한 기술은 아님 + 어노테이션 지향적인 코드를 생성하기 위한 기능도 아님!
  • adress라는 단순한 주소값을 갖고 있는 녀석 하나 만드는데 보일러 플레이트 코드가 비대해지는 결과를 가짐

📌 record의 특징

  • record는 불변 객체로 abstract로 선언할 수 없으며 암시적으로 final로 선언된다.
    한 번 값이 정해지면 setter를 통해 값을 변경할 수 없으며 상속을 할 수 없다.

  • record 내 각 필드(헤더에 나열한 컴포넌트)는 private final로 정의된다.

  • 다른 클래스를 상속 받을 수 없지만, 인터페이스로는 구현이 가능하다. (extends : X, implements : O)

  • 레코드 내부에 멤버 변수(인스턴스 필드)를 선언할 수 없다. 그러나 static 변수는 생성이 가능하다. 이는 헤더에서 정의한 멤버만을 record에서 관리하기 위함이다.

  • 위의 4가지 주요 특징을 제외하고는 자바의 클래스 개발과 동일하게 사용할 수 있다.

    • new 키워드를 통해 객체화 가능
    • static 메소드, static 필드 선언 가능
    • 중첩 클래스 사용 가능 및 제너릭 타입으로 지정 가능

[record의 구조]

public record Gundam(String name, String pilot, String series) {}

record의 구조를 살펴보면 class 선언 시 들어가는 class 대신 record를 사용한다.
레코드명(헤더), {바디}의 구조를 가지는데 헤더에 나열되는 필드를 컴포넌트라고 부른다. 위의 예제 코드로 보면 이름은 Gundam, private final 필드를 name, pilot, series를 가진 record라고 볼 수 있다.

컴파일러는 헤더를 통해 내부 필드를 추론하는데, 이 때 String 타입의 name, pilot, series가 있다는 것을 인식하게 된다.
이후 코드에 명시적으로 접근자와 생성자, toString, equals, hashCode를 선언하지 않아도 이에 대한 구현을 자동으로 제공한다.

[컴팩트 생성자]

컴팩트 생성자는 생성자 매개 변수를 받는 부분이 사라진 형태이다.
개발자가 명시적으로 인스턴스 필드를 초기화 하지 않아도 컴팩트 생성자의 마지막 부분에 초기화 구문이 자동으로 삽입된다. 일반적으로 사용하는 표준 생성자와는 달리 컴팩트 생성자 내부에서는 인스턴스 필드에 접근할 수 없다.

이러한 이유로 컴팩트 생성자에는 컴포넌트로 들어온 값을 불변으로 만들거나 불변식이 만족하는지(유효성 체크, 예를 들어 null check) 등의 작업을 하기에 적합하다.

public record Gundam(String name, String pilot, String series) {

    public Gundam {
        Objects.requireNonNull(name);
        Objects.requireNonNull(pilot);
        Objects.requireNonNull(series);
    }
}

이렇게 선언한 컴팩트 생성자는 일반 생성자 쓰듯이 똑같이 사용하면 된다.

Gundam freedom = new Gundam("freedom_gunDam", "kira yamato", "SEED");

여기까지 보면 이 record라는 녀석은 jpa의 entity로 쓸 수 있지 않을까? 라는 생각이 든다.
데이터 전송 객체를 만들 때 나오는 자바의 한계를 극복하는 게 목적인 record를 jpa의 entity로 사용하면 entity의 구조를 단순화 하면서 직관적으로 쓸 수 있지 않을까?

📌 Record는 Entity가 아닌 DTO로 사용하자

record는 엔티티가 될 수 없는 이유는 다음과 같다.

  • hibernate와 같은 jpa는 프록시 생성을 위해 인수 생성자, non-final 필드, setter 및 non-final 클래스가 없는 엔티티에 의존한다. 즉, 프록시를 생성하기 위해서 entity는 불변이면 안된다.
    (jpa의 프록시는 일대일 매핑 시 지연 로딩 제공 등 다양하게 쓰인다.)
  • 쿼리 결과를 매핑할 때 객체를 인스턴스화 할 수 있도록 매개변수가 없는 생성자가 필요하다.
    → record는 매개변수가 없는 생성자를 제공하지 않는다. (record는 불변 객체이기 때문에 setter를 사용할 수 없다. 이로 인해 모든 필드의 값을 입력한 후에 생성할 수 있다)
  • 접근자 메소드인 getter가 필수 명명 규칙을 따르지 않는다. record의 getter는 필드명을 그대로 사용하고 있다.(name(), pilot() …)
    → 쿼리 결과 처리 후 수행할 getter, setter에 접근할 수 없다.

위의 이유로 record는 엔티티가 되기 위한 조건을 만족하지 않는다. 결국 entity는 표준 java 클래스로 구현해야 해당 기준을 쉽게 충족할 수 있습니다.

record는 목적에 맞게 쓸 수 있는 곳이 없는 걸까?
이 글을 시작하기에 앞서 살펴본 DTO가 있다. DB에 저장된 데이터의 전송 객체로서 record는 아주 적합한 형태를 가지고 있다.

읽어온 정보를 변경하지 않으려면 record 타입의 DTO가 적격이다. record는 한 번 값이 정해지고 나면 setter를 통해 값을 변경할 수 없는데, 자바 내부에서 데이터 가공 시 중간에 변질될 우려가 없다.

일단 DTO를 사용하면 API에서 도메인 모델을 분리할 수 있는데 백엔드 내부의 entity를 외부에 공개할 필요가 없게 된다.


jetBrain 자바 보고서 - 전세계 자바 버전 점유율

하지만, 여전히 한국의 자바 환경은 8버전이 주를 이루고 있으며 나머지 파이는 11버전이 차지하고 있다. record를 제대로 된 스펙으로 사용하려면 16이상의 버전을 사용해야 하는데 버전을 올렸을 때 겪을 리스크가 있다. 그러나 세계적인 추이가 점점 새로운 자바 버전의 사용이 늘고 있는 추이기 때문에 훗날을 위해서라도 record를 DTO로 사용하는 것에 대해 친숙해질 필요는 있다고 생각한다.


출처
https://openjdk.org/jeps/395
https://mostadmired.tistory.com/133
https://scshim.tistory.com/372
https://blog.hexabrain.net/399
https://stackoverflow.com/questions/70601508/can-i-use-java-16-record-with-jpa-entity
https://thorben-janssen.com/java-records-hibernate-jpa/
https://thorben-janssen.com/dto-projections/
https://wkorando.github.io/sip-of-java/015.html
https://medium.com/javarevisited/javas-records-vs-kotlin-s-data-classes-b3fef851155a
https://www.reddit.com/r/java/comments/fzuex7/will_records_become_the_standard_way_to_create/

profile
스프링 백엔드를 공부중인 대학생입니다!

0개의 댓글