[Java] record

2025. 6. 3. 11:47·Study/Java

📝 개요

기존에 자바에서 단순 데이터 전달용 객체 (DTO, VO) 는 필드, 생성자, getter, setter, equals, hashCode, toString 등 전부 만들어야 했다. 이런 Boilerplate Code 들은 실수를 야기하고 코드의 양도 많아져 가독성을 떨어트리게 된다. Java 에서 이런 반복과 실수를 줄이기 위해 Java14 에서 Preview 도입, Java16 에서 공식적으로 record 를 도입하게 된다.

Spring Framework 로 Java 기반 백엔드 서버를 개발하게 되면 DTO 와 VO 에 대한 정의가 많아지는데, 이를 record 를 통해서 대체 가능하다. record 를 알아보면서 적용된 프로젝트의 DTO 와 VO 설계를 살펴보겠다.


📀 Record

1️⃣ 특징

  • 자동 생성 : 필드, 생성자, getter(메서드명은 필드명), equals, hashCode, toString 을 자동 생성한다.
  • 불변 객체 : 모든 필드가 final 이고, setter는 존재하지 않는다. 절대 값이 바뀌지 않는다. (불변성)
  • 상속 금지 : record 는 암묵적으로 fianl 이며 상속이 불가능하다. 오직 인터페이스만 구현 가능하다.
  • 클래스 선언 간소화 : Boilerplate Code 코드를 완전 최소화 가능하다.

2️⃣ 단점

  • 상속 불가 : 공통 기능 추상화나, 계층 구조 필요 시 맞지 않다.
  • 필드 불변만 보장 : 필드가 리스트 일 때 내부 데이터는 바뀔 수 있다. (컬렉션의 내용은 불변 X)
  • 복잡한 생성자/커스텀 로직 : 생성자나 메서드에 로직이 많아지면 record 의 의미가 퇴색된다.
  • JPA Entity 불가 : JPA 는 setter, proxy, 기본 생성자 등이 필요하다. record 의 구조와는 맞지 않다.
  • 직렬화/라이브러리 호환성 : Jackson 등에서 버전에 따라 옵션이 필요하다. (완벽 호환이 안될 수 있음)

📂 사용 예시

1️⃣ DTO 예시 : Lombok 어노테이션 사용

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
    private String id;
    private String name;
    private int age;
}

2️⃣ DTO 예시 : Record 사용

public record UserDTO(String id, String name, int age) { }

✅ 프로젝트 적용 (DTO Class VS DTO Record)

사실상 Lombok 을 사용한다면 record 처럼 완벽하게 대체는 불가능하지만, Boilerplate Code 를 효과적으로 줄일 수 있다. 그러면 어떤 경우에 record 를 적용하는게 가장 적절할까?

1️⃣ Groupware 프로젝트 적용 상황

DTO 유형 record 사용 가능 여부 이유
VO (DB 매핑 객체) ✅ 가능 값이 변하지 않음 (Immutable)
ResponseDTO (응답 객체) ✅ 가능 DB 조회 후 클라이언트에 전달 (Immutable)
RequestDTO (요청 객체) ⚠️ 경우에 따라 다름 값 변경이 필요하면 class, 아니면 record 가능

Groupware 백엔드 프로젝트에서는 VO 에 대해서 Record 로 적용이 가능하지만, 정적 팩토리메서드나 기타 비즈니스 로직을 좀 더 유연하게 사용하고 테스트 코드 생성에 유연함을 주기 위해서 Class 방식을 선택했다.

ResponseDTO 객체 또한 상세조회와 같이 리스트 조회 기능과 비즈니스 로직을 공유하는 DTO 객체 같은 경우도 Class 방식을 선택했다.

RequestDTO 같은 경우 조회/검색, 토큰 응답 과 같이 단순하게 데이터만 전달하거나, 값이 한 번 생성되면 변경할 필요가 없다면 Record 를 사용하도록 했다.

Record 의 의도와 맞게 간단하고 불변성을 띄는 DTO (예 : 토큰 갱신 응답, 로그인 응답, 페이징 요청 및 응답) 만 사용했다 다음은 그 예시이다.

2️⃣ Record 사용 실제 코드 #1

// 로그인 응답 DTO : 엑세스 토큰과 사용자 정보만 반환
public record LoginResDTO(String accessToken, UserInfoDTO user) {}

// 토큰 갱신 응답 DTO : 엑세스 토큰과 만료시간 반환
public record RefreshTokenResDTO(String accessToken, long accessExpiresIn) {}

// 페이지 요청 DTO (공통) : 페이지 요청에 대한 파마리터 불변성 유지
public record PageReqDTO(
        Integer page,

        @Min(value = CommonConstants.DEFAULT_PAGE_SIZE, message = "페이지 크기는 " + CommonConstants.DEFAULT_PAGE_SIZE + "이상이어야 합니다.")
        @Max(value = CommonConstants.MAX_PAGE_SIZE, message = "페이지 크기는 최대 " + CommonConstants.MAX_PAGE_SIZE + "까지 가능합니다.")
        Integer size,

        String keyword
) {
    public PageReqDTO(Integer page, Integer size, String keyword) {
        this.page = (page == null || page < 1) ? CommonConstants.DEFAULT_PAGE : page;  // 페이지 값이 0 이하 이면 1페이지로 초기화
        this.size = (size == null) ? CommonConstants.DEFAULT_PAGE_SIZE : size;  // size 값은 사용자 입력에 따라 결정, 그 이외 값이면 Valid 로 예외차리
        this.keyword = (keyword == null || keyword.isBlank() ? null : keyword.trim());
    }

    public int getOffset() {
        return (page - 1) * size;
    }
}

// 페이지 응답 DTO (공통) : 페이지 응답에 대한 불변성 유지
public record PageResDTO<T>(List<T> dataList, int totalCount, int currentPage, int pageSize) {}

3️⃣ Record 사용 실제 코드 #2

public record ReportSearchReqDTO(
        Integer page,

        @Min(value = CommonConstants.DEFAULT_PAGE_SIZE, message = "페이지 크기는 " + CommonConstants.DEFAULT_PAGE_SIZE + "이상이어야 합니다.")
        @Max(value = CommonConstants.MAX_PAGE_SIZE, message = "페이지 크기는 최대 " + CommonConstants.MAX_PAGE_SIZE + "까지 가능합니다.")
        Integer size,

        @Min(value = 2000, message = "보고서 검색 연도는 2000년도 이상이어야 합니다.")
        @Max(value = 3000, message = "보고서 검색 연도는 3000년도 이하여야 합니다.")
        Integer reportYear,

        @Min(value = 1, message = "보고서 검색 주차는 1주차 이상이어야 합니다.")
        @Max(value = 53, message = "보고서 검색 주차는 53주차 이하여야 합니다.")
        Integer reportWeek,

        @Size(max = 100, message = "프로젝트 내용은 100자까지 검색이 가능합니다.")
        String reportProject,

        @Size(max = 100, message = "업무내용은 100자까지 검색이 가능합니다.")
        String reportWork,

        @Size(max = 50, message = "사용자 이름은 50자 이하여야 합니다.")
        String userName,

        // 권한 및 부서 정보 조회를 위한 필드
        String userDepartment,

        @Schema(hidden = true)
        String userId,

        // 이전 주차 조회를 위한 필드
        @Schema(hidden = true)
        Integer previousYear,

        @Schema(hidden = true)
        Integer previousWeek
) {
    // Record Compact Constructor
    public ReportSearchReqDTO {
        reportYear = (reportYear == null) ? DateUtil.getCurrentYear() : reportYear;  // 입력 없을 때 현재 연도 초기화
        reportWeek = (reportWeek == null) ? DateUtil.getCurrentWeek() : reportWeek;  // 입력 없을 때 현재 주차 초기화

        userId = AuthUtil.getLoginUserId();  // 현재 로그인한 사용자 ID 로 초기화

        int[] prev = DateUtil.getPreviousWeek(reportYear, reportWeek);
        previousYear = prev[0];  // previousYear : 만약 1주차 전이라면 이전 년도 반환
        previousWeek = prev[1];  // 만약 1주차 전이라면 이전 52주차 반환
    }

    public PageReqDTO toPageReqDTO() {
        return new PageReqDTO(page, size, null);
    }
}

Report 도메인의 페이지 요청 DTO 는 페이지에 대한 기본 값 설정을 Compact Constructor 로 정의하고 있다. 복잡한 비즈니스 로직 없이 초기 값을 세팅하는 정도이면 Record 의 장점을 살릴 수 있다. 또한 공통 페이지 객체로 값을 전달하여 초기화 하는 로직을 줄일 수 있다.

4️⃣ Compact Constructor

Compact Constructor 는 Record 의 생성자를 짧고 간결하게 정의하는 문법이다. 보통의 클래스 생성자처럼 필드 값 검증, 기본값 설정, 예외 처리 등을 할 수 있다. 단, 필드 선언과 동시에 파라미터가 자동생성 된다. (헤더의 필드 이름이 생성자 파라미터)

// 문법 구조
public record MyRecord(Type1 a, Type2 b) {
    public MyRecord {
        // 여기서 a, b는 이미 파라미터로 존재
        // 생성자 본문에서 검증, 초기화, 값 가공 가능
        if (a == null) throw new IllegalArgumentException("a는 필수");
        b = (b == null) ? getDefaultB() : b;
    }
}
  • this.필드명 = value 대신 파라미터명 = value 로 값 할당
  • record 는 불변이 기본이라 Compact Constructor 에서만 값을 세팅 가능하다. (생성 이후 변경 불가)

'Study > Java' 카테고리의 다른 글

[Java] ==, equals(), Objects.equals()  (1) 2025.06.30
[Java] Utility Class  (0) 2025.06.03
'Study/Java' 카테고리의 다른 글
  • [Java] ==, equals(), Objects.equals()
  • [Java] Utility Class
arraysort
arraysort
arraysort 님의 블로그 입니다.
  • arraysort
    arraysort 님의 블로그
    arraysort
  • 전체
    오늘
    어제
    • 분류 전체보기 (14)
      • Study (5)
        • Java (3)
        • DataBase (1)
        • Spring-Boot (1)
        • React (0)
        • WEB (0)
      • Project (9)
        • Backend (5)
        • Frontend (2)
        • Database (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    API
    lombok
    oracle
    backend
    DTO
    spring boot
    mabatis
    objects.eqauls()
    VO
    CDB
    FilterChain
    TypeScript
    Spring Security
    SQL
    java
    SQL Mapper
    react
    Database
    Groupware
    utilityclass
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
arraysort
[Java] record
상단으로

티스토리툴바