티스토리 뷰
오늘 다뤄볼 주제는 자바 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 가 나옵니다.
이제 $2의 내용들을 확인해보도록 하겠습니다. $2에는 static 메소드들이 들어갑니다. 기본적인 non-static 메소드는 getter 말고 입력할 수 없습니다.
원인을 찾아보게 되면 각각의 프로퍼티 (id, name, email) 값은 final 선언이 되어 있어 처음 설정한 값으로 set이 되면 변경이 불가능하기 때문입니다 ㅠㅠ 즉, immutable 한 객체임을 알 수 있습니다.
그래서 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;
}
3. 그렇기 때문에 record는 entity 로 사용 불가능
1. Primary Key 설정 불가능
$2영역에 기본적으로 메서드 및 필드 선언에 따른 어노테이션 설정을 해주어야 합니다. 우선 @Id, @GeneratedValue 부터 사용이 불가능하기 때문에 엔티티 클래스를 만들 수 없습니다.
2. static 에서는 setter 가 제공되지 않음
값을 @GeneratedValue 정책에 맞게 자동생성하여 입력한다고 하여도, 결국은 setter 허용이 되지 않습니다.
4. DTO 로 훌륭한 재원인 record
대신 record 는 API에서 사용하는 요청 및 응답값에 탁월한 DTO 로 사용할 수 있습니다. DTO를 사용할 수 있는 조건은 아래 몇가지이기 때문입니다.
- setter 로 값을 1번만 입력해도 된다. 즉, 데이터가 바뀌지 않는다.
- 값 입력 전에 이미 Service Layer 에서 값이 변환되고 변환된 값을 1번 set 한다.
- 기본적인 동등 비교 함수나 생성자를 만들지 않아도 된다. 즉, 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
'Server' 카테고리의 다른 글
[Java] @Value Annotation static 변수로 선언하기 (0) | 2023.02.24 |
---|---|
[Java] String template 사용하기 (0) | 2023.02.11 |
[Jenkins] 배포 자동화 알림 Slack 으로 전송하기 (0) | 2023.02.06 |
[Java] 함수형 인터페이스에 대해 알아보기 (0) | 2023.02.01 |
[Spring] Security 를 사용했을 때 세션의 만료 체크하기 (0) | 2023.01.31 |
[Java] Serialization 직렬화 분석 및 확장하기 (2023-01 수정) (1) | 2023.01.30 |