
Spring Boot로 개발하다 보면 DTO 클래스를 어떻게 이름 짓는지가 은근히 고민이다. 예전 프로젝트 코드를 보면 UpdateGameRequestDto, RankingResponseDto 처럼 클래스명에 Dto가 붙어있는 경우가 많다. 하지만 최근 프로젝트들을 보면 UpdateGameRequest, RankingResponse 처럼 접미사 없이 깔끔하게 작성한다.
찾아보니 이런 변화는 2010년대 중반부터 시작됐다. Spring Boot 1.x가 나오고 마이크로서비스 아키텍처가 보급되면서 패키지 구조로 역할을 구분하는 방식이 선호되기 시작했다. 지금은 대부분의 프로젝트에서 domain/{feature}/dto/request, domain/{feature}/dto/response 같은 패키지 구조를 사용한다.
// 예전 방식
public class UpdateGameRequestDto { }
// 요즘 방식
package com.example.domain.game.dto.request;
public class UpdateGameRequest { }
패키지 경로 자체가 이미 DTO임을 나타내므로, 클래스명에 중복으로 Dto를 붙일 필요가 없다. 코드가 간결해지고 IDE에서 패키지별로 그룹화되어 관리하기도 편하다.
물론 파일명만 봤을 때 역할을 바로 파악하기 어렵다는 단점도 있다. 하지만 요즘은 IDE 지원이 워낙 좋아서 패키지 구조를 쉽게 확인할 수 있고, JavaDoc 주석으로도 충분히 보완 가능하다.
Java 16부터 정식으로 도입된 Record는 DTO 작성 방식을 완전히 바꿔놨다. 기존 Class 방식으로 DTO를 만들면 Lombok 어노테이션을 여러 개 붙이고, 필드마다 private 키워드를 써야 했다.
// Class 방식 - 약 46줄
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UpdateGameRequest {
private String name;
private Integer price;
private String genre;
// ...
}
Record를 쓰면 이게 한 줄로 끝난다.
// Record 방식 - 약 18줄 (60% 감소)
@Builder
public record RankingResponse(
Integer rank,
Long userId,
String userName,
Integer score,
LocalDateTime playedAt
) {}
Record는 컴파일러가 생성자, getter, equals(), hashCode(), toString()을 자동으로 만들어준다. 필드가 모두 final이라 불변성도 보장된다. 멀티스레드 환경에서 안전하고, 코드량도 확 줄어든다.
단, getter 메서드명이 일반 Class와 다르다. getRank()가 아니라 rank() 형태다.
RankingResponse response = new RankingResponse(1, 100L, "홍길동");
Integer rank = response.rank(); // rank() 메서드로 접근
Record가 간편하긴 하지만 모든 상황에 적합한 건 아니다.
Record를 쓰면 좋은 경우:
Class를 써야 하는 경우:
MultipartFile 필드 사용)@ModelAttribute 바인딩이 필요할 때실무에서 가장 많이 겪는 케이스가 파일 업로드다. Record는 불변이라 MultipartFile 처리가 까다롭다. 이럴 땐 Class를 쓰는 게 낫다.
// 파일 업로드 - Class 사용
@Getter
@Setter
public class UpdateGameRequest {
private String name;
private MultipartFile thumbnailImage;
private Boolean deleteThumbnail;
}
실무에서는 보통 이런 구조로 관리한다.
domain/
├── game/
│ ├── dto/
│ │ ├── request/
│ │ │ ├── CreateGameRequest.java (Record)
│ │ │ └── UpdateGameRequest.java (Class)
│ │ └── response/
│ │ └── GameResponse.java (Record)
│ └── entity/
│ └── Game.java
Request는 파일 업로드 여부에 따라 Record/Class를 선택하고, Response는 거의 항상 Record를 쓴다.
Record에 정적 팩토리 메서드를 추가하면 엔티티 변환도 깔끔하게 처리할 수 있다.
public record UserGameResponse(
Long id,
Long userId,
String gameName,
Integer score
) {
public static UserGameResponse of(UserGame userGame, String gameName) {
return new UserGameResponse(
userGame.getId(),
userGame.getUserId(),
gameName,
userGame.getScore()
);
}
}
DTO 작성의 핵심은 일관성이다. 패키지 구조로 역할을 구분하고, Record를 우선 사용하되 필요한 경우에만 Class를 쓰는 게 좋다.
dto/request, dto/response로 명확히 분리이 정도만 지켜도 코드가 훨씬 깔끔해지고 유지보수하기 편해진다. Record의 불변성과 간결함, 패키지 기반 네이밍의 명확함을 활용하면 더 나은 코드를 작성할 수 있다.