DTO는 record가 답일까

Lui.Slki·2026년 1월 14일

개발 성장일지

목록 보기
2/6


개인 프로젝트를 진행하다가 DTO에 관해 조금 더 확실하게 정리를 하고 가면 좋을것같다는 생각이 들었다.

웹개발 수업으로 처음 스프링 프레임워크를 접했을 때, 수업은 JSP / MyBatis 위주로 진행되었다. DTO도 자연스럽게 class로 만들게 되었는데, 프레임워크의 편의성(JPA 같은 것들)보다 ‘뜨거운 커스텀의 맛’을 먼저 보는 바람에 당시에는 계속 “이게 뭔데…” 스탠스를 유지할 수밖에 없었다.

그러다 이후에 record로 DTO를 작성할 기회가 많아졌고, 문득 이런 생각이 들었다.

“예전엔 class로 만들었는데, 지금은 record로도 만들잖아? 그럼 둘은 정확히 뭐가 다르지?”

이 글은 class DTO와 record DTO의 차이, 그리고 내 프로젝트에서 어떤 기준으로 선택하면 좋은지를 정리해보는 글이다.

DTO

DTO(Data Transfer Object)는 말 그대로 데이터를 전달하기 위한 객체다.
여기서 중요한 포인트는 "객체"가 아닌 전달(Transfer)이다.

즉, DTO는 보통 이런 상황에서 등장한다.

  • 클라이언트(프론트) <-> 서버(백엔드) 가 데이터를 주고받을 때
  • 컨트롤러 <-> 서비스/도메인 사이에서 데이터를 옮길 때

DTO의 역할은 간단하다.
"필요한 데이터만, 약속된 형태로, 안전하게 전달하기."

왜 DTO를 따로 만들까?

  1. 필요 없는 정보까지 노출될 가능성
    예: User 엔티티에 password, role, 내부 상태값이 있는데 응답으로 그대로 나가버릴 수도 있음
  2. API 응답 스펙이 엔티티에 끌려다님
    화면에 필요한 응답은 바뀌는데, 그때마다 DB 설계(Entity)를 건드릴 순 없음
  3. 화면에 맞게 가공해서 보내기 어렵다
    현재 진행하고있는 프로젝트에서 이름 + 악기 + 레슨 가격을 한 번에 내려줘야하는 상황이 있는데, 엔티티 구조가 그대로면 비효율적이다.

그래서 DTO는 "엔티티/도메인과 별개로" API 계약서 역할을 해준다.

택배 상자를 예로 들어보자.

엔티티가 "집 안의 물건 전체"라면, DTO는 "택배 상자" 같은 느낌이다.

  • 집(서버 내부)에는 물건이 많고, 민감한 것도 있고, 정리도 제각각이다.
  • 근데 택배(응답(Response)/요청(Request))로 보낼 때는
    - 필요한 것만
    - 정해진 포장 방식으로
    - 안전하게 보내야함
    즉, DTO는 "택배 상자에 담을 목록 + 포장 규격"이다.

결론적으로, DTO는 "필요한 정보만 담는 약속된 그릇"이다.

  • DTO = 데이터 전달 목적
  • Entity = DB와 도메인 모델링이 목적

여기까지는 이해했고, 해당 목적에 맞게 프로젝트에 적용할 수 있다.
하지만 class vs record 사용방식 차이를 설명하라고 한다면?

"편하니까 record." 라고 밖에 대답 못하겠다.

그래서 class DTO와 record DTO는 뭐가 다를까?

record를 사용하면 class 로 만들 때보다 코드가 짧아지고, getter 만들 필요도 없고, IDE가 자동으로 다 해주는 느낌이라 편리하다.

하지만 계속 증식하는 DTO를 바라보고 있자니 record가 편한 이유는 단순히 "짧아서"가 아니었다.
DTO의 목적(전달/계약)에 더 잘 맞는 방향으로 코드를 강제하기 때문이다.

핵심 차이는
class DTO는 '가변 객체'가 되기 쉽고, record DTO는 불변 계약을 기본값으로 가진다.

1) 가장 큰 차이: “불변(Immutable)” vs “가변(Mutable)"

DTO는 아까 말했듯 "필요한 데이터만 약속된 형태로 전달" 하는 역할이다.
그 말은 곧, 한번 만들어진 DTO가 중간에 바뀌지 않는 게 안전하다는 뜻이기도 하다.

class DTO(가변이 되기 쉬움)

class는 기본적으로 setter를 열어두기 쉽다.

  • 처음에는 "바인딩이 편하니까"
  • 나중에는 "값 수정할 일이 있을 수도 있으니까"
  • 그러다가 어느 순간 DTO가 중간에 값이 바뀌는 객체가 되어버린다.

이게 왜 문제냐면, DTO는 원래 "계약서"인데
계약서가 중간에 수정되면 어디서 값이 바뀌었는지 추적이 어려워진다.

record DTO(불변이 기본값)

record는 생성 시점에 값이 정해지고, 그 이후에는 바꿀 수 없다.
즉, DTO를 record로 작성하는 순간 코드 자체가 "이 DTO는 전달용이고, 한 번 만들어지면 변하면 안된다" 고 말하는 셈이다.

DTO목적에 딱 맞는 성질을 언어 차원에서 기본값으로 제공하는 게 record의 제일 큰 장점이다.

2) "계약서" 로서의 명확함 : 생성자 한 방에 스펙이 고정됨

class DTO는 필드가 많아질 수록 해당 패턴이 흔하다.

  • 기본 생성자 만드로
  • setter로 하나씩 채우고
  • 값이 다 채워졌는지 확신은 없음
    record는 생성자 한 번으로 스펙이 확정된다.
  • 어떤 값이 DTO를 구성하는지 한 줄에 보이고
  • 필수 값이 빠지면 컴파일/실행 단계에서 바로 티가 난다.
    "DTO는 계약" 이라는 관점에서 record가 훨씬 직관적이다.

3) 보일러플레이트: "짧아져서 편하다" 는 사실 맞다

record는 다음을 자동으로 제공한다

  • getter(정확히는 component accessor)
  • equals / hashCode
  • toString
  • canonical constructor
    그래서 코드량이 줄고 실수 포인트도 줄어든다.

하지만 중요한건 "코드가 짧다"가 아니라
DTO가 DTO답게 유지되도록 돕는다는 점이다.(일관성 유지)

그럼에도 class DTO가 존재하는 이유

record로 대부분 커버가 되긴 하지만, 프로젝트를 진행하다 보면 DTO가 "계약서"가 아닌 상태/과정/확장에 가까워지는 순간이 잇다.
그럴때는 class가 더 자연스럽거나, 팀/도구 환경상 class가 편한 경우가 생긴다.

1) 단계적으로 채워지는 요청(임시저장 / Draft / Step Form)

예를 들어 신청서처럼

  • 소개 먼저 저장
  • 경력 나중에 저장
  • 1선택 추가
  • 2태그 추가
    이런 식으로 값이 순차적으로 쌓이는 요청은, DTO가 이미 "완성된 계약" 이라기 보다 "작성 중인 문서"가 된다.
    record는 불변이라 매 단계마다 새 객체를 만들어야 하고, 경우에 따라 null/Optional이 많아져 오히려 복잡해질 수 있다.
    이럴 땐 class가 더 자연스럽다.

2) 상속/버전 분기 같은 "확장 구조"가 필요할 때

record는 상속이 불가능하다.
공통 베이스 DTO를 두고 v1/v2 로 확장한다거나, 계증 구조를 활용하는 설계를 택하면 class를 써야한다.

3) DTO에 "변환/조립 책임"을 섞는 스타일일 때

예전에 내가 만들었던 DTO처럼 요청 DTO 안에 toUser(), toOAuth2User() 같은 변환 메서드를 넣는 경우가 있다.
이건 DTO가 순수 전달 객체라기보다 “조립/변환기” 역할까지 가지게 되는 형태라서, class로 두는 편이 자연스럽게 느껴질 수 있다.
(물론 record에서도 메서드는 가능하지만, 설계 의도 자체가 달라진다.)

내가 이해한 결론

잘 쓰면 class DTO든 record DTO든 결과는 거의 같다.
생성자 한 번에 값을 채워서(new response 같은) "완성된 DTO"를 만들고, 이후에는 건드리지 않는다면 둘 다 안전하게 DTO 역할을 한다.

차이는 기능 차이보다 실수 방지(가드레일) 에 가깝다.
record는 기본이 불변이라 setter 기반의 "미완성 DTO/중간 수정" 패턴이 구조적으로 나오기 어렵고, 코드만 봐도 "이건 계약/전달용 데이터 묶음" 이라는 의도가 명확하다.

반대로 class는 유연한 만큼 팀이 규칙을 안 세우면 DTO가 점점 "상태를 가진 객체" 처럼 변질되기 쉽다.
그래서 나는 기본값을 record로 두고, "작성 중" 흐름이 필요한 경우 class를 고려하기로 했다.

결국 record가 편한 이유도 “짧아서”라기보다, DTO를 DTO답게 유지하게 해주는 기본값이기 때문이다.

0개의 댓글