티스토리 뷰

반응형

오늘 다뤄볼 주제는 자바 14버전에서 나오게 된 record 입니다. 제목에서 나와있지만 record 를 class 와 같이 적용하려다가 결국 실패하고, DTO로 사용하게 된 내용을 말씀드리려 합니다. 클래스의 내용은 자바 개발자분들은 많이 아실거라 믿고, 제 블로그에서도 JPA 관련하여 여러 가지 글을 소개한 경험이 있어 넘기기로 하고, 이번 글에서는 record에 대해 중점적으로 다뤄보려 합니다. 

목적 : 자바 record 에 대해 알아보고, Entity 는 클래스를 쓰자

가장 먼저 JPA Entity 는 클래스로 이루어져 있고, 이런 내용을 spring-data 의존성 내에서 자동으로 변환해주는 역할을 합니다. 예시가 가장 대표적이므로 예시를 보겠습니다. 아래의 내용은 앱을 사용하는 사용자의 Entity class 입니다. 

 

AppUser.java

@Entity
@Data
@SQLDelete(sql = "UPDATE user SET deleted_at = NOW() WHERE id = ?")
@Where(clause = "deleted_at is null")
@Table(name = "user")
public class AppUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long id;

    private String email;

    @JsonIgnore
    private String name;

    @JsonIgnore
    private String phone;

    @JsonIgnore
    private String password;

    @Enumerated(EnumType.STRING)
    @JsonIgnore
    private UserGrade userGrade;

    @JsonIgnore
    private String socialId;

    @JsonIgnore
    @Enumerated(EnumType.STRING)
    private SocialLoginType socialLoginType;

    @JsonIgnore
    private String recentVersion;

    @CreationTimestamp
    @JsonIgnore
    private LocalDateTime createdAt;

    @JsonIgnore
    private LocalDateTime deletedAt;
}

 

1. record 란?

자바 사용자들이 반복적으로 사용하는 보일러플레이트 코드 (getter, setter, constructor, hashCode, equals 메서드들) 들이 Lombok 과 IDE의 발달로 자동생성이 가능하게 되었습니다. 그래서 이런 불필요한 코드들을 전부 싹다 집어넣고 한 번에 개선하고 싶어 만들어진 타입(?), 객체라고 볼 수 있습니다.

 

2. record 의 생김새

그럼 여기에서 record의 구조를 간단히 파악해보고자 합니다. record의 기본 포맷은 아래와 같습니다. 

public record AppUser($1) {
	$2(static)
}

여기서 $1 에는 다양한 파라미터들이 들어갑니다. 위의 AppUser.java 를 기준으로 보았을 때는 id, email, name, password 등 다양한 정보가 들어갈 것입니다. 예시는 아래와 같습니다.

public record RecordTest(long id, String email, String name, String phone, String password, UserGrade userGrade, SocialLoginType loginType) {

}

 

이렇게 되면 생성자가 자동으로 생성됩니다. @AllArgsConstructor 가 붙은 원리와 같습니다. 즉, $1 에 들어가는 내용은 필드명입니다.

자동 완성으로 필드가 똑같이 채워졌습니다.

그리고 equals와 같은 비교도 가능합니다. 예시가 좀 길어진 관계로 조금만 줄여서 필드를 3개 기준으로 작성해보려 합니다. 다음으로 같은 객체인지 비교를 하는 equals 함수를 사용하면 class 에서는 false 가 나오지만 record 에서는 true 가 나옵니다.

record equals test

이제 $2의 내용들을 확인해보도록 하겠습니다. $2에는 static 메소드들이 들어갑니다. 기본적인 non-static 메소드는 getter 말고 입력할 수 없습니다. 

원인을 찾아보게 되면 각각의 프로퍼티 (id, name, email) 값은 final 선언이 되어 있어 처음 설정한 값으로 set이 되면 변경이 불가능하기 때문입니다 ㅠㅠ 즉, immutable 한 객체임을 알 수 있습니다.

set 을 못하는 이유 = final 이기 때문
생성자를 만들 수 없는 이유

 

그래서 of 메서드와 같은 static 메서드의 생성은 가능합니다. 하지만 불편하게도 결국은 다 맞춰주어야 하는 부분은 어쩔수 없이 존재합니다. 아래와 같이 Map으로 넣어도 무방하지만, 통상적으로 email, name 을 넣을것이면 기본 생성자를 쓰는게 낫습니다. 

public RecordTest build() {
    long id = 1L;
    String email = "email@gmail.com";
    String name = "abbo";
    Map<String, String> map = Map.of("email", email,
        "name", name);
    RecordTest staticBuilder = RecordTest.of(id, map);
    return staticBuilder;
}

컴파일 에러가 나지 않는 of 생성자

 

3. 그렇기 때문에 record는 entity 로 사용 불가능

1. Primary Key 설정 불가능

$2영역에 기본적으로 메서드 및 필드 선언에 따른 어노테이션 설정을 해주어야 합니다. 우선 @Id, @GeneratedValue 부터 사용이 불가능하기 때문에 엔티티 클래스를 만들 수 없습니다. 

 

2. static 에서는 setter 가 제공되지 않음

값을 @GeneratedValue 정책에 맞게 자동생성하여 입력한다고 하여도, 결국은 setter 허용이 되지 않습니다. 

 

4. DTO 로 훌륭한 재원인 record

대신 record 는 API에서 사용하는 요청 및 응답값에 탁월한 DTO 로 사용할 수 있습니다. DTO를 사용할 수 있는 조건은 아래 몇가지이기 때문입니다.

  1. setter 로 값을 1번만 입력해도 된다. 즉, 데이터가 바뀌지 않는다. 
  2. 값 입력 전에 이미 Service Layer 에서 값이 변환되고 변환된 값을 1번 set 한다. 
  3. 기본적인 동등 비교 함수나 생성자를 만들지 않아도 된다. 즉, new DTO(1) == new DTO(1) 이다.

 

EntityManager 에서 가져올 수 있는 NativeQuery 예제를 한 번 보겠습니다.

private List<RecordTest> getRecordList(String name, String email) {
    String query = "SELECT id, name, email FROM app_user WHERE name = ?1 OR email = ?2";
    final Query nativeQuery = em.createNativeQuery(query, RecordTest.class);
    if(StringUtils.isNotBlank(name)) {
        nativeQuery.setParameter(1, name);
    }
    if(StringUtils.isNotBlank(name) && StringUtils.isNotBlank(email)) {
        nativeQuery.setParameter(2, email);
    }
    final List<RecordTest> resultList = nativeQuery.getResultList();
    return resultList;
}

 

RecordTest.java

public record RecordTest(long id, String email, String name) {
    
}

기본적인 SELECT 쿼리에서 가져오는 값의 타입을 Record을 넣고 record 로 생성하여도 컴파일 에러가 나지 않고 결과를 손쉽게 가져올 수 있습니다. 

아니면 ModelMapper 를 사용하여 @PostMapping 요청을 받아 데이터를 입력하는 과정중에 사용하는 것도 방법이지 않을까요?

@RestController
@RequestMapping("/api/record")
record RecordController(AppUserRepository appUserRepository, ModelMapper modelMapper) {

    @PostMapping
    public RecordCreatePayload insert(RecordTest param) throws Exception {
        AppUser appUser = modelMapper.map(param, AppUser.class);
        AppUser savedUser = appUserRepository.save(appUser);
        return RecordCreatePayload.of(savedUser);
    }
}

 


 

결론 : record 는 DTO에서만 쓰기!

 

참고한 내용입니다. 

https://thorben-janssen.com/java-records-hibernate-jpa/

 

Java Records - How to use them with Hibernate and JPA

Java records seem to be a perfect match for your persistence layer. But there are several limitations. Learn how to use Java records with JPA and Hibernate.

thorben-janssen.com

https://stackoverflow.com/questions/70601508/can-i-use-java-16-record-with-jpa-entity

 

Can I use Java 16 record with JPA entity?

I am trying to do something similar like below. @Entity @Table(name="Sample") public record Sample(Integer id, String name) { @Id @GeneratedValue(strategy = GenerationType.IDENTI...

stackoverflow.com

 

반응형
댓글
공지사항