본문 바로가기
Server/🌱 Spring Boot (java)

[Spring Boot] Record 객체를 이용하여 DTO 작성

by 코딩하는 동현😎 2025. 1. 25.

Java record란?

Java record는 Java 14에서 도입된 기능이다. 주로 데이터 전달 객체(DTO)와 같은 불변 객체를 정의할 때 사용된다. record는 클래스를 정의할 때 필드, 생성자, 접근자, toString(), equals() 및 hashCode() 메서드를 자동으로 생성하여 코드가 간결해진다.

 

주요 특징은 다음과 같다:

1. 불변성 (Immutability):

record는 기본적으로 불변 객체이다. 생성 후 상태를 변경할 수 없다.

2. 자동 생성되는 메서드들:

record는 필드를 기반으로 toString(), equals(), hashCode(), getter 메서드를 자동으로 생성한다.

3. 간결한 문법:

record는 클래스를 정의하는 것보다 간단한 문법을 제공한다.


기존 class를 이용해서 DTO 작성 방식

기존 class를 이용하는 방식으로는 세 가지 DTO 클래스(HouseInfoDto, RoomDto, RoommateDto)를 lombok으로 관리한다고 해도 다음과 같은 불편함이 존재한다.

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HouseInfoDto {
    private Long houseId;
    private String name;
    private String mainImgUrl;
    private String monthlyRent;
    private String deposit;
    private String location;
    private String occupancyTypes;
    private String occupancyStatus;
    private String genderPolicy;
    private int contractTerm;
    private List<String> moodTags;
    private String roomMood;
    private List<String> groundRule;
    private int maintenanceCost;
    private boolean isPinned;
    private List<String> safetyLivingFacility;
    private List<String> kitchenFacility;
}

@Data
@Builder
public class RoomDto {
    private Long roomId;
    private String name;
    private boolean status;
    private int occupancyType;
    private String gender;
    private int deposit;
    private int prepaidUtilities;
    private int monthlyRent;
    private String contractPeriod;
    private String managementFee;
}

@Data
@Builder
public class RoommateDto {
    private String name;
    private int age;
    private String job;
    private String mbti;
    private String sleepTime;
    private String activityTime;
}
  1. 중복된 Boilerplate(별 수정 없이 반복적으로 사용되는 코드) 코드:
    • DTO 클래스마다 @Data, @Builder, @NoArgsConstructor, @AllArgsConstructor를 반복적으로 선언해야 했음.
    • 이는 코드가 불필요하게 길어지고 유지보수의 부담을 가중시킴.
  2. 일관된 데이터 구조 제공 부족:
    • 특정 엔드포인트에서 HouseInfoDto, RoomDto, RoommateDto를 조합하여 응답을 반환할 때, 이 세 클래스가 독립적으로 관리되다 보니 직관적인 구조를 제공하기 어려웠음.
    • 이로 인해 API 응답 객체를 파악하기 힘들고, 가독성이 떨어지는 JSON 응답 구조가 만들어질 수 있었음.
  3. 코드 유지보수 및 확장성 저하:
    • 연관된 클래스들이 분리되어 있어 변경사항이 발생하면 각각의 파일을 수정해야 했고, 이로 인해 유지보수성이 저하됨.
    • 특히 연관성이 높은 데이터 구조를 한곳에서 확인하고 관리할 수 있는 장점이 사라졌음.

Record를 이용해서 DTO 작성

 

이러한 문제를 해결하기 위해 다음과 같은 방식을 적용하였다.

public record HouseDetailsResponseDto (
    HouseInfoDto houseInfo,
    List<RoomDto> rooms,
    List<RoommateDto> roommates
) {
    @Builder
    public HouseDetailsResponseDto{
    }
    public record HouseInfoDto (
        Long houseId,
        String name,
        String mainImgUrl,
        String monthlyRent,
        String deposit,
        String location,
        String occupancyTypes,
        String occupancyStatus,
        String genderPolicy,
        int contractTerm,
        List<String> moodTags,
        String roomMood,
        List<String> groundRule,
        int maintenanceCost,
        boolean isPinned,
        List<String> safetyLivingFacility,
        List<String> kitchenFacility
    ) {
        @Builder
        public HouseInfoDto{
        }
    }
    public record RoomDto (
            Long roomId,
            String name,
            boolean status,
            int occupancyType,
            String gender,
            int deposit,
            int prepaidUtilities,
            int monthlyRent,
            String contractPeriod,
            String managementFee
    ) {
        @Builder
        public RoomDto {
        }
    }
    public record RoommateDto (
            String name,
            int age,
            String job,
            String mbti,
            String sleepTime,
            String activityTime
    ) {
        @Builder
        public RoommateDto {
        }
    }
}
  1. Record로 전환:
    • Java의 record는 간결한 문법을 제공하여 데이터 모델을 작성할 때 반복적인 boilerplate 코드를 제거해줌.
    • 기존 DTO에서 사용되던 @Data, @Builder, @NoArgsConstructor, @AllArgsConstructor를 제거하고, record 문법으로 대체함으로써 클래스 정의를 단순화함.
  2. 단일 응답 객체로 통합:
    • HouseDetailsResponseDto를 생성하여 HouseInfoDto, RoomDto, RoommateDto를 하나의 응답 객체로 묶음.
    • 응답 데이터의 구조를 명확히 하고 관련 데이터 간의 관계를 직관적으로 보여줄 수 있도록 설계함.

 

ApiResponseDto record 활용(BaseResponse)

아래와 같이 base response record 작성하고 contoller layer에서 활용한다.

package server.producer.common.dto;

import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.annotation.Nullable;
import lombok.Builder;
import lombok.NonNull;
import org.springframework.http.HttpStatus;
import server.producer.common.dto.enums.ErrorCode;
import server.producer.common.dto.enums.SuccessCode;

@Builder
public record ApiResponseDto<T>(
		int code,
		@NonNull String message,
		@JsonInclude(value = NON_NULL) T data
) {
	public static <T> ApiResponseDto<T> success(final SuccessCode successCode, @Nullable final T data) {
		return ApiResponseDto.<T>builder()
				.code(successCode.getCode())
				.message(successCode.getMessage())
				.data(data)
				.build();
	}

	public static <T> ApiResponseDto<T> success(final SuccessCode successCode) {
		return ApiResponseDto.<T>builder()
				.code(successCode.getCode())
				.message(successCode.getMessage())
				.data(null)
				.build();
	}

	public static <T> ApiResponseDto<T> fail(final ErrorCode errorCode) {
		return ApiResponseDto.<T>builder()
				.code(errorCode.getCode())
				.message(errorCode.getMessage())
				.data(null)
				.build();
	}
}

Contoller Layer에서 적용

@GetMapping("/{houseId}/details")
    public ApiResponseDto<HouseDetailsResponseDto> getHouseDetails(@PathVariable Long houseId) {
        try{
            HouseDetailsResponseDto responseDto = houseService.getHouseDetails(houseId, userId);
            return ApiResponseDto.success(SuccessCode.HOUSE_DETAIL_GET_SUCCESS, responseDto);
        } catch (EntityNotFoundException e) {
            return ApiResponseDto.fail(ErrorCode.HOUSE_NOT_FOUND);
        } catch (InvalidParameterException e) {
            return ApiResponseDto.fail(ErrorCode.INVALID_PARAMETER);
        } catch (Exception e){
            return ApiResponseDto.fail(ErrorCode.INTERNAL_SERVER_ERROR);
        }
    }
반응형

댓글